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,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::Configuration do
4
+ describe "valores padrão" do
5
+ it "usa homologação, timeouts numéricos, tokens nil e cabeçalhos vazios" do
6
+ expect(described_class.new).to have_attributes(
7
+ token_empresa: nil, token_conta: nil, environment: :homologacao, timeout: 30,
8
+ open_timeout: 10, logger: nil, http_adapter: nil, headers: {}
9
+ )
10
+ end
11
+ end
12
+
13
+ describe "atributos" do
14
+ it "aceita todos os atributos via argumentos nomeados" do
15
+ logger = Object.new
16
+ config = described_class.new(token_empresa: "te", token_conta: "tc", environment: :producao,
17
+ timeout: 5, open_timeout: 2, logger: logger, headers: { "X" => "1" })
18
+
19
+ expect(config).to have_attributes(token_empresa: "te", token_conta: "tc", environment: :producao,
20
+ timeout: 5, open_timeout: 2, logger: logger, headers: { "X" => "1" })
21
+ end
22
+
23
+ it "permite escrever os tokens após a construção", :aggregate_failures do
24
+ config = described_class.new
25
+ config.token_empresa = "te-123"
26
+ config.token_conta = "tc-123"
27
+
28
+ expect(config).to have_attributes(token_empresa: "te-123", token_conta: "tc-123")
29
+ end
30
+
31
+ it "apenas armazena a referência ao logger nesta fase" do
32
+ logger = Object.new
33
+
34
+ expect(described_class.new(logger: logger).logger).to be(logger)
35
+ end
36
+ end
37
+
38
+ describe "#token_de" do
39
+ it "resolve o token de cada escopo", :aggregate_failures do
40
+ config = described_class.new(token_empresa: "te", token_conta: "tc")
41
+
42
+ expect(config.token_de(:empresa)).to eq("te")
43
+ expect(config.token_de(:conta)).to eq("tc")
44
+ end
45
+ end
46
+
47
+ describe "#base_url" do
48
+ it "resolve a URL de produção" do
49
+ expect(described_class.new(environment: :producao).base_url).to eq("https://api.focusnfe.com.br")
50
+ end
51
+
52
+ it "resolve a URL de homologação" do
53
+ expect(described_class.new(environment: :homologacao).base_url).to eq("https://homologacao.focusnfe.com.br")
54
+ end
55
+
56
+ it "levanta ConfigurationError para ambiente desconhecido" do
57
+ expect { described_class.new(environment: :sandbox).base_url }.to raise_error(FocusNfe::Errors::ConfigurationError)
58
+ end
59
+ end
60
+
61
+ describe "#validate!" do
62
+ it "levanta ConfigurationError quando nenhum token está presente" do
63
+ config = described_class.new(token_empresa: nil, token_conta: nil, environment: :producao)
64
+
65
+ expect { config.validate! }.to raise_error(FocusNfe::Errors::ConfigurationError, /token/)
66
+ end
67
+
68
+ it "levanta ConfigurationError quando os tokens são vazios ou só espaços" do
69
+ config = described_class.new(token_empresa: " ", token_conta: "", environment: :producao)
70
+
71
+ expect { config.validate! }.to raise_error(FocusNfe::Errors::ConfigurationError, /token/)
72
+ end
73
+
74
+ it "aceita apenas o token de empresa" do
75
+ config = described_class.new(token_empresa: "te", environment: :producao)
76
+
77
+ expect(config.validate!).to be(config)
78
+ end
79
+
80
+ it "aceita apenas o token de conta" do
81
+ config = described_class.new(token_conta: "tc", environment: :producao)
82
+
83
+ expect(config.validate!).to be(config)
84
+ end
85
+
86
+ it "levanta ConfigurationError quando o ambiente é desconhecido" do
87
+ config = described_class.new(token_empresa: "te", environment: :sandbox)
88
+
89
+ expect { config.validate! }.to raise_error(FocusNfe::Errors::ConfigurationError, /ambiente/)
90
+ end
91
+ end
92
+
93
+ describe "#validate_token!" do
94
+ it "devolve a própria configuração quando o token do escopo está presente" do
95
+ config = described_class.new(token_empresa: "te", token_conta: "tc")
96
+
97
+ expect(config.validate_token!(:empresa)).to be(config)
98
+ end
99
+
100
+ it "levanta ConfigurationError citando token_empresa quando o escopo :empresa não tem token" do
101
+ config = described_class.new(token_conta: "tc")
102
+
103
+ expect { config.validate_token!(:empresa) }
104
+ .to raise_error(FocusNfe::Errors::ConfigurationError, /token_empresa/)
105
+ end
106
+
107
+ it "levanta ConfigurationError citando token_conta quando o escopo :conta não tem token" do
108
+ config = described_class.new(token_empresa: "te")
109
+
110
+ expect { config.validate_token!(:conta) }
111
+ .to raise_error(FocusNfe::Errors::ConfigurationError, /token_conta/)
112
+ end
113
+
114
+ it "valida o ambiente antes do token" do
115
+ config = described_class.new(token_empresa: "te", environment: :sandbox)
116
+
117
+ expect { config.validate_token!(:empresa) }
118
+ .to raise_error(FocusNfe::Errors::ConfigurationError, /ambiente/)
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::Errors do
4
+ describe ".class_for" do
5
+ {
6
+ 400 => FocusNfe::Errors::BadRequest,
7
+ 401 => FocusNfe::Errors::Unauthorized,
8
+ 403 => FocusNfe::Errors::Forbidden,
9
+ 404 => FocusNfe::Errors::NotFound,
10
+ 409 => FocusNfe::Errors::Conflict,
11
+ 422 => FocusNfe::Errors::ValidationError,
12
+ 429 => FocusNfe::Errors::RateLimited
13
+ }.each do |status, klass|
14
+ it "mapeia #{status} para #{klass}" do
15
+ expect(described_class.class_for(status)).to eq(klass)
16
+ end
17
+ end
18
+
19
+ it "mapeia qualquer 5xx para ServerError", :aggregate_failures do
20
+ expect(described_class.class_for(500)).to eq(described_class::ServerError)
21
+ expect(described_class.class_for(503)).to eq(described_class::ServerError)
22
+ expect(described_class.class_for(599)).to eq(described_class::ServerError)
23
+ end
24
+
25
+ it "mapeia status não-2xx não previstos para UnexpectedResponse", :aggregate_failures do
26
+ expect(described_class.class_for(418)).to eq(described_class::UnexpectedResponse)
27
+ expect(described_class.class_for(451)).to eq(described_class::UnexpectedResponse)
28
+ expect(described_class.class_for(300)).to eq(described_class::UnexpectedResponse)
29
+ end
30
+ end
31
+
32
+ describe ".from_response" do
33
+ def response(status:, body: nil)
34
+ FocusNfe::HTTP::Response.new(
35
+ status: status,
36
+ headers: { "Content-Type" => "application/json" },
37
+ body: body
38
+ )
39
+ end
40
+
41
+ it "instancia a classe correta conforme o status" do
42
+ error = described_class.from_response(response(status: 422))
43
+
44
+ expect(error).to be_a(described_class::ValidationError)
45
+ end
46
+
47
+ it "preenche status, corpo e resposta a partir da Response", :aggregate_failures do
48
+ original = response(status: 422, body: '{"mensagem":"ref inválida"}')
49
+ error = described_class.from_response(original)
50
+
51
+ expect(error.status).to eq(422)
52
+ expect(error.body).to eq("mensagem" => "ref inválida")
53
+ expect(error.response).to be(original)
54
+ end
55
+
56
+ it "usa UnexpectedResponse para status não mapeado" do
57
+ error = described_class.from_response(response(status: 418))
58
+
59
+ expect(error).to be_a(described_class::UnexpectedResponse)
60
+ end
61
+
62
+ it "produz uma exceção levantável que carrega o status na mensagem" do
63
+ error = described_class.from_response(response(status: 500))
64
+
65
+ expect { raise error }.to raise_error(described_class::ServerError, /500/)
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::Errors do
4
+ describe "FocusNfe::Error (raiz da hierarquia)" do
5
+ it "descende de StandardError" do
6
+ expect(FocusNfe::Error.new).to be_a(StandardError)
7
+ end
8
+
9
+ it "substitui a antiga FocusNfe::Erro (português), que deixa de existir" do
10
+ expect(FocusNfe.const_defined?(:Erro, false)).to be(false)
11
+ end
12
+ end
13
+
14
+ describe FocusNfe::Errors::HttpError do
15
+ it "descende de FocusNfe::Error" do
16
+ expect(described_class.ancestors).to include(FocusNfe::Error)
17
+ end
18
+
19
+ it "expõe status, corpo e resposta informados na construção" do
20
+ response = Object.new
21
+ error = described_class.new("falhou", status: 422, body: { "msg" => "ref" }, response: response)
22
+
23
+ expect(error).to have_attributes(message: "falhou", status: 422, body: { "msg" => "ref" }, response: response)
24
+ end
25
+
26
+ it "pode ser construído sem argumentos, com leitores nil", :aggregate_failures do
27
+ error = described_class.new
28
+
29
+ expect(error.status).to be_nil
30
+ expect(error.body).to be_nil
31
+ expect(error.response).to be_nil
32
+ end
33
+
34
+ describe "#codigo" do
35
+ it "extrai o código do corpo estruturado" do
36
+ error = described_class.new(body: { "codigo" => "nao_encontrado", "mensagem" => "..." })
37
+
38
+ expect(error.codigo).to eq("nao_encontrado")
39
+ end
40
+
41
+ it "é nil quando o corpo não traz codigo, é String ou é nil", :aggregate_failures do
42
+ expect(described_class.new(body: { "mensagem" => "..." }).codigo).to be_nil
43
+ expect(described_class.new(body: "<html>500</html>").codigo).to be_nil
44
+ expect(described_class.new(body: nil).codigo).to be_nil
45
+ end
46
+ end
47
+
48
+ describe "#erros" do
49
+ it "devolve a lista de validação como está" do
50
+ lista = [{ "campo" => "natureza_operacao", "mensagem" => "não pode ficar em branco" }]
51
+ error = described_class.new(body: { "erros" => lista })
52
+
53
+ expect(error.erros).to eq(lista)
54
+ end
55
+
56
+ it "embrulha o erro único no formato { campo, mensagem }" do
57
+ error = described_class.new(body: { "codigo" => "x", "mensagem" => "A NFe não está autorizada" })
58
+
59
+ expect(error.erros).to eq([{ "campo" => nil, "mensagem" => "A NFe não está autorizada" }])
60
+ end
61
+
62
+ it "é vazio sem erros nem mensagem, ou com corpo não-Hash", :aggregate_failures do
63
+ expect(described_class.new(body: { "codigo" => "x" }).erros).to eq([])
64
+ expect(described_class.new(body: "<html>500</html>").erros).to eq([])
65
+ expect(described_class.new(body: nil).erros).to eq([])
66
+ end
67
+ end
68
+ end
69
+
70
+ describe "subclasses tipadas de HttpError" do
71
+ %i[
72
+ BadRequest Unauthorized Forbidden NotFound Conflict
73
+ ValidationError RateLimited ServerError UnexpectedResponse
74
+ ].each do |name|
75
+ it "#{name} descende de HttpError, Error e StandardError", :aggregate_failures do
76
+ klass = described_class.const_get(name)
77
+
78
+ expect(klass.ancestors).to include(FocusNfe::Errors::HttpError)
79
+ expect(klass.ancestors).to include(FocusNfe::Error)
80
+ expect(klass.ancestors).to include(StandardError)
81
+ end
82
+ end
83
+
84
+ it "reúnem exatamente 9 classes distintas, sem aliasing" do
85
+ todas = described_class.constants.map { |name| described_class.const_get(name) }
86
+ subclasses = todas.select { |const| const.is_a?(Class) && const < described_class::HttpError }
87
+
88
+ expect(subclasses.uniq.size).to eq(9)
89
+ end
90
+ end
91
+
92
+ describe "erros não-HTTP (client-side e transporte)" do
93
+ it "ConfigurationError descende de Error, mas não de HttpError", :aggregate_failures do
94
+ ancestrais = FocusNfe::Errors::ConfigurationError.ancestors
95
+
96
+ expect(ancestrais).to include(FocusNfe::Error)
97
+ expect(ancestrais).not_to include(FocusNfe::Errors::HttpError)
98
+ end
99
+
100
+ it "ConnectionError descende de Error, mas não de HttpError", :aggregate_failures do
101
+ ancestrais = FocusNfe::Errors::ConnectionError.ancestors
102
+
103
+ expect(ancestrais).to include(FocusNfe::Error)
104
+ expect(ancestrais).not_to include(FocusNfe::Errors::HttpError)
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,291 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::Esquemas::Campo do
4
+ def campo(atributos)
5
+ described_class.new(atributos)
6
+ end
7
+
8
+ describe "#nome e #obrigatorio?" do
9
+ it "lê o nome e a obrigatoriedade da definição", :aggregate_failures do
10
+ c = campo("name" => "natureza_operacao", "type" => "String[1-60]", "required" => true)
11
+
12
+ expect(c.nome).to eq("natureza_operacao")
13
+ expect(c).to be_obrigatorio
14
+ end
15
+
16
+ it "não é obrigatório quando required é false" do
17
+ expect(campo("name" => "x", "type" => "String[1-60]", "required" => false)).not_to be_obrigatorio
18
+ end
19
+ end
20
+
21
+ describe "parsing do tipo" do
22
+ it "interpreta String[1-60] como string com mínimo e máximo", :aggregate_failures do
23
+ c = campo("name" => "x", "type" => "String[1-60]")
24
+
25
+ expect(c.tipo).to eq(:string)
26
+ expect(c.tamanho_minimo).to eq(1)
27
+ expect(c.tamanho_maximo).to eq(60)
28
+ end
29
+
30
+ it "interpreta String[14] como tamanho fixo", :aggregate_failures do
31
+ c = campo("name" => "x", "type" => "String[14]")
32
+
33
+ expect(c.tipo).to eq(:string)
34
+ expect(c.tamanho_minimo).to eq(14)
35
+ expect(c.tamanho_maximo).to eq(14)
36
+ end
37
+
38
+ it "interpreta Integer[1-9] como inteiro com faixa de dígitos", :aggregate_failures do
39
+ c = campo("name" => "x", "type" => "Integer[1-9]")
40
+
41
+ expect(c.tipo).to eq(:integer)
42
+ expect(c.tamanho_minimo).to eq(1)
43
+ expect(c.tamanho_maximo).to eq(9)
44
+ end
45
+
46
+ it "interpreta Decimal[13.2] como inteiros e casas fixas", :aggregate_failures do
47
+ c = campo("name" => "x", "type" => "Decimal[13.2]")
48
+
49
+ expect(c.tipo).to eq(:decimal)
50
+ expect(c.decimal.inteiros).to eq(13)
51
+ expect(c.decimal.casas_minimas).to eq(2)
52
+ expect(c.decimal.casas_maximas).to eq(2)
53
+ end
54
+
55
+ it "interpreta Decimal[13.2-4] como faixa de casas", :aggregate_failures do
56
+ c = campo("name" => "x", "type" => "Decimal[13.2-4]")
57
+
58
+ expect(c.decimal.casas_minimas).to eq(2)
59
+ expect(c.decimal.casas_maximas).to eq(4)
60
+ end
61
+
62
+ it "interpreta Decimal[13.] como zero casas", :aggregate_failures do
63
+ c = campo("name" => "x", "type" => "Decimal[13.]")
64
+
65
+ expect(c.decimal.inteiros).to eq(13)
66
+ expect(c.decimal.casas_minimas).to eq(0)
67
+ expect(c.decimal.casas_maximas).to eq(0)
68
+ end
69
+
70
+ it "interpreta Decimal[2] sem ponto como zero casas", :aggregate_failures do
71
+ c = campo("name" => "x", "type" => "Decimal[2]")
72
+
73
+ expect(c.decimal.inteiros).to eq(2)
74
+ expect(c.decimal.casas_maximas).to eq(0)
75
+ end
76
+
77
+ it "interpreta Decimal[11.0-10] com casas a partir de zero", :aggregate_failures do
78
+ c = campo("name" => "x", "type" => "Decimal[11.0-10]")
79
+
80
+ expect(c.decimal.inteiros).to eq(11)
81
+ expect(c.decimal.casas_minimas).to eq(0)
82
+ expect(c.decimal.casas_maximas).to eq(10)
83
+ end
84
+
85
+ it "interpreta DateTime" do
86
+ expect(campo("name" => "x", "type" => "DateTime").tipo).to eq(:datetime)
87
+ end
88
+
89
+ it "interpreta Date" do
90
+ expect(campo("name" => "x", "type" => "Date").tipo).to eq(:date)
91
+ end
92
+
93
+ it "interpreta type nulo como enum quando há enum" do
94
+ c = campo("name" => "x", "type" => nil, "enum" => "* +1+: Sim")
95
+
96
+ expect(c.tipo).to eq(:enum)
97
+ end
98
+
99
+ it "marca coleções", :aggregate_failures do
100
+ c = campo("name" => "items", "type" => "Coleção[1-990]", "collection" => { "object_attributes" => [] })
101
+
102
+ expect(c).to be_colecao
103
+ expect(c.tipo).to eq(:colecao)
104
+ end
105
+ end
106
+
107
+ describe "#esquema_colecao" do
108
+ let(:atributos) do
109
+ [
110
+ { "name" => "numero", "type" => "Integer[1-3]", "required" => true },
111
+ { "name" => "descricao", "type" => "String[1-120]", "required" => true }
112
+ ]
113
+ end
114
+
115
+ it "devolve nil para campo escalar" do
116
+ expect(campo("name" => "x", "type" => "String[1-60]").esquema_colecao).to be_nil
117
+ end
118
+
119
+ it "devolve nil para coleção sem object_attributes" do
120
+ expect(campo("name" => "x", "type" => "Coleção[0-5]", "collection" => {}).esquema_colecao).to be_nil
121
+ end
122
+
123
+ it "constrói um Esquema com os subcampos da coleção" do
124
+ c = campo("name" => "items", "type" => "Coleção[1-990]", "collection" => { "object_attributes" => atributos })
125
+
126
+ expect(c.esquema_colecao.campos.map(&:nome)).to eq(%w[numero descricao])
127
+ end
128
+ end
129
+
130
+ describe "#to_h" do
131
+ it "descreve um campo escalar com tipo, tamanho e metadados", :aggregate_failures do
132
+ h = campo(
133
+ "name" => "natureza_operacao", "description" => "Descrição da natureza de operação.",
134
+ "type" => "String[1-60]", "required" => true, "tag" => "natOp"
135
+ ).to_h
136
+
137
+ expect(h).to include(
138
+ nome: "natureza_operacao", descricao: "Descrição da natureza de operação.",
139
+ tipo: :string, tipo_bruto: "String[1-60]", obrigatorio: true,
140
+ tamanho_minimo: 1, tamanho_maximo: 60, enum: nil, tag: "natOp", colecao: nil
141
+ )
142
+ end
143
+
144
+ it "expõe o enum e marca o tipo :enum", :aggregate_failures do
145
+ h = campo("name" => "tipo_documento", "type" => nil, "enum" => "* +1+: Sim").to_h
146
+
147
+ expect(h[:tipo]).to eq(:enum)
148
+ expect(h[:enum]).to eq("* +1+: Sim")
149
+ end
150
+
151
+ it "aninha os subcampos de uma coleção em :colecao", :aggregate_failures do
152
+ atributos = [{ "name" => "numero", "type" => "Integer[1-3]", "required" => true }]
153
+ colecao = { "object_attributes" => atributos }
154
+ h = campo("name" => "items", "type" => "Coleção[1-990]", "collection" => colecao).to_h
155
+
156
+ expect(h[:tipo]).to eq(:colecao)
157
+ expect(h[:colecao]).to eq([campo(atributos.first).to_h])
158
+ end
159
+
160
+ it "deixa :colecao nil para coleção sem object_attributes" do
161
+ h = campo("name" => "x", "type" => "Coleção[0-5]", "collection" => {}).to_h
162
+
163
+ expect(h[:colecao]).to be_nil
164
+ end
165
+ end
166
+
167
+ describe "#validar_valor" do
168
+ it "aceita string dentro do tamanho" do
169
+ expect(campo("name" => "x", "type" => "String[1-60]").validar_valor("ok")).to be_nil
170
+ end
171
+
172
+ it "rejeita string acima do máximo" do
173
+ mensagem = campo("name" => "natureza_operacao", "type" => "String[1-3]").validar_valor("abcd")
174
+
175
+ expect(mensagem).to include("natureza_operacao")
176
+ end
177
+
178
+ it "rejeita inteiro com não-dígitos" do
179
+ expect(campo("name" => "numero", "type" => "Integer[1-9]").validar_valor("12a")).to include("numero")
180
+ end
181
+
182
+ it "aceita inteiro válido na faixa de dígitos" do
183
+ expect(campo("name" => "numero", "type" => "Integer[1-9]").validar_valor(12_345)).to be_nil
184
+ end
185
+
186
+ it "rejeita inteiro com quantidade de dígitos fora da faixa", :aggregate_failures do
187
+ mensagem = campo("name" => "numero", "type" => "Integer[1-3]").validar_valor(12_345)
188
+
189
+ expect(mensagem).to include("numero")
190
+ expect(mensagem).to include("5 dígitos")
191
+ end
192
+
193
+ it "não restringe coleções nem tipos desconhecidos", :aggregate_failures do
194
+ expect(campo("name" => "x", "type" => "Coleção[0-5]", "collection" => {}).validar_valor([])).to be_nil
195
+ expect(campo("name" => "x", "type" => "Algo[1-2]").validar_valor("qualquer")).to be_nil
196
+ end
197
+
198
+ context "com decimais" do
199
+ it "aceita decimal dentro de inteiros e casas" do
200
+ expect(campo("name" => "valor", "type" => "Decimal[13.2]").validar_valor("10.50")).to be_nil
201
+ end
202
+
203
+ it "aceita menos casas que o mínimo" do
204
+ expect(campo("name" => "valor", "type" => "Decimal[13.2]").validar_valor("10")).to be_nil
205
+ end
206
+
207
+ it "aceita Numeric além de String" do
208
+ expect(campo("name" => "valor", "type" => "Decimal[13.2]").validar_valor(10.5)).to be_nil
209
+ end
210
+
211
+ it "rejeita inteiros demais", :aggregate_failures do
212
+ mensagem = campo("name" => "valor", "type" => "Decimal[2.2]").validar_valor("1234.5")
213
+
214
+ expect(mensagem).to include("valor")
215
+ end
216
+
217
+ it "rejeita casas decimais além do máximo" do
218
+ expect(campo("name" => "valor", "type" => "Decimal[13.2]").validar_valor("10.123")).to include("valor")
219
+ end
220
+
221
+ it "rejeita valor não numérico" do
222
+ expect(campo("name" => "valor", "type" => "Decimal[13.2]").validar_valor("abc")).to include("valor")
223
+ end
224
+ end
225
+
226
+ context "com enums" do
227
+ it "aceita valor dentro do conjunto declarado" do
228
+ expect(campo("name" => "modalidade", "type" => nil, "enum" => "* +1+: Sim").validar_valor("1")).to be_nil
229
+ end
230
+
231
+ it "aceita Integer correspondente ao código String" do
232
+ expect(campo("name" => "modalidade", "type" => nil, "enum" => "* +1+: Sim").validar_valor(1)).to be_nil
233
+ end
234
+
235
+ it "rejeita valor fora do conjunto" do
236
+ c = campo("name" => "modalidade", "type" => nil, "enum" => "* +0+: Não\\n* +1+: Sim")
237
+
238
+ expect(c.validar_valor("9")).to include("modalidade")
239
+ end
240
+
241
+ it "valida o conjunto mesmo quando o campo também tem tipo escalar" do
242
+ c = campo("name" => "indicador", "type" => "Integer[1-1]", "enum" => "* +0+: Não\\n* +1+: Sim")
243
+
244
+ expect(c.validar_valor(9)).to include("indicador")
245
+ end
246
+
247
+ it "rejeita o tipo escalar antes do enum" do
248
+ c = campo("name" => "indicador", "type" => "Integer[1-1]", "enum" => "* +1+: Sim")
249
+
250
+ expect(c.validar_valor("x")).to include("indicador")
251
+ end
252
+ end
253
+
254
+ context "com datas" do
255
+ it "aceita Date em ISO 8601" do
256
+ expect(campo("name" => "data", "type" => "Date").validar_valor("2026-06-14")).to be_nil
257
+ end
258
+
259
+ it "rejeita Date inválida" do
260
+ expect(campo("name" => "data", "type" => "Date").validar_valor("14/06/2026")).to include("data")
261
+ end
262
+
263
+ it "aceita DateTime com offset" do
264
+ expect(campo("name" => "emissao", "type" => "DateTime").validar_valor("2026-06-14T10:00:00-03:00")).to be_nil
265
+ end
266
+
267
+ it "rejeita DateTime inválido" do
268
+ expect(campo("name" => "emissao", "type" => "DateTime").validar_valor("qualquer")).to include("emissao")
269
+ end
270
+ end
271
+ end
272
+
273
+ describe "#valores_enum" do
274
+ it "extrai códigos do formato com espaço" do
275
+ c = campo("name" => "x", "type" => nil, "enum" => "* +0+: Correios\\n* +1+: Conta própria")
276
+
277
+ expect(c.valores_enum).to eq(%w[0 1])
278
+ end
279
+
280
+ it "extrai códigos do formato sem espaço e multi-caractere" do
281
+ c = campo("name" => "x", "type" => nil, "enum" => "*+01+: Repasse\\n*+99+: Outros")
282
+
283
+ expect(c.valores_enum).to eq(%w[01 99])
284
+ end
285
+
286
+ it "devolve vazio para enum em branco", :aggregate_failures do
287
+ expect(campo("name" => "x", "type" => "String[1-2]", "enum" => "").valores_enum).to eq([])
288
+ expect(campo("name" => "x", "type" => "String[1-2]").enum?).to be(false)
289
+ end
290
+ end
291
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::Esquemas::Decimal do
4
+ describe ".parsear" do
5
+ it "devolve nil para tipo que não é decimal", :aggregate_failures do
6
+ expect(described_class.parsear(nil)).to be_nil
7
+ expect(described_class.parsear("String[1-60]")).to be_nil
8
+ end
9
+
10
+ it "parseia inteiros e a faixa de casas", :aggregate_failures do
11
+ d = described_class.parsear("Decimal[13.2-4]")
12
+
13
+ expect(d.inteiros).to eq(13)
14
+ expect(d.casas_minimas).to eq(2)
15
+ expect(d.casas_maximas).to eq(4)
16
+ end
17
+
18
+ it "trata ponto sem casas como zero casas" do
19
+ expect(described_class.parsear("Decimal[13.]").casas_maximas).to eq(0)
20
+ end
21
+ end
22
+
23
+ describe "#validar" do
24
+ subject(:decimal) { described_class.parsear("Decimal[13.2]") }
25
+
26
+ it "aceita valor dentro de inteiros e casas" do
27
+ expect(decimal.validar("10.50")).to be_nil
28
+ end
29
+
30
+ it "aceita menos casas que o mínimo" do
31
+ expect(decimal.validar("10")).to be_nil
32
+ end
33
+
34
+ it "rejeita inteiros demais" do
35
+ expect(described_class.parsear("Decimal[2.2]").validar("1234.5")).to include("inteiros")
36
+ end
37
+
38
+ it "rejeita casas decimais além do máximo" do
39
+ expect(decimal.validar("10.123")).to include("casas")
40
+ end
41
+
42
+ it "rejeita valor não numérico" do
43
+ expect(decimal.validar("abc")).to include("inválido")
44
+ end
45
+ end
46
+
47
+ describe "#to_h" do
48
+ it "descreve a especificação" do
49
+ expect(described_class.parsear("Decimal[13.2-4]").to_h).to eq(
50
+ inteiros: 13, casas_minimas: 2, casas_maximas: 4
51
+ )
52
+ end
53
+ end
54
+ end