focus_nfe 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (206) hide show
  1. checksums.yaml +7 -0
  2. data/.git-hooks/pre_push/steep.rb +18 -0
  3. data/.git-hooks/pre_push/yard_doc.rb +18 -0
  4. data/.gitattributes +1 -0
  5. data/.overcommit.yml +69 -0
  6. data/.rspec +3 -0
  7. data/.yardopts +11 -0
  8. data/CHANGELOG.md +77 -0
  9. data/CLAUDE.md +118 -0
  10. data/CODE_OF_CONDUCT.md +10 -0
  11. data/LICENSE.txt +21 -0
  12. data/README.md +348 -0
  13. data/Rakefile +105 -0
  14. data/data/schemas/schema_cte.json +2793 -0
  15. data/data/schemas/schema_cte_os.json +1335 -0
  16. data/data/schemas/schema_cte_os_transporte_rodoviario.json +109 -0
  17. data/data/schemas/schema_cte_transporte_aereo.json +115 -0
  18. data/data/schemas/schema_cte_transporte_aquaviario.json +174 -0
  19. data/data/schemas/schema_cte_transporte_dutoviario.json +65 -0
  20. data/data/schemas/schema_cte_transporte_ferroviario.json +144 -0
  21. data/data/schemas/schema_cte_transporte_multimodal.json +45 -0
  22. data/data/schemas/schema_cte_transporte_rodoviario.json +78 -0
  23. data/data/schemas/schema_dce.json +549 -0
  24. data/data/schemas/schema_mdfe.json +1102 -0
  25. data/data/schemas/schema_mdfe_transporte_aereo.json +44 -0
  26. data/data/schemas/schema_mdfe_transporte_aquaviario.json +209 -0
  27. data/data/schemas/schema_mdfe_transporte_ferroviario.json +99 -0
  28. data/data/schemas/schema_mdfe_transporte_rodoviario.json +628 -0
  29. data/data/schemas/schema_nfcom.json +1859 -0
  30. data/data/schemas/schema_nfe.json +4750 -0
  31. data/data/schemas/schema_nfe_forma_pagamento.json +97 -0
  32. data/data/schemas/schema_nfe_item.json +2574 -0
  33. data/data/schemas/schema_nfgas.json +2316 -0
  34. data/data/schemas/schema_nfse_nacional.json +1847 -0
  35. data/data/schemas/schema_nfse_recebida.json +548 -0
  36. data/lib/focus_nfe/client.rb +162 -0
  37. data/lib/focus_nfe/configuration.rb +104 -0
  38. data/lib/focus_nfe/errors.rb +123 -0
  39. data/lib/focus_nfe/esquemas/campo.rb +171 -0
  40. data/lib/focus_nfe/esquemas/catalogo.rb +34 -0
  41. data/lib/focus_nfe/esquemas/decimal.rb +66 -0
  42. data/lib/focus_nfe/esquemas/esquema.rb +72 -0
  43. data/lib/focus_nfe/esquemas/validador.rb +87 -0
  44. data/lib/focus_nfe/http/adapter.rb +25 -0
  45. data/lib/focus_nfe/http/adapters/net_http.rb +99 -0
  46. data/lib/focus_nfe/http/authentication.rb +23 -0
  47. data/lib/focus_nfe/http/connection.rb +118 -0
  48. data/lib/focus_nfe/http/logging.rb +100 -0
  49. data/lib/focus_nfe/http/response.rb +75 -0
  50. data/lib/focus_nfe/modelos/documento.rb +113 -0
  51. data/lib/focus_nfe/modelos/inutilizacao.rb +75 -0
  52. data/lib/focus_nfe/modelos/pagina.rb +54 -0
  53. data/lib/focus_nfe/recursos/backups.rb +17 -0
  54. data/lib/focus_nfe/recursos/base.rb +91 -0
  55. data/lib/focus_nfe/recursos/ceps.rb +16 -0
  56. data/lib/focus_nfe/recursos/cfops.rb +16 -0
  57. data/lib/focus_nfe/recursos/cnaes.rb +16 -0
  58. data/lib/focus_nfe/recursos/cnpjs.rb +13 -0
  59. data/lib/focus_nfe/recursos/concerns/baixavel.rb +41 -0
  60. data/lib/focus_nfe/recursos/concerns/baixavel_eventos.rb +34 -0
  61. data/lib/focus_nfe/recursos/concerns/cancelavel.rb +25 -0
  62. data/lib/focus_nfe/recursos/concerns/conciliavel.rb +66 -0
  63. data/lib/focus_nfe/recursos/concerns/consultavel.rb +26 -0
  64. data/lib/focus_nfe/recursos/concerns/corrigivel.rb +45 -0
  65. data/lib/focus_nfe/recursos/concerns/corrigivel_cte.rb +60 -0
  66. data/lib/focus_nfe/recursos/concerns/emitivel.rb +51 -0
  67. data/lib/focus_nfe/recursos/concerns/enviavel.rb +40 -0
  68. data/lib/focus_nfe/recursos/concerns/eventavel.rb +46 -0
  69. data/lib/focus_nfe/recursos/concerns/inutilizavel.rb +96 -0
  70. data/lib/focus_nfe/recursos/concerns/listavel.rb +22 -0
  71. data/lib/focus_nfe/recursos/concerns/localizavel.rb +22 -0
  72. data/lib/focus_nfe/recursos/concerns/notificavel.rb +23 -0
  73. data/lib/focus_nfe/recursos/concerns/removivel.rb +20 -0
  74. data/lib/focus_nfe/recursos/concerns/visualizavel.rb +28 -0
  75. data/lib/focus_nfe/recursos/cte.rb +35 -0
  76. data/lib/focus_nfe/recursos/cte_os.rb +29 -0
  77. data/lib/focus_nfe/recursos/ctes_recebidas.rb +38 -0
  78. data/lib/focus_nfe/recursos/dce.rb +16 -0
  79. data/lib/focus_nfe/recursos/emails_bloqueados.rb +31 -0
  80. data/lib/focus_nfe/recursos/empresas.rb +35 -0
  81. data/lib/focus_nfe/recursos/mdfe.rb +78 -0
  82. data/lib/focus_nfe/recursos/municipios.rb +60 -0
  83. data/lib/focus_nfe/recursos/ncms.rb +16 -0
  84. data/lib/focus_nfe/recursos/nfce.rb +19 -0
  85. data/lib/focus_nfe/recursos/nfcom.rb +16 -0
  86. data/lib/focus_nfe/recursos/nfe.rb +106 -0
  87. data/lib/focus_nfe/recursos/nfes_recebidas.rb +56 -0
  88. data/lib/focus_nfe/recursos/nfgas.rb +16 -0
  89. data/lib/focus_nfe/recursos/nfse.rb +18 -0
  90. data/lib/focus_nfe/recursos/nfse_nacional.rb +16 -0
  91. data/lib/focus_nfe/recursos/nfses_nacionais_recebidas.rb +21 -0
  92. data/lib/focus_nfe/recursos/webhooks.rb +22 -0
  93. data/lib/focus_nfe/version.rb +6 -0
  94. data/lib/focus_nfe/webhook.rb +68 -0
  95. data/lib/focus_nfe.rb +124 -0
  96. data/sig/focus_nfe/client.rbs +38 -0
  97. data/sig/focus_nfe/configuration.rbs +29 -0
  98. data/sig/focus_nfe/errors.rbs +59 -0
  99. data/sig/focus_nfe/esquemas/campo.rbs +47 -0
  100. data/sig/focus_nfe/esquemas/catalogo.rbs +8 -0
  101. data/sig/focus_nfe/esquemas/decimal.rbs +25 -0
  102. data/sig/focus_nfe/esquemas/esquema.rbs +30 -0
  103. data/sig/focus_nfe/esquemas/validador.rbs +20 -0
  104. data/sig/focus_nfe/http/adapter.rbs +7 -0
  105. data/sig/focus_nfe/http/adapters/net_http.rbs +25 -0
  106. data/sig/focus_nfe/http/authentication.rbs +9 -0
  107. data/sig/focus_nfe/http/connection.rbs +32 -0
  108. data/sig/focus_nfe/http/logging.rbs +30 -0
  109. data/sig/focus_nfe/http/response.rbs +28 -0
  110. data/sig/focus_nfe/modelos/documento.rbs +34 -0
  111. data/sig/focus_nfe/modelos/inutilizacao.rbs +24 -0
  112. data/sig/focus_nfe/modelos/pagina.rbs +21 -0
  113. data/sig/focus_nfe/recursos/backups.rbs +7 -0
  114. data/sig/focus_nfe/recursos/base.rbs +25 -0
  115. data/sig/focus_nfe/recursos/ceps.rbs +10 -0
  116. data/sig/focus_nfe/recursos/cfops.rbs +10 -0
  117. data/sig/focus_nfe/recursos/cnaes.rbs +10 -0
  118. data/sig/focus_nfe/recursos/cnpjs.rbs +7 -0
  119. data/sig/focus_nfe/recursos/concerns/baixavel.rbs +12 -0
  120. data/sig/focus_nfe/recursos/concerns/baixavel_eventos.rbs +14 -0
  121. data/sig/focus_nfe/recursos/concerns/cancelavel.rbs +9 -0
  122. data/sig/focus_nfe/recursos/concerns/conciliavel.rbs +15 -0
  123. data/sig/focus_nfe/recursos/concerns/consultavel.rbs +9 -0
  124. data/sig/focus_nfe/recursos/concerns/corrigivel.rbs +15 -0
  125. data/sig/focus_nfe/recursos/concerns/corrigivel_cte.rbs +17 -0
  126. data/sig/focus_nfe/recursos/concerns/emitivel.rbs +14 -0
  127. data/sig/focus_nfe/recursos/concerns/enviavel.rbs +15 -0
  128. data/sig/focus_nfe/recursos/concerns/eventavel.rbs +12 -0
  129. data/sig/focus_nfe/recursos/concerns/inutilizavel.rbs +20 -0
  130. data/sig/focus_nfe/recursos/concerns/listavel.rbs +9 -0
  131. data/sig/focus_nfe/recursos/concerns/localizavel.rbs +9 -0
  132. data/sig/focus_nfe/recursos/concerns/notificavel.rbs +9 -0
  133. data/sig/focus_nfe/recursos/concerns/removivel.rbs +9 -0
  134. data/sig/focus_nfe/recursos/concerns/visualizavel.rbs +9 -0
  135. data/sig/focus_nfe/recursos/cte.rbs +17 -0
  136. data/sig/focus_nfe/recursos/cte_os.rbs +17 -0
  137. data/sig/focus_nfe/recursos/ctes_recebidas.rbs +14 -0
  138. data/sig/focus_nfe/recursos/dce.rbs +10 -0
  139. data/sig/focus_nfe/recursos/emails_bloqueados.rbs +12 -0
  140. data/sig/focus_nfe/recursos/empresas.rbs +12 -0
  141. data/sig/focus_nfe/recursos/mdfe.rbs +21 -0
  142. data/sig/focus_nfe/recursos/municipios.rbs +19 -0
  143. data/sig/focus_nfe/recursos/ncms.rbs +10 -0
  144. data/sig/focus_nfe/recursos/nfce.rbs +12 -0
  145. data/sig/focus_nfe/recursos/nfcom.rbs +10 -0
  146. data/sig/focus_nfe/recursos/nfe.rbs +23 -0
  147. data/sig/focus_nfe/recursos/nfes_recebidas.rbs +16 -0
  148. data/sig/focus_nfe/recursos/nfgas.rbs +10 -0
  149. data/sig/focus_nfe/recursos/nfse.rbs +11 -0
  150. data/sig/focus_nfe/recursos/nfse_nacional.rbs +10 -0
  151. data/sig/focus_nfe/recursos/nfses_nacionais_recebidas.rbs +11 -0
  152. data/sig/focus_nfe/recursos/webhooks.rbs +11 -0
  153. data/sig/focus_nfe/webhook.rbs +11 -0
  154. data/sig/focus_nfe.rbs +10 -0
  155. data/spec/focus_nfe/client_spec.rb +208 -0
  156. data/spec/focus_nfe/configuration_spec.rb +121 -0
  157. data/spec/focus_nfe/errors_mapping_spec.rb +68 -0
  158. data/spec/focus_nfe/errors_spec.rb +107 -0
  159. data/spec/focus_nfe/esquemas/campo_spec.rb +291 -0
  160. data/spec/focus_nfe/esquemas/decimal_spec.rb +54 -0
  161. data/spec/focus_nfe/esquemas/esquema_spec.rb +73 -0
  162. data/spec/focus_nfe/esquemas/validador_spec.rb +167 -0
  163. data/spec/focus_nfe/esquemas_spec.rb +42 -0
  164. data/spec/focus_nfe/http/adapter_spec.rb +8 -0
  165. data/spec/focus_nfe/http/adapters/net_http_spec.rb +181 -0
  166. data/spec/focus_nfe/http/authentication_spec.rb +24 -0
  167. data/spec/focus_nfe/http/connection_spec.rb +255 -0
  168. data/spec/focus_nfe/http/logging_spec.rb +83 -0
  169. data/spec/focus_nfe/http/response_spec.rb +161 -0
  170. data/spec/focus_nfe/modelos/documento_spec.rb +150 -0
  171. data/spec/focus_nfe/modelos/inutilizacao_spec.rb +91 -0
  172. data/spec/focus_nfe/modelos/pagina_spec.rb +77 -0
  173. data/spec/focus_nfe/recursos/backups_spec.rb +29 -0
  174. data/spec/focus_nfe/recursos/base_spec.rb +56 -0
  175. data/spec/focus_nfe/recursos/ceps_spec.rb +16 -0
  176. data/spec/focus_nfe/recursos/cfops_spec.rb +16 -0
  177. data/spec/focus_nfe/recursos/cnaes_spec.rb +20 -0
  178. data/spec/focus_nfe/recursos/cnpjs_spec.rb +11 -0
  179. data/spec/focus_nfe/recursos/concerns/emitivel_validacao_spec.rb +158 -0
  180. data/spec/focus_nfe/recursos/cte_os_spec.rb +9 -0
  181. data/spec/focus_nfe/recursos/cte_spec.rb +9 -0
  182. data/spec/focus_nfe/recursos/ctes_recebidas_spec.rb +56 -0
  183. data/spec/focus_nfe/recursos/dce_spec.rb +8 -0
  184. data/spec/focus_nfe/recursos/emails_bloqueados_spec.rb +29 -0
  185. data/spec/focus_nfe/recursos/empresas_spec.rb +45 -0
  186. data/spec/focus_nfe/recursos/mdfe_spec.rb +100 -0
  187. data/spec/focus_nfe/recursos/municipios_spec.rb +58 -0
  188. data/spec/focus_nfe/recursos/ncms_spec.rb +16 -0
  189. data/spec/focus_nfe/recursos/nfce_spec.rb +10 -0
  190. data/spec/focus_nfe/recursos/nfcom_spec.rb +8 -0
  191. data/spec/focus_nfe/recursos/nfe_spec.rb +262 -0
  192. data/spec/focus_nfe/recursos/nfes_recebidas_spec.rb +87 -0
  193. data/spec/focus_nfe/recursos/nfgas_spec.rb +8 -0
  194. data/spec/focus_nfe/recursos/nfse_nacional_spec.rb +8 -0
  195. data/spec/focus_nfe/recursos/nfse_spec.rb +9 -0
  196. data/spec/focus_nfe/recursos/nfses_nacionais_recebidas_spec.rb +17 -0
  197. data/spec/focus_nfe/recursos/webhooks_spec.rb +22 -0
  198. data/spec/focus_nfe/webhook_spec.rb +66 -0
  199. data/spec/focus_nfe_global_configuration_spec.rb +70 -0
  200. data/spec/focus_nfe_require_spec.rb +87 -0
  201. data/spec/focus_nfe_spec.rb +11 -0
  202. data/spec/spec_helper.rb +58 -0
  203. data/spec/support/shared_examples/recurso_fiscal.rb +445 -0
  204. data/spec/support/shared_examples/recurso_leitura.rb +217 -0
  205. data/tools/pull_fields.rb +62 -0
  206. metadata +420 -0
@@ -0,0 +1,162 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FocusNfe
4
+ # Raiz de acesso à API. Cada cliente carrega sua própria {Configuration} e
5
+ # mantém duas {HTTP::Connection} memoizadas, uma por token: a de empresa
6
+ # (emissão/consulta de documentos) e a de conta (consultas auxiliares e gestão
7
+ # de empresas). Permite coexistência de várias empresas no mesmo processo sem
8
+ # estado compartilhado. Expõe os recursos da API instanciados preguiçosamente.
9
+ class Client
10
+ # @return [FocusNfe::Configuration] configuração validada deste cliente
11
+ attr_reader :configuration
12
+
13
+ # @overload initialize(configuration)
14
+ # @param configuration [FocusNfe::Configuration] configuração já montada
15
+ # @overload initialize(token_empresa:, token_conta:, environment:, **options)
16
+ # @param token_empresa [String] token da empresa (emissão/consulta de documentos)
17
+ # @param token_conta [String] token da conta (consultas auxiliares e gestão de empresas)
18
+ # @param environment [Symbol] :producao ou :homologacao
19
+ # @param options [Hash] demais opções da {Configuration} (timeout, headers, …)
20
+ # @raise [FocusNfe::Errors::ConfigurationError] se a configuração for inválida
21
+ def initialize(configuration = nil, **)
22
+ @configuration = (configuration || Configuration.new(**)).tap(&:validate!)
23
+ end
24
+
25
+ # @return [FocusNfe::HTTP::Connection] conexão da empresa, memoizada
26
+ # @raise [FocusNfe::Errors::ConfigurationError] se +token_empresa+ estiver ausente
27
+ def connection
28
+ @connection ||= build_connection(:empresa)
29
+ end
30
+
31
+ # @return [FocusNfe::HTTP::Connection] conexão da conta, memoizada
32
+ # @raise [FocusNfe::Errors::ConfigurationError] se +token_conta+ estiver ausente
33
+ def connection_conta
34
+ @connection_conta ||= build_connection(:conta)
35
+ end
36
+
37
+ # @return [FocusNfe::Recursos::Nfe] recurso de NF-e, memoizado
38
+ def nfe
39
+ @nfe ||= Recursos::Nfe.new(connection)
40
+ end
41
+
42
+ # @return [FocusNfe::Recursos::Nfce] recurso de NFC-e, memoizado
43
+ def nfce
44
+ @nfce ||= Recursos::Nfce.new(connection)
45
+ end
46
+
47
+ # @return [FocusNfe::Recursos::Nfse] recurso de NFS-e, memoizado
48
+ def nfse
49
+ @nfse ||= Recursos::Nfse.new(connection)
50
+ end
51
+
52
+ # @return [FocusNfe::Recursos::NfseNacional] recurso de NFS-e nacional, memoizado
53
+ def nfse_nacional
54
+ @nfse_nacional ||= Recursos::NfseNacional.new(connection)
55
+ end
56
+
57
+ # @return [FocusNfe::Recursos::Cte] recurso de CT-e, memoizado
58
+ def cte
59
+ @cte ||= Recursos::Cte.new(connection)
60
+ end
61
+
62
+ # @return [FocusNfe::Recursos::CteOs] recurso de CT-e OS, memoizado
63
+ def cte_os
64
+ @cte_os ||= Recursos::CteOs.new(connection)
65
+ end
66
+
67
+ # @return [FocusNfe::Recursos::Mdfe] recurso de MDF-e, memoizado
68
+ def mdfe
69
+ @mdfe ||= Recursos::Mdfe.new(connection)
70
+ end
71
+
72
+ # @return [FocusNfe::Recursos::Nfcom] recurso de NFCom, memoizado
73
+ def nfcom
74
+ @nfcom ||= Recursos::Nfcom.new(connection)
75
+ end
76
+
77
+ # @return [FocusNfe::Recursos::Dce] recurso de DC-e, memoizado
78
+ def dce
79
+ @dce ||= Recursos::Dce.new(connection)
80
+ end
81
+
82
+ # @return [FocusNfe::Recursos::Nfgas] recurso de NFGas, memoizado
83
+ def nfgas
84
+ @nfgas ||= Recursos::Nfgas.new(connection)
85
+ end
86
+
87
+ # @return [FocusNfe::Recursos::NfesRecebidas] recurso de NF-e recebidas, memoizado
88
+ def nfes_recebidas
89
+ @nfes_recebidas ||= Recursos::NfesRecebidas.new(connection)
90
+ end
91
+
92
+ # @return [FocusNfe::Recursos::CtesRecebidas] recurso de CT-e recebidos, memoizado
93
+ def ctes_recebidas
94
+ @ctes_recebidas ||= Recursos::CtesRecebidas.new(connection)
95
+ end
96
+
97
+ # @return [FocusNfe::Recursos::NfsesNacionaisRecebidas] recurso de NFS-e nacionais recebidas, memoizado
98
+ def nfses_nacionais_recebidas
99
+ @nfses_nacionais_recebidas ||= Recursos::NfsesNacionaisRecebidas.new(connection)
100
+ end
101
+
102
+ # @return [FocusNfe::Recursos::Ceps] recurso de consulta de CEP, memoizado
103
+ def ceps
104
+ @ceps ||= Recursos::Ceps.new(connection_conta)
105
+ end
106
+
107
+ # @return [FocusNfe::Recursos::Municipios] recurso de consulta de municípios, memoizado
108
+ def municipios
109
+ @municipios ||= Recursos::Municipios.new(connection_conta)
110
+ end
111
+
112
+ # @return [FocusNfe::Recursos::Cfops] recurso de consulta de CFOP, memoizado
113
+ def cfops
114
+ @cfops ||= Recursos::Cfops.new(connection_conta)
115
+ end
116
+
117
+ # @return [FocusNfe::Recursos::Cnaes] recurso de consulta de CNAE, memoizado
118
+ def cnaes
119
+ @cnaes ||= Recursos::Cnaes.new(connection_conta)
120
+ end
121
+
122
+ # @return [FocusNfe::Recursos::Ncms] recurso de consulta de NCM, memoizado
123
+ def ncms
124
+ @ncms ||= Recursos::Ncms.new(connection_conta)
125
+ end
126
+
127
+ # @return [FocusNfe::Recursos::Cnpjs] recurso de consulta de CNPJ, memoizado
128
+ def cnpjs
129
+ @cnpjs ||= Recursos::Cnpjs.new(connection_conta)
130
+ end
131
+
132
+ # @return [FocusNfe::Recursos::Empresas] recurso de gestão de empresas, memoizado
133
+ def empresas
134
+ @empresas ||= Recursos::Empresas.new(connection_conta)
135
+ end
136
+
137
+ # @return [FocusNfe::Recursos::Webhooks] recurso de gestão de webhooks, memoizado
138
+ def webhooks
139
+ @webhooks ||= Recursos::Webhooks.new(connection)
140
+ end
141
+
142
+ # @return [FocusNfe::Recursos::EmailsBloqueados] recurso de e-mails bloqueados, memoizado
143
+ def emails_bloqueados
144
+ @emails_bloqueados ||= Recursos::EmailsBloqueados.new(connection)
145
+ end
146
+
147
+ # @return [FocusNfe::Recursos::Backups] recurso de backups de XML, memoizado
148
+ def backups
149
+ @backups ||= Recursos::Backups.new(connection)
150
+ end
151
+
152
+ private
153
+
154
+ # @param escopo [Symbol] :empresa ou :conta
155
+ # @return [FocusNfe::HTTP::Connection] conexão autenticada com o token do escopo
156
+ # @raise [FocusNfe::Errors::ConfigurationError] se o token do escopo estiver ausente
157
+ def build_connection(escopo)
158
+ configuration.validate_token!(escopo)
159
+ HTTP::Connection.new(configuration, token: configuration.token_de(escopo).to_s)
160
+ end
161
+ end
162
+ end
@@ -0,0 +1,104 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FocusNfe
4
+ # Guarda as opções de uso da gem: os dois tokens da Focus NFe (+token_empresa+,
5
+ # que identifica a empresa emitente/consultada, e +token_conta+, usado nas
6
+ # consultas auxiliares e na gestão de empresas), o ambiente (que resolve a URL
7
+ # base), timeouts, logger, adaptador HTTP e cabeçalhos extras. Serve tanto ao
8
+ # modo global (FocusNfe.configure) quanto ao Client explícito (multi-empresa).
9
+ class Configuration
10
+ # @return [Hash{Symbol=>String}] ambiente => URL base da API (sem o prefixo /v2)
11
+ BASE_URLS = {
12
+ producao: "https://api.focusnfe.com.br",
13
+ homologacao: "https://homologacao.focusnfe.com.br"
14
+ }.freeze
15
+
16
+ # @return [Hash{Symbol=>Symbol}] escopo => atributo de token correspondente
17
+ ESCOPOS_TOKEN = { empresa: :token_empresa, conta: :token_conta }.freeze
18
+
19
+ # @return [Symbol] ambiente padrão quando nenhum é informado
20
+ DEFAULT_ENVIRONMENT = :homologacao
21
+ # @return [Integer] timeout de leitura padrão, em segundos
22
+ DEFAULT_TIMEOUT = 30
23
+ # @return [Integer] timeout de conexão padrão, em segundos
24
+ DEFAULT_OPEN_TIMEOUT = 10
25
+
26
+ attr_accessor :token_empresa, :token_conta, :environment, :timeout,
27
+ :open_timeout, :logger, :http_adapter, :headers
28
+
29
+ # @param token_empresa [String, nil] token da empresa (emissão/consulta de documentos)
30
+ # @param token_conta [String, nil] token da conta (consultas auxiliares e gestão de empresas)
31
+ # @param environment [Symbol] :producao ou :homologacao
32
+ # @param timeout [Integer] timeout de leitura, em segundos
33
+ # @param open_timeout [Integer] timeout de conexão, em segundos
34
+ # @param logger [_Logger, nil] logger plugável usado pela {HTTP::Connection}; deve responder a
35
+ # +debug+/+info+/+warn+/+error+ (compatível com o +Logger+ da stdlib, +Rails.logger+ etc.).
36
+ # +nil+ (padrão) desliga o logging. O +Authorization+ é sempre redigido — ver {HTTP::Logging}.
37
+ # @param http_adapter [FocusNfe::HTTP::Adapter, nil] instância pronta (nil => Connection cria a default)
38
+ # @param headers [Hash] cabeçalhos extras enviados em toda requisição
39
+ def initialize(token_empresa: nil, token_conta: nil, environment: DEFAULT_ENVIRONMENT,
40
+ timeout: DEFAULT_TIMEOUT, open_timeout: DEFAULT_OPEN_TIMEOUT,
41
+ logger: nil, http_adapter: nil, headers: {})
42
+ @token_empresa = token_empresa
43
+ @token_conta = token_conta
44
+ @environment = environment
45
+ @timeout = timeout
46
+ @open_timeout = open_timeout
47
+ @logger = logger
48
+ @http_adapter = http_adapter
49
+ @headers = headers
50
+ end
51
+
52
+ # @return [String] URL base correspondente ao ambiente atual
53
+ # @raise [FocusNfe::Errors::ConfigurationError] se o ambiente for desconhecido
54
+ def base_url
55
+ validate_environment!
56
+ BASE_URLS.fetch(environment)
57
+ end
58
+
59
+ # @param escopo [Symbol] :empresa ou :conta
60
+ # @return [String, nil] token correspondente ao escopo
61
+ def token_de(escopo)
62
+ public_send(ESCOPOS_TOKEN.fetch(escopo))
63
+ end
64
+
65
+ # Valida o ambiente e a presença de ao menos um token, falhando cedo quando a
66
+ # configuração é inutilizável em qualquer escopo.
67
+ #
68
+ # @return [self]
69
+ # @raise [FocusNfe::Errors::ConfigurationError] ambiente inválido ou nenhum token presente
70
+ def validate!
71
+ validate_environment!
72
+ return self if ESCOPOS_TOKEN.keys.any? { |escopo| token_presente?(escopo) }
73
+
74
+ raise Errors::ConfigurationError, "informe token_empresa e/ou token_conta"
75
+ end
76
+
77
+ # Valida o ambiente e a presença do token de um escopo específico, no momento
78
+ # em que um recurso daquele escopo é efetivamente usado.
79
+ #
80
+ # @param escopo [Symbol] :empresa ou :conta
81
+ # @return [self]
82
+ # @raise [FocusNfe::Errors::ConfigurationError] ambiente inválido ou token do escopo ausente
83
+ def validate_token!(escopo)
84
+ validate_environment!
85
+ return self if token_presente?(escopo)
86
+
87
+ raise Errors::ConfigurationError, "#{ESCOPOS_TOKEN.fetch(escopo)} é obrigatório para esta operação"
88
+ end
89
+
90
+ private
91
+
92
+ # @raise [FocusNfe::Errors::ConfigurationError] se o ambiente não for reconhecido
93
+ def validate_environment!
94
+ return if BASE_URLS.key?(environment)
95
+
96
+ raise Errors::ConfigurationError,
97
+ "ambiente inválido: #{environment.inspect} (use :producao ou :homologacao)"
98
+ end
99
+
100
+ def token_presente?(escopo)
101
+ !token_de(escopo).to_s.strip.empty?
102
+ end
103
+ end
104
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FocusNfe
4
+ # Raiz de toda a hierarquia de exceções da gem. Permite ao integrador capturar
5
+ # qualquer falha do `focus_nfe` com um único `rescue FocusNfe::Error`.
6
+ class Error < StandardError; end
7
+
8
+ # Exceções tipadas: falhas HTTP (por faixa de status), de configuração
9
+ # (client-side) e de conexão (transporte). Reúne também o mapeamento
10
+ # status → classe e a construção da exceção a partir de uma `Response`.
11
+ module Errors
12
+ # Falha HTTP retornada pela API, carregando o status, o corpo parseado com
13
+ # as mensagens de erro da API e a {FocusNfe::HTTP::Response} original.
14
+ class HttpError < Error
15
+ # @return [Integer, nil] código de status HTTP da resposta
16
+ attr_reader :status
17
+
18
+ # @return [Object, nil] corpo parseado com as mensagens de erro da API
19
+ attr_reader :body
20
+
21
+ # @return [FocusNfe::HTTP::Response, nil] resposta original que originou o erro
22
+ attr_reader :response
23
+
24
+ # @param message [String, nil] mensagem da exceção
25
+ # @param status [Integer, nil] código de status HTTP
26
+ # @param body [Object, nil] corpo parseado da resposta
27
+ # @param response [FocusNfe::HTTP::Response, nil] resposta original
28
+ def initialize(message = nil, status: nil, body: nil, response: nil)
29
+ @status = status
30
+ @body = body
31
+ @response = response
32
+ super(message)
33
+ end
34
+
35
+ # @return [String, nil] código de erro da API, quando o corpo é estruturado
36
+ def codigo
37
+ body["codigo"] if body.is_a?(Hash)
38
+ end
39
+
40
+ # @return [Array<Hash>] erros no formato +{ "campo" => String?, "mensagem" => String }+;
41
+ # vazio quando o corpo não traz erros estruturados
42
+ def erros
43
+ return [] unless body.is_a?(Hash)
44
+ return body["erros"] if body["erros"].is_a?(Array)
45
+
46
+ body["mensagem"] ? [{ "campo" => nil, "mensagem" => body["mensagem"] }] : []
47
+ end
48
+ end
49
+
50
+ # 400 — requisição malformada.
51
+ class BadRequest < HttpError; end
52
+
53
+ # 401 — token ausente ou inválido.
54
+ class Unauthorized < HttpError; end
55
+
56
+ # 403 — autenticado, porém sem permissão para o recurso.
57
+ class Forbidden < HttpError; end
58
+
59
+ # 404 — recurso inexistente.
60
+ class NotFound < HttpError; end
61
+
62
+ # 409 — conflito de estado (ex.: `ref` já utilizada).
63
+ class Conflict < HttpError; end
64
+
65
+ # 422 — erro de validação dos campos enviados.
66
+ class ValidationError < HttpError; end
67
+
68
+ # 429 — limite de requisições excedido.
69
+ class RateLimited < HttpError; end
70
+
71
+ # 5xx — erro interno do servidor da Focus NFe.
72
+ class ServerError < HttpError; end
73
+
74
+ # Status não-2xx sem mapeamento específico (ex.: 418, 451).
75
+ class UnexpectedResponse < HttpError; end
76
+
77
+ # Erro client-side de configuração (token ausente, ambiente inválido) —
78
+ # não envolve resposta HTTP.
79
+ class ConfigurationError < Error; end
80
+
81
+ # Falha de transporte (timeout, conexão recusada, excesso de redirects).
82
+ class ConnectionError < Error; end
83
+
84
+ # Falha ao processar um webhook inbound (corpo malformado) — client-side,
85
+ # não envolve resposta HTTP.
86
+ class WebhookError < Error; end
87
+
88
+ # @return [Hash{Integer=>Class}] status HTTP específico => classe de exceção
89
+ BY_STATUS = {
90
+ 400 => BadRequest,
91
+ 401 => Unauthorized,
92
+ 403 => Forbidden,
93
+ 404 => NotFound,
94
+ 409 => Conflict,
95
+ 422 => ValidationError,
96
+ 429 => RateLimited
97
+ }.freeze
98
+
99
+ module_function
100
+
101
+ # Resolve a classe de exceção correspondente a um status HTTP.
102
+ #
103
+ # @param status [Integer] código de status HTTP não-2xx
104
+ # @return [Class] subclasse de {HttpError}; qualquer 5xx vira {ServerError}
105
+ # e qualquer status sem mapeamento específico vira {UnexpectedResponse}
106
+ def class_for(status)
107
+ BY_STATUS[status] || (status.between?(500, 599) ? ServerError : UnexpectedResponse)
108
+ end
109
+
110
+ # Constrói a exceção tipada já preenchida a partir de uma resposta.
111
+ #
112
+ # @param response [FocusNfe::HTTP::Response] resposta não-2xx recebida
113
+ # @return [HttpError] instância da classe certa com status/corpo/resposta
114
+ def from_response(response)
115
+ class_for(response.status).new(
116
+ "requisição falhou com status #{response.status}",
117
+ status: response.status,
118
+ body: response.body,
119
+ response: response
120
+ )
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "date"
4
+
5
+ module FocusNfe
6
+ module Esquemas
7
+ # Um campo de um {Esquema} de emissão, derivado das definições de
8
+ # +campos.focusnfe.com.br+. Conhece o nome do campo, sua obrigatoriedade e
9
+ # sabe parsear o tipo fiscal (+String[1-60]+, +Integer[1-9]+, +Decimal[13.2]+,
10
+ # +Date+, +DateTime+, enum, coleção) em restrições aplicáveis a um valor.
11
+ class Campo
12
+ # @return [Regexp] captura base e tamanho de um escalar de comprimento (ex.: +String[1-60]+)
13
+ ESCALAR = /\A(?<base>String|Integer)\[(?<inicio>\d+)(?:-(?<fim>\d+))?\]/
14
+
15
+ # @return [Regexp] captura os códigos declarados em um enum (+* +0+: …+ ou +*+0+: …+)
16
+ CODIGO_ENUM = /\*\s*\+([^+]+)\+/
17
+
18
+ # @param definicao [Hash] entrada do schema ({ "name", "type", "required", "collection", … })
19
+ def initialize(definicao)
20
+ @definicao = definicao
21
+ parsear_tipo
22
+ end
23
+
24
+ # @return [String] nome do campo no payload de emissão
25
+ def nome = @definicao["name"]
26
+
27
+ # @return [String, nil] descrição do campo conforme +campos.focusnfe.com.br+
28
+ def descricao = @definicao["description"]
29
+
30
+ # @return [String, nil] tipo bruto como documentado (ex.: +"String[1-60]"+)
31
+ def tipo_bruto = @definicao["type"]
32
+
33
+ # @return [String, nil] enumeração dos valores aceitos, como documentada
34
+ def enum = @definicao["enum"]
35
+
36
+ # @return [Array<String>] códigos aceitos extraídos do {#enum} (vazio se não houver)
37
+ def valores_enum
38
+ @valores_enum ||= enum.to_s.scan(CODIGO_ENUM).flatten
39
+ end
40
+
41
+ # @return [Boolean] se o campo declara um conjunto de valores aceitos
42
+ def enum? = !valores_enum.empty?
43
+
44
+ # @return [String, nil] tag XML subjacente do campo
45
+ def tag = @definicao["tag"]
46
+
47
+ # Representação serializável do campo, para introspecção externa (devs e
48
+ # ferramentas automatizadas). Coleções aninham a descrição dos subcampos em
49
+ # +:colecao+, em profundidade arbitrária; campos escalares têm +:colecao+ nil.
50
+ #
51
+ # @return [Hash] descrição estruturada do campo
52
+ def to_h
53
+ {
54
+ nome: nome, descricao: descricao, tipo: tipo, tipo_bruto: tipo_bruto,
55
+ obrigatorio: obrigatorio?, tamanho_minimo: tamanho_minimo, tamanho_maximo: tamanho_maximo,
56
+ decimal: decimal&.to_h, enum: enum, tag: tag, colecao: esquema_colecao&.descrever
57
+ }
58
+ end
59
+
60
+ # @return [Boolean] se o campo é obrigatório na emissão
61
+ def obrigatorio? = @definicao["required"] == true
62
+
63
+ # @return [Boolean] se o campo é uma coleção de subitens
64
+ def colecao? = tipo == :colecao
65
+
66
+ # @return [Symbol] tipo parseado (+:string+, +:integer+, +:decimal+, +:date+,
67
+ # +:datetime+, +:enum+, +:colecao+ ou +:desconhecido+)
68
+ attr_reader :tipo
69
+
70
+ # @return [Esquema, nil] esquema dos subcampos da coleção, ou +nil+ se o campo
71
+ # não for coleção ou não declarar +object_attributes+
72
+ def esquema_colecao
73
+ return unless colecao?
74
+
75
+ @esquema_colecao ||= (atributos = @definicao.dig("collection", "object_attributes")) && Esquema.new(atributos)
76
+ end
77
+
78
+ # @return [Integer, nil] tamanho/quantidade de dígitos mínimo (escalares)
79
+ attr_reader :tamanho_minimo
80
+
81
+ # @return [Integer, nil] tamanho/quantidade de dígitos máximo (escalares)
82
+ attr_reader :tamanho_maximo
83
+
84
+ # @return [Decimal, nil] especificação decimal do campo, ou +nil+ se não for decimal
85
+ attr_reader :decimal
86
+
87
+ # Valida um valor contra o tipo/tamanho e o conjunto de enum do campo.
88
+ # Coleções e tipos desconhecidos não restringem nesta etapa.
89
+ #
90
+ # @param valor [Object] valor informado para o campo
91
+ # @return [String, nil] mensagem de erro ou +nil+ se válido
92
+ def validar_valor(valor)
93
+ erro = validar_tipo(valor)
94
+ return erro if erro
95
+
96
+ validar_enum(valor) if enum?
97
+ end
98
+
99
+ private
100
+
101
+ def parsear_tipo
102
+ bruto = @definicao["type"]
103
+ escalar = bruto && ESCALAR.match(bruto)
104
+ return parsear_escalar(escalar) if escalar
105
+
106
+ @decimal = Decimal.parsear(bruto)
107
+ return @tipo = :decimal if @decimal
108
+
109
+ @tipo = tipo_nao_escalar(bruto)
110
+ end
111
+
112
+ def tipo_nao_escalar(bruto)
113
+ return :colecao if @definicao.key?("collection") || bruto.to_s.start_with?("Coleção")
114
+ return @definicao["enum"] ? :enum : :desconhecido if bruto.nil?
115
+ return :datetime if bruto.start_with?("DateTime")
116
+ return :date if bruto.start_with?("Date")
117
+
118
+ :desconhecido
119
+ end
120
+
121
+ def parsear_escalar(match)
122
+ @tipo = { "String" => :string, "Integer" => :integer }[match[:base].to_s]
123
+ @tamanho_minimo = Integer(match[:inicio])
124
+ @tamanho_maximo = match[:fim] ? Integer(match[:fim]) : @tamanho_minimo
125
+ end
126
+
127
+ def validar_tipo(valor)
128
+ case tipo
129
+ when :string then validar_string(valor)
130
+ when :integer then validar_integer(valor)
131
+ when :decimal then validar_decimal(valor)
132
+ when :date then validar_data(valor, Date)
133
+ when :datetime then validar_data(valor, DateTime)
134
+ end
135
+ end
136
+
137
+ def validar_string(valor)
138
+ comprimento = valor.to_s.length
139
+ return if comprimento.between?(tamanho_minimo, tamanho_maximo)
140
+
141
+ "#{nome}: tamanho #{comprimento} fora do intervalo #{tamanho_minimo}-#{tamanho_maximo}"
142
+ end
143
+
144
+ def validar_integer(valor)
145
+ digitos = valor.to_s
146
+ return "#{nome}: deve conter apenas dígitos" unless digitos.match?(/\A\d+\z/)
147
+ return if digitos.length.between?(tamanho_minimo, tamanho_maximo)
148
+
149
+ "#{nome}: #{digitos.length} dígitos fora do intervalo #{tamanho_minimo}-#{tamanho_maximo}"
150
+ end
151
+
152
+ def validar_decimal(valor)
153
+ mensagem = decimal&.validar(valor)
154
+ "#{nome}: #{mensagem}" if mensagem
155
+ end
156
+
157
+ def validar_data(valor, classe)
158
+ classe.iso8601(valor.to_s)
159
+ nil
160
+ rescue ArgumentError, TypeError
161
+ "#{nome}: data inválida (esperado ISO 8601)"
162
+ end
163
+
164
+ def validar_enum(valor)
165
+ return if valores_enum.include?(valor.to_s)
166
+
167
+ "#{nome}: valor #{valor.inspect} fora do conjunto permitido (#{valores_enum.join(", ")})"
168
+ end
169
+ end
170
+ end
171
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FocusNfe
4
+ # Introspecção pública dos esquemas empacotados: lista os documentos com schema
5
+ # e descreve seus campos como dado serializável, para devs e ferramentas
6
+ # automatizadas — sem token nem conexão.
7
+ module Esquemas
8
+ # @return [String] glob dos arquivos de schema empacotados em +data/schemas/+
9
+ GLOB_SCHEMAS = "schema_*.json"
10
+
11
+ class << self
12
+ # Nomes dos documentos com schema empacotado, ordenados. Inclui os
13
+ # sub-schemas auxiliares (ex.: +"nfe_item"+, +"cte_transporte_aereo"+), pois
14
+ # também são introspectáveis via {descrever}.
15
+ #
16
+ # @return [Array<String>] nomes de documento aceitos por {descrever}
17
+ def disponiveis
18
+ Dir.glob(File.join(Esquema::DIRETORIO, GLOB_SCHEMAS))
19
+ .map { |caminho| File.basename(caminho, ".json").delete_prefix("schema_") }
20
+ .sort
21
+ end
22
+
23
+ # Descrição estruturada dos campos de um documento, para devs e ferramentas
24
+ # automatizadas conhecerem nomes, tipos e obrigatoriedade sem token nem
25
+ # conexão. Coleções aninham seus subcampos (ver {Campo#to_h}).
26
+ #
27
+ # @param nome [String] nome do documento (ver {disponiveis})
28
+ # @return [Array<Hash>, nil] descrição de cada campo, ou +nil+ se não houver schema
29
+ def descrever(nome)
30
+ Esquema.carregar(nome)&.descrever
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FocusNfe
4
+ module Esquemas
5
+ # Especificação decimal de um {Campo} (ex.: +Decimal[13.2]+, +Decimal[13.2-4]+):
6
+ # até +inteiros+ dígitos inteiros e de +casas_minimas+ a +casas_maximas+ casas
7
+ # decimais. Sabe validar um valor informado contra esses limites.
8
+ class Decimal
9
+ # @return [Regexp] captura inteiros e a faixa de casas (ex.: +Decimal[13.2-4]+)
10
+ ESPEC = /\ADecimal\[(?<inteiros>\d+)(?:\.(?<casas_min>\d*)(?:-(?<casas_max>\d+))?)?\]/
11
+
12
+ # @return [Regexp] decompõe um valor em parte inteira e fracionária
13
+ VALOR = /\A-?(?<inteira>\d+)(?:[.,](?<fracionaria>\d+))?\z/
14
+
15
+ # @param bruto [String, nil] tipo bruto do campo
16
+ # @return [Decimal, nil] a especificação parseada, ou +nil+ se +bruto+ não for decimal
17
+ def self.parsear(bruto)
18
+ match = bruto && ESPEC.match(bruto)
19
+ match && new(match)
20
+ end
21
+
22
+ # @return [Integer] quantidade máxima de dígitos inteiros
23
+ attr_reader :inteiros
24
+
25
+ # @return [Integer] quantidade mínima de casas decimais
26
+ attr_reader :casas_minimas
27
+
28
+ # @return [Integer] quantidade máxima de casas decimais
29
+ attr_reader :casas_maximas
30
+
31
+ # @param match [MatchData] captura de {ESPEC}
32
+ def initialize(match)
33
+ @inteiros = Integer(match[:inteiros])
34
+ @casas_minimas = match[:casas_min].to_s.empty? ? 0 : Integer(match[:casas_min])
35
+ @casas_maximas = match[:casas_max] ? Integer(match[:casas_max]) : @casas_minimas
36
+ end
37
+
38
+ # Valida um valor decimal. Lenient: aceita menos casas que o mínimo (zeros à
39
+ # direita são equivalentes) — rejeita apenas o claramente inválido.
40
+ #
41
+ # @param valor [Object] valor informado
42
+ # @return [String, nil] mensagem de erro (sem o nome do campo) ou +nil+ se válido
43
+ def validar(valor)
44
+ match = VALOR.match(valor.to_s.strip)
45
+ return "valor decimal inválido" unless match
46
+
47
+ erro_de_tamanho(match[:inteira].to_s, match[:fracionaria])
48
+ end
49
+
50
+ # @return [Hash] descrição serializável da especificação
51
+ def to_h
52
+ { inteiros: inteiros, casas_minimas: casas_minimas, casas_maximas: casas_maximas }
53
+ end
54
+
55
+ private
56
+
57
+ def erro_de_tamanho(inteira, fracionaria)
58
+ digitos = inteira.sub(/\A0+(?=\d)/, "").length
59
+ casas = fracionaria&.length || 0
60
+ return "#{digitos} dígitos inteiros excedem o máximo de #{inteiros}" if digitos > inteiros
61
+
62
+ "#{casas} casas decimais excedem o máximo de #{casas_maximas}" if casas > casas_maximas
63
+ end
64
+ end
65
+ end
66
+ end