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,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FocusNfe
4
+ module Recursos
5
+ module Concerns
6
+ # Comportamento de Carta de Correção Eletrônica (CC-e) do CT-e/CT-e OS: +POST
7
+ # /<base>/<ref>/carta_correcao+ com a correção _por campo_ no corpo — diferente
8
+ # da CC-e de texto livre da NF-e ({Corrigivel}). Devolve o {Modelos::Documento}
9
+ # com o estado fiscal da CC-e.
10
+ module CorrigivelCte
11
+ # Emite uma Carta de Correção Eletrônica corrigindo um campo do documento.
12
+ #
13
+ # São aceitas até 20 correções por documento, cada uma em uma requisição; vale
14
+ # apenas a última correção enviada. Variáveis tributárias, dados de remetente/
15
+ # destinatário e datas de emissão/saída não podem ser corrigidos.
16
+ #
17
+ # @param ref [String] referência do documento
18
+ # @param campo_corrigido [String] nome do campo a corrigir
19
+ # @param valor_corrigido [String] novo valor do campo
20
+ # @param grupo_corrigido [String, nil] grupo que contém o campo (ex.: +"cargas"+)
21
+ # @param numero_item_grupo_corrigido [String, nil] índice do item, quando o campo está numa lista (inicia em 1)
22
+ # @param campo_api [Integer, String, nil] +1+ para nomes de campo da API (padrão), +0+ para tags XML
23
+ # @return [FocusNfe::Modelos::Documento]
24
+ # @raise [ArgumentError] se a +ref+, +campo_corrigido+ ou +valor_corrigido+ forem inválidos
25
+ # @raise [FocusNfe::Errors::HttpError] em respostas não-2xx
26
+ def corrigir(ref, campo_corrigido:, valor_corrigido:, grupo_corrigido: nil,
27
+ numero_item_grupo_corrigido: nil, campo_api: nil)
28
+ validar_referencia!(ref)
29
+ validar_campos_correcao!(campo_corrigido, valor_corrigido)
30
+
31
+ body = {
32
+ campo_corrigido: campo_corrigido, valor_corrigido: valor_corrigido,
33
+ grupo_corrigido: grupo_corrigido, numero_item_grupo_corrigido: numero_item_grupo_corrigido,
34
+ campo_api: campo_api
35
+ }.compact
36
+ response = connection.post("#{caminho_referencia(ref)}/carta_correcao", body: body)
37
+ Modelos::Documento.from_response(response, ref: ref)
38
+ end
39
+
40
+ private
41
+
42
+ # @param campo_corrigido [String] campo informado
43
+ # @param valor_corrigido [String] valor informado
44
+ # @return [void]
45
+ # @raise [ArgumentError] se algum deles não for uma String não-vazia
46
+ def validar_campos_correcao!(campo_corrigido, valor_corrigido)
47
+ return if string_preenchida?(campo_corrigido) && string_preenchida?(valor_corrigido)
48
+
49
+ raise ArgumentError, "correção inválida: campo_corrigido e valor_corrigido são obrigatórios"
50
+ end
51
+
52
+ # @param valor [Object] valor a checar
53
+ # @return [Boolean] true se for uma String com conteúdo não em branco
54
+ def string_preenchida?(valor)
55
+ valor.is_a?(String) && !valor.strip.empty?
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FocusNfe
4
+ module Recursos
5
+ module Concerns
6
+ # Comportamento de emissão (+POST /<base>?ref=+). Devolve o {Modelos::Documento}
7
+ # com o estado fiscal inicial (em geral assíncrono: +processando_autorizacao+).
8
+ module Emitivel
9
+ # Emite um documento fiscal.
10
+ #
11
+ # @param ref [String] referência única do documento na sua aplicação
12
+ # @param dados [Hash] payload de emissão (campos do schema, validados server-side)
13
+ # @param validar [Boolean] se +true+, valida +dados+ contra o schema empacotado
14
+ # antes do envio (documentos sem schema próprio são emitidos sem validar)
15
+ # @param opcoes [Hash] parâmetros de query adicionais (ex.: emissão síncrona)
16
+ # @return [FocusNfe::Modelos::Documento]
17
+ # @raise [ArgumentError] se a +ref+ for inválida
18
+ # @raise [FocusNfe::Esquemas::ErroDeValidacao] se +validar+ e +dados+ violarem o schema
19
+ # @raise [FocusNfe::Errors::HttpError] em respostas não-2xx
20
+ def emitir(ref:, dados:, validar: false, **opcoes)
21
+ validar_referencia!(ref)
22
+ validar_dados!(dados) if validar
23
+
24
+ response = connection.post(caminho_base, params: { ref: ref, **opcoes }, body: dados)
25
+ Modelos::Documento.from_response(response, ref: ref)
26
+ end
27
+
28
+ private
29
+
30
+ # @param dados [Hash] payload de emissão a validar contra o schema
31
+ # @return [void]
32
+ # @raise [FocusNfe::Esquemas::ErroDeValidacao] se houver campo inválido/ausente
33
+ def validar_dados!(dados)
34
+ esquema = Esquemas::Esquema.carregar(caminho_base)
35
+ return unless esquema
36
+
37
+ normalizados = dados.transform_keys(&:to_s)
38
+ aninhados = carregar_aninhados(normalizados)
39
+ erros = Esquemas::Validador.new(esquema, aninhados: aninhados).validar(normalizados)
40
+ raise Esquemas::ErroDeValidacao, erros unless erros.empty?
41
+ end
42
+
43
+ # @param dados [Hash] payload normalizado (chaves String)
44
+ # @return [Hash{String => FocusNfe::Esquemas::Esquema}] sub-esquemas por chave aninhada
45
+ def carregar_aninhados(dados)
46
+ esquemas_extras(dados).transform_values { |nome| Esquemas::Esquema.carregar(nome) }.compact
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FocusNfe
4
+ module Recursos
5
+ module Concerns
6
+ # Comportamento de reenvio do documento por e-mail (+POST
7
+ # /<base>/<ref>/email+ com a lista de destinatários no corpo). Devolve o
8
+ # corpo cru da resposta.
9
+ module Enviavel
10
+ # @return [Integer] número máximo de destinatários aceito pela API
11
+ MAX_EMAILS = 10
12
+
13
+ # Reenvia o documento por e-mail para os destinatários informados.
14
+ #
15
+ # @param ref [String] referência do documento
16
+ # @param emails [Array<String>] destinatários (1 a 10)
17
+ # @return [Hash, Array, nil] corpo cru da resposta
18
+ # @raise [ArgumentError] se a +ref+ ou a lista de +emails+ forem inválidas
19
+ # @raise [FocusNfe::Errors::HttpError] em respostas não-2xx
20
+ def enviar_email(ref, emails:)
21
+ validar_referencia!(ref)
22
+ validar_emails!(emails)
23
+
24
+ connection.post("#{caminho_referencia(ref)}/email", body: { emails: emails }).body
25
+ end
26
+
27
+ private
28
+
29
+ # @param emails [Array<String>] lista informada
30
+ # @return [void]
31
+ # @raise [ArgumentError] se não for Array de 1 a 10 elementos
32
+ def validar_emails!(emails)
33
+ return if emails.is_a?(Array) && (1..MAX_EMAILS).cover?(emails.size)
34
+
35
+ raise ArgumentError, "emails inválidos: esperado Array com 1 a #{MAX_EMAILS} destinatários"
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FocusNfe
4
+ module Recursos
5
+ module Concerns
6
+ # Transporte de eventos fiscais sobre um documento (+POST+/+DELETE+ num
7
+ # sub-caminho da referência). Cada recurso expõe seus eventos por cima
8
+ # destes helpers — o MDF-e com nomes próprios (+encerrar+, +incluir_condutor+,
9
+ # +incluir_dfe+) e as notas recebidas com +emitir_evento+/+cancelar_evento+.
10
+ # Devolve o {Modelos::Documento} com o estado fiscal resultante do evento.
11
+ module Eventavel
12
+ private
13
+
14
+ # Emite um evento sobre o documento (+POST /<base>/<ref>/<caminho>+).
15
+ #
16
+ # @param ref [String] referência do documento
17
+ # @param caminho [String] sub-caminho do evento (ex.: +encerrar+)
18
+ # @param dados [Hash] campos do corpo do evento
19
+ # @return [FocusNfe::Modelos::Documento]
20
+ # @raise [ArgumentError] se a +ref+ for inválida
21
+ # @raise [FocusNfe::Errors::HttpError] em respostas não-2xx
22
+ def emitir_evento_em(ref, caminho:, **dados)
23
+ validar_referencia!(ref)
24
+
25
+ response = connection.post("#{caminho_referencia(ref)}/#{caminho}", body: dados)
26
+ Modelos::Documento.from_response(response, ref: ref)
27
+ end
28
+
29
+ # Cancela um evento do documento (+DELETE /<base>/<ref>/<caminho>+).
30
+ #
31
+ # @param ref [String] referência do documento
32
+ # @param caminho [String] sub-caminho do evento (ex.: +evento+)
33
+ # @param dados [Hash] campos do corpo do cancelamento; omitidos não enviam corpo
34
+ # @return [FocusNfe::Modelos::Documento]
35
+ # @raise [ArgumentError] se a +ref+ for inválida
36
+ # @raise [FocusNfe::Errors::HttpError] em respostas não-2xx
37
+ def cancelar_evento_em(ref, caminho:, **dados)
38
+ validar_referencia!(ref)
39
+
40
+ response = connection.delete("#{caminho_referencia(ref)}/#{caminho}", body: dados.empty? ? nil : dados)
41
+ Modelos::Documento.from_response(response, ref: ref)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FocusNfe
4
+ module Recursos
5
+ module Concerns
6
+ # Comportamento de inutilização de numeração: criação (+POST
7
+ # /<base>/inutilizacao+, devolvendo um {Modelos::Inutilizacao}) e consulta
8
+ # das já criadas (+GET /<base>/inutilizacoes+, devolvendo uma lista de
9
+ # {Modelos::Inutilizacao}). É uma operação de coleção — sem +ref+ por
10
+ # documento.
11
+ module Inutilizavel
12
+ # @return [String] segmento do caminho de criação (singular)
13
+ CAMINHO_INUTILIZACAO = "inutilizacao"
14
+
15
+ # @return [String] segmento do caminho de consulta (plural)
16
+ CAMINHO_INUTILIZACOES = "inutilizacoes"
17
+
18
+ # @return [Integer] tamanho mínimo da justificativa exigido pela SEFAZ
19
+ JUSTIFICATIVA_MINIMA = 15
20
+
21
+ # Inutiliza uma faixa de numeração não utilizada.
22
+ #
23
+ # @param cnpj [String] CNPJ do emitente
24
+ # @param serie [String, Integer] série da numeração
25
+ # @param numero_inicial [String, Integer] número inicial da faixa
26
+ # @param numero_final [String, Integer] número final da faixa
27
+ # @param justificativa [String] motivo da inutilização (mínimo 15 caracteres)
28
+ # @return [FocusNfe::Modelos::Inutilizacao]
29
+ # @raise [ArgumentError] se a +justificativa+ for inválida
30
+ # @raise [FocusNfe::Errors::HttpError] em respostas não-2xx
31
+ def inutilizar(cnpj:, serie:, numero_inicial:, numero_final:, justificativa:)
32
+ validar_justificativa!(justificativa)
33
+ validar_faixa!(numero_inicial, numero_final)
34
+
35
+ body = { cnpj: cnpj, serie: serie, numero_inicial: numero_inicial,
36
+ numero_final: numero_final, justificativa: justificativa }
37
+ response = connection.post("#{caminho_base}/#{CAMINHO_INUTILIZACAO}", body: body)
38
+ Modelos::Inutilizacao.from_response(response)
39
+ end
40
+
41
+ # Consulta as inutilizações já registradas, aplicando os filtros como query string.
42
+ #
43
+ # @param filtros [Hash] filtros aceitos pela API (ex.: +cnpj:+, +cpf:+, +numero_inicial:+)
44
+ # @return [Array<FocusNfe::Modelos::Inutilizacao>]
45
+ # @raise [FocusNfe::Errors::HttpError] em respostas não-2xx
46
+ def consultar_inutilizacoes(**filtros)
47
+ response = connection.get("#{caminho_base}/#{CAMINHO_INUTILIZACOES}", params: filtros)
48
+ itens = response.body.is_a?(Array) ? response.body : []
49
+ itens.map { |item| Modelos::Inutilizacao.from_item(item) }
50
+ end
51
+
52
+ private
53
+
54
+ # @param justificativa [String] justificativa informada
55
+ # @return [void]
56
+ # @raise [ArgumentError] se não for String ou tiver menos de 15 caracteres
57
+ def validar_justificativa!(justificativa)
58
+ return if justificativa.is_a?(String) && justificativa.length >= JUSTIFICATIVA_MINIMA
59
+
60
+ raise ArgumentError,
61
+ "justificativa inválida: esperado String com ao menos #{JUSTIFICATIVA_MINIMA} caracteres"
62
+ end
63
+
64
+ # Valida a faixa de numeração: ambos os números presentes, inteiros, e
65
+ # +numero_inicial+ não maior que +numero_final+ (faixa de um único número
66
+ # é permitida).
67
+ #
68
+ # @param numero_inicial [String, Integer] número inicial informado
69
+ # @param numero_final [String, Integer] número final informado
70
+ # @return [void]
71
+ # @raise [ArgumentError] se algum número for ausente/não inteiro ou se inicial > final
72
+ def validar_faixa!(numero_inicial, numero_final)
73
+ inicial = numero_para_inteiro(numero_inicial)
74
+ final = numero_para_inteiro(numero_final)
75
+
76
+ if inicial.nil? || final.nil?
77
+ raise ArgumentError, "numeração inválida: numero_inicial e numero_final devem ser inteiros"
78
+ end
79
+
80
+ return if inicial <= final
81
+
82
+ raise ArgumentError, "numeração inválida: numero_inicial (#{inicial}) maior que numero_final (#{final})"
83
+ end
84
+
85
+ # @param valor [Object] valor informado
86
+ # @return [Integer, nil] o valor como inteiro (base 10) ou +nil+ se não for conversível
87
+ def numero_para_inteiro(valor)
88
+ case valor
89
+ when Integer then valor
90
+ when String then Integer(valor, 10, exception: false)
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FocusNfe
4
+ module Recursos
5
+ module Concerns
6
+ # Comportamento de listagem (+GET /<base>+). Os filtros viram query string e
7
+ # a resposta — corpo + cabeçalhos de paginação — é encapsulada numa
8
+ # {Modelos::Pagina}.
9
+ module Listavel
10
+ # Lista os registros do recurso, aplicando os filtros como query string.
11
+ #
12
+ # @param filtros [Hash] filtros e paginação aceitos pelo recurso (ex.: +cnpj:+, +versao:+, +offset:+)
13
+ # @return [FocusNfe::Modelos::Pagina]
14
+ # @raise [FocusNfe::Errors::HttpError] em respostas não-2xx
15
+ def listar(**filtros)
16
+ response = connection.get(caminho_base, params: filtros)
17
+ Modelos::Pagina.from_response(response)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FocusNfe
4
+ module Recursos
5
+ module Concerns
6
+ # Comportamento de consulta por identificador (+GET /<base>/<identificador>+),
7
+ # para recursos de leitura e referência. Devolve o corpo cru da resposta
8
+ # (Hash) — sem validação fiscal de +ref+, pois o identificador é um código,
9
+ # chave ou CNPJ.
10
+ module Localizavel
11
+ # Consulta um registro pelo seu identificador.
12
+ #
13
+ # @param identificador [String] código/chave/CNPJ do registro
14
+ # @return [Hash, Array, nil] corpo cru da resposta
15
+ # @raise [FocusNfe::Errors::HttpError] em respostas não-2xx
16
+ def consultar(identificador)
17
+ connection.get(caminho_referencia(identificador)).body
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FocusNfe
4
+ module Recursos
5
+ module Concerns
6
+ # Comportamento de reenvio de notificação (+POST /<base>/<identificador>/hook+),
7
+ # disparando de novo o webhook do documento.
8
+ module Notificavel
9
+ # Reenvia a notificação (webhook) do documento.
10
+ #
11
+ # @param identificador [String] chave ou referência do documento
12
+ # @return [Hash, Array, nil] corpo cru da resposta
13
+ # @raise [ArgumentError] se o +identificador+ for inválido
14
+ # @raise [FocusNfe::Errors::HttpError] em respostas não-2xx
15
+ def reenviar_hook(identificador)
16
+ validar_referencia!(identificador)
17
+
18
+ connection.post("#{caminho_referencia(identificador)}/hook").body
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FocusNfe
4
+ module Recursos
5
+ module Concerns
6
+ # Comportamento de exclusão por identificador (+DELETE /<base>/<identificador>+),
7
+ # para recursos de gestão (CRUD).
8
+ module Removivel
9
+ # Exclui um registro pelo seu identificador.
10
+ #
11
+ # @param identificador [String] id do registro
12
+ # @return [Hash, Array, nil] corpo cru da resposta
13
+ # @raise [FocusNfe::Errors::HttpError] em respostas não-2xx
14
+ def excluir(identificador)
15
+ connection.delete(caminho_referencia(identificador)).body
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FocusNfe
4
+ module Recursos
5
+ module Concerns
6
+ # Comportamento de prévia/visualização (+POST /<base_previa>+ com o payload
7
+ # no corpo). Gera o PDF apenas para conferência visual — sem valor fiscal e
8
+ # sem emitir o documento. O caminho é declarado por
9
+ # {Base.caminho_base_previa} em cada recurso (ex.: +"nfe/danfe"+), para que
10
+ # outros documentos com prévia o adotem sem alterar o mixin. A validação
11
+ # opt-in reaproveita o schema via {Concerns::Emitivel}.
12
+ module Visualizavel
13
+ # Gera a prévia do documento a partir do payload, devolvendo os bytes do PDF.
14
+ #
15
+ # @param dados [Hash] payload do documento (mesmos campos da emissão)
16
+ # @param validar [Boolean] se +true+, valida +dados+ contra o schema empacotado antes do envio
17
+ # @return [String, nil] bytes crus do PDF da prévia
18
+ # @raise [FocusNfe::Esquemas::ErroDeValidacao] se +validar+ e +dados+ violarem o schema
19
+ # @raise [FocusNfe::Errors::HttpError] em respostas não-2xx
20
+ def previa(dados:, validar: false)
21
+ validar_dados!(dados) if validar
22
+
23
+ connection.post(caminho_base_previa, body: dados).raw_body
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FocusNfe
4
+ module Recursos
5
+ # Recurso de Conhecimento de Transporte eletrônico (CT-e). Emissão
6
+ # assíncrona (síncrona opcional) com consulta de estado, cancelamento e
7
+ # Carta de Correção Eletrônica (CC-e) por campo.
8
+ class Cte < Base
9
+ include Concerns::Emitivel
10
+ include Concerns::Consultavel
11
+ include Concerns::Cancelavel
12
+ include Concerns::CorrigivelCte
13
+ include Concerns::Notificavel
14
+
15
+ caminho_base "cte"
16
+
17
+ # @return [Hash{String=>Hash{String=>String}}] código do modal => chave do payload => nome do sub-esquema
18
+ MODAIS = {
19
+ "01" => { "modal_rodoviario" => "cte_transporte_rodoviario" },
20
+ "02" => { "modal_aereo" => "cte_transporte_aereo" },
21
+ "03" => { "modal_aquaviario" => "cte_transporte_aquaviario" },
22
+ "04" => { "modal_ferroviario" => "cte_transporte_ferroviario" },
23
+ "05" => { "modal_dutoviario" => "cte_transporte_dutoviario" },
24
+ "06" => { "modal_multimodal" => "cte_transporte_multimodal" }
25
+ }.freeze
26
+
27
+ private
28
+
29
+ # @see FocusNfe::Recursos::Base#esquemas_extras
30
+ def esquemas_extras(dados)
31
+ MODAIS.fetch(dados["modal"], {})
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FocusNfe
4
+ module Recursos
5
+ # Recurso de CT-e Outros Serviços (CT-e OS). Emissão síncrona com consulta
6
+ # de estado e cancelamento.
7
+ class CteOs < Base
8
+ include Concerns::Emitivel
9
+ include Concerns::Consultavel
10
+ include Concerns::Cancelavel
11
+ include Concerns::CorrigivelCte
12
+ include Concerns::Notificavel
13
+
14
+ caminho_base "cte_os"
15
+
16
+ # @return [Hash{String=>Hash{String=>String}}] código do modal => chave do payload => nome do sub-esquema
17
+ MODAIS = {
18
+ "01" => { "modal_rodoviario" => "cte_os_transporte_rodoviario" }
19
+ }.freeze
20
+
21
+ private
22
+
23
+ # @see FocusNfe::Recursos::Base#esquemas_extras
24
+ def esquemas_extras(dados)
25
+ MODAIS.fetch(dados["modal"], {})
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FocusNfe
4
+ module Recursos
5
+ # Conhecimentos de transporte (CT-e) recebidos contra um CNPJ. Listagem com
6
+ # sincronização incremental, consulta por chave, downloads e prestação de
7
+ # desacordo de serviço.
8
+ class CtesRecebidas < Base
9
+ include Concerns::Listavel
10
+ include Concerns::Baixavel
11
+ include Concerns::BaixavelEventos
12
+ include Concerns::Notificavel
13
+
14
+ caminho_base "ctes_recebidas"
15
+
16
+ # @param chave [String] chave de acesso do CT-e
17
+ # @return [Hash] corpo cru da resposta
18
+ def consultar(chave)
19
+ connection.get(caminho_referencia(chave)).body
20
+ end
21
+
22
+ # Registra a prestação de desacordo do serviço de transporte.
23
+ #
24
+ # @param chave [String] chave de acesso do CT-e
25
+ # @param observacoes [String] motivo do desacordo (15 a 255 caracteres)
26
+ # @return [Hash] corpo cru da resposta
27
+ def desacordo(chave, observacoes:)
28
+ connection.post("#{caminho_referencia(chave)}/desacordo", body: { observacoes: observacoes }).body
29
+ end
30
+
31
+ # @param chave [String] chave de acesso do CT-e
32
+ # @return [Hash] corpo cru da resposta com o desacordo registrado
33
+ def consultar_desacordo(chave)
34
+ connection.get("#{caminho_referencia(chave)}/desacordo").body
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FocusNfe
4
+ module Recursos
5
+ # Recurso de Declaração de Conteúdo eletrônica (DC-e). Emissão assíncrona
6
+ # com consulta de estado e cancelamento.
7
+ class Dce < Base
8
+ include Concerns::Emitivel
9
+ include Concerns::Consultavel
10
+ include Concerns::Cancelavel
11
+ include Concerns::Notificavel
12
+
13
+ caminho_base "dce"
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FocusNfe
4
+ module Recursos
5
+ # Gestão de e-mails bloqueados (bounces/reclamações). O caminho da API é
6
+ # +blocked_emails+; o e-mail compõe o path e é escapado.
7
+ class EmailsBloqueados < Base
8
+ caminho_base "blocked_emails"
9
+
10
+ # @param email [String] e-mail a consultar
11
+ # @return [Hash] corpo cru da resposta com o motivo do bloqueio
12
+ def consultar(email)
13
+ connection.get(caminho_email(email)).body
14
+ end
15
+
16
+ # Solicita o desbloqueio (exclusão) de um e-mail.
17
+ #
18
+ # @param email [String] e-mail a desbloquear
19
+ # @return [Hash] corpo cru da resposta
20
+ def desbloquear(email)
21
+ connection.delete(caminho_email(email)).body
22
+ end
23
+
24
+ private
25
+
26
+ def caminho_email(email)
27
+ "#{caminho_base}/#{URI.encode_www_form_component(email)}"
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FocusNfe
4
+ module Recursos
5
+ # Gestão de empresas (CRUD). Disponível apenas em produção na Focus NFe; o
6
+ # certificado vai em base64 nos dados. A simulação (+dry_run+) valida o cadastro
7
+ # sem persistir.
8
+ class Empresas < Base
9
+ include Concerns::Listavel
10
+ include Concerns::Localizavel
11
+ include Concerns::Removivel
12
+
13
+ caminho_base "empresas"
14
+
15
+ # Cadastra uma empresa.
16
+ #
17
+ # @param dados [Hash] dados da empresa (certificado PFX em base64 quando aplicável)
18
+ # @param dry_run [Boolean] quando +true+, valida sem persistir (+dry_run=1+)
19
+ # @return [Hash] corpo cru da resposta
20
+ def criar(dados:, dry_run: false)
21
+ params = dry_run ? { dry_run: 1 } : {}
22
+ connection.post(caminho_base, params: params, body: dados).body
23
+ end
24
+
25
+ # Atualiza uma empresa existente.
26
+ #
27
+ # @param id [String, Integer] identificador da empresa
28
+ # @param dados [Hash] campos a atualizar
29
+ # @return [Hash] corpo cru da resposta
30
+ def atualizar(id, dados:)
31
+ connection.put(caminho_referencia(id), body: dados).body
32
+ end
33
+ end
34
+ end
35
+ end