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,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::Modelos::Inutilizacao do
4
+ def response(body:, status: 200)
5
+ FocusNfe::HTTP::Response.new(
6
+ status: status,
7
+ headers: { "Content-Type" => "application/json" },
8
+ body: JSON.generate(body)
9
+ )
10
+ end
11
+
12
+ describe ".from_response" do
13
+ it "mapeia status, status_sefaz e mensagem_sefaz", :aggregate_failures do
14
+ corpo = { "status" => "autorizado", "status_sefaz" => "102", "mensagem_sefaz" => "ok" }
15
+ inut = described_class.from_response(response(body: corpo))
16
+
17
+ expect(inut).to have_attributes(status: "autorizado", status_sefaz: "102", mensagem_sefaz: "ok")
18
+ end
19
+
20
+ it "mapeia serie, numero_inicial e numero_final", :aggregate_failures do
21
+ corpo = { "serie" => "1", "numero_inicial" => "10", "numero_final" => "20" }
22
+ inut = described_class.from_response(response(body: corpo))
23
+
24
+ expect(inut).to have_attributes(serie: "1", numero_inicial: "10", numero_final: "20")
25
+ end
26
+
27
+ it "expõe protocolo a partir de protocolo_sefaz" do
28
+ inut = described_class.from_response(response(body: { "protocolo_sefaz" => "135200" }))
29
+
30
+ expect(inut.protocolo).to eq("135200")
31
+ end
32
+
33
+ it "guarda a resposta original para inspeção" do
34
+ resp = response(body: { "status" => "autorizado" })
35
+
36
+ expect(described_class.from_response(resp).response).to be(resp)
37
+ end
38
+
39
+ it "usa dados vazios quando o corpo não é um Hash", :aggregate_failures do
40
+ inut = described_class.from_response(response(body: ["x"]))
41
+
42
+ expect(inut.dados).to eq({})
43
+ expect(inut.status).to be_nil
44
+ end
45
+ end
46
+
47
+ describe ".from_item" do
48
+ it "constrói a partir de um Hash cru, sem resposta", :aggregate_failures do
49
+ inut = described_class.from_item("status" => "autorizado", "protocolo_sefaz" => "1")
50
+
51
+ expect(inut.response).to be_nil
52
+ expect(inut).to have_attributes(status: "autorizado", protocolo: "1")
53
+ end
54
+
55
+ it "usa dados vazios quando o item não é um Hash" do
56
+ expect(described_class.from_item(nil).dados).to eq({})
57
+ end
58
+ end
59
+
60
+ describe "#autorizado?" do
61
+ def inut(status)
62
+ described_class.from_item("status" => status)
63
+ end
64
+
65
+ it "é verdadeiro só para 'autorizado'", :aggregate_failures do
66
+ expect(inut("autorizado")).to be_autorizado
67
+ expect(inut("erro_autorizacao")).not_to be_autorizado
68
+ end
69
+ end
70
+
71
+ describe "acesso bruto" do
72
+ subject(:inut) do
73
+ described_class.from_item("status" => "autorizado", "cnpj" => "123", "modelo" => "55", "caminho_xml" => "/x.xml")
74
+ end
75
+
76
+ it "expõe o Hash cru via #dados" do
77
+ expect(inut.dados).to include("cnpj" => "123", "modelo" => "55", "caminho_xml" => "/x.xml")
78
+ end
79
+
80
+ it "delega #[] ao Hash para campos não mapeados", :aggregate_failures do
81
+ expect(inut["cnpj"]).to eq("123")
82
+ expect(inut["caminho_xml"]).to eq("/x.xml")
83
+ end
84
+ end
85
+
86
+ describe "imutabilidade" do
87
+ it "congela a instância" do
88
+ expect(described_class.from_item("status" => "autorizado")).to be_frozen
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::Modelos::Pagina do
4
+ def response(body:, headers: {})
5
+ FocusNfe::HTTP::Response.new(
6
+ status: 200,
7
+ headers: { "Content-Type" => "application/json" }.merge(headers),
8
+ body: JSON.generate(body)
9
+ )
10
+ end
11
+
12
+ describe ".from_response" do
13
+ it "expõe os itens do corpo (Array)" do
14
+ pagina = described_class.from_response(response(body: [{ "a" => 1 }, { "a" => 2 }]))
15
+
16
+ expect(pagina.itens).to eq([{ "a" => 1 }, { "a" => 2 }])
17
+ end
18
+
19
+ it "usa lista vazia quando o corpo não é um Array" do
20
+ pagina = described_class.from_response(response(body: { "erro" => "x" }))
21
+
22
+ expect(pagina.itens).to eq([])
23
+ end
24
+
25
+ it "lê total e versao_maxima dos headers de paginação", :aggregate_failures do
26
+ pagina = described_class.from_response(
27
+ response(body: [], headers: { "X-Total-Count" => "42", "X-Max-Version" => "7" })
28
+ )
29
+
30
+ expect(pagina.total).to eq(42)
31
+ expect(pagina.versao_maxima).to eq(7)
32
+ end
33
+
34
+ it "deixa total e versao_maxima nil quando os headers estão ausentes", :aggregate_failures do
35
+ pagina = described_class.from_response(response(body: []))
36
+
37
+ expect(pagina.total).to be_nil
38
+ expect(pagina.versao_maxima).to be_nil
39
+ end
40
+
41
+ it "guarda a resposta original" do
42
+ resp = response(body: [])
43
+
44
+ expect(described_class.from_response(resp).response).to be(resp)
45
+ end
46
+ end
47
+
48
+ describe "enumerável" do
49
+ subject(:pagina) { described_class.from_response(response(body: [1, 2, 3])) }
50
+
51
+ it "itera com cada", :aggregate_failures do
52
+ coletados = []
53
+ retorno = pagina.cada { |item| coletados << item }
54
+
55
+ expect(coletados).to eq([1, 2, 3])
56
+ expect(retorno).to be(pagina)
57
+ end
58
+
59
+ it "compõe os helpers de Enumerable (map/select)", :aggregate_failures do
60
+ expect(pagina.map { |i| i * 2 }).to eq([2, 4, 6])
61
+ expect(pagina.select(&:even?)).to eq([2])
62
+ end
63
+
64
+ it "devolve um Enumerator quando chamado sem bloco", :aggregate_failures do
65
+ enum = pagina.cada
66
+
67
+ expect(enum).to be_a(Enumerator)
68
+ expect(enum.to_a).to eq([1, 2, 3])
69
+ end
70
+ end
71
+
72
+ describe "imutabilidade" do
73
+ it "congela a instância" do
74
+ expect(described_class.from_response(response(body: []))).to be_frozen
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::Recursos::Backups do
4
+ include_context "com recurso conectado"
5
+
6
+ let(:cnpj) { "12345678000123" }
7
+
8
+ it "usa o caminho_base 'backups'" do
9
+ expect(recurso.caminho_base).to eq("backups")
10
+ end
11
+
12
+ describe "#consultar" do
13
+ it "faz GET em /v2/backups/{cnpj}.json e devolve o corpo cru" do
14
+ stub_get("backups/#{cnpj}.json", body: '[{"mes":"202605"}]')
15
+
16
+ expect(recurso.consultar(cnpj)).to eq([{ "mes" => "202605" }])
17
+ end
18
+
19
+ it "escapa o cnpj, sem injetar query nem traversal no path" do
20
+ stub_request(:get, /focusnfe/).to_return(status: 200, body: "[]", headers: json)
21
+
22
+ recurso.consultar("../empresas/1?x=y")
23
+
24
+ enviado = a_request(:get, /focusnfe/)
25
+ .with { |req| req.uri.query.nil? && req.uri.path.include?("..%2Fempresas%2F1%3Fx") }
26
+ expect(enviado).to have_been_made
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::Recursos::Base do
4
+ let(:connection) { instance_double(FocusNfe::HTTP::Connection) }
5
+
6
+ describe "DSL caminho_base" do
7
+ it "expõe o caminho declarado na classe via método de instância" do
8
+ classe = Class.new(described_class) { caminho_base "nfe" }
9
+
10
+ expect(classe.new(connection).caminho_base).to eq("nfe")
11
+ end
12
+
13
+ it "é por classe — subclasses não vazam entre si", :aggregate_failures do
14
+ uma = Class.new(described_class) { caminho_base "nfe" }
15
+ outra = Class.new(described_class) { caminho_base "nfce" }
16
+
17
+ expect(uma.new(connection).caminho_base).to eq("nfe")
18
+ expect(outra.new(connection).caminho_base).to eq("nfce")
19
+ end
20
+ end
21
+
22
+ describe "#validar_referencia!" do
23
+ subject(:recurso) { Class.new(described_class) { caminho_base "nfe" }.new(connection) }
24
+
25
+ it "aceita refs alfanuméricas com hífen/underscore", :aggregate_failures do
26
+ expect { recurso.send(:validar_referencia!, "pedido-42") }.not_to raise_error
27
+ expect { recurso.send(:validar_referencia!, "venda_1001") }.not_to raise_error
28
+ end
29
+
30
+ it "rejeita refs com espaço ou vazias", :aggregate_failures do
31
+ expect { recurso.send(:validar_referencia!, "a b") }.to raise_error(ArgumentError)
32
+ expect { recurso.send(:validar_referencia!, "") }.to raise_error(ArgumentError)
33
+ expect { recurso.send(:validar_referencia!, nil) }.to raise_error(ArgumentError)
34
+ end
35
+ end
36
+
37
+ describe "#caminho_referencia" do
38
+ subject(:recurso) { Class.new(described_class) { caminho_base "nfe" }.new(connection) }
39
+
40
+ it "combina o caminho base com a ref" do
41
+ expect(recurso.send(:caminho_referencia, "pedido-42")).to eq("nfe/pedido-42")
42
+ end
43
+
44
+ it "preserva identificadores comuns (dígitos, hífen, underscore, ponto)", :aggregate_failures do
45
+ expect(recurso.send(:caminho_referencia, "12345678000190")).to eq("nfe/12345678000190")
46
+ expect(recurso.send(:caminho_referencia, "venda_1001")).to eq("nfe/venda_1001")
47
+ expect(recurso.send(:caminho_referencia, "35200114200166000187.55")).to eq("nfe/35200114200166000187.55")
48
+ end
49
+
50
+ it "escapa caracteres que sequestrariam o path ou injetariam query", :aggregate_failures do
51
+ expect(recurso.send(:caminho_referencia, "a/b")).to eq("nfe/a%2Fb")
52
+ expect(recurso.send(:caminho_referencia, "x?y")).to eq("nfe/x%3Fy")
53
+ expect(recurso.send(:caminho_referencia, "../../empresas")).to eq("nfe/..%2F..%2Fempresas")
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::Recursos::Ceps do
4
+ it_behaves_like "um recurso listável", "ceps"
5
+ it_behaves_like "um recurso localizável", "ceps"
6
+
7
+ include_context "com recurso conectado"
8
+
9
+ describe "#buscar" do
10
+ it "é um apelido de #listar e devolve uma Pagina" do
11
+ stub_get("ceps", query: { uf: "AM", logradouro: "Eduardo" }, body: "[]")
12
+
13
+ expect(recurso.buscar(uf: "AM", logradouro: "Eduardo")).to be_a(FocusNfe::Modelos::Pagina)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::Recursos::Cfops do
4
+ it_behaves_like "um recurso listável", "cfops"
5
+ it_behaves_like "um recurso localizável", "cfops"
6
+
7
+ include_context "com recurso conectado"
8
+
9
+ describe "#buscar" do
10
+ it "é um apelido de #listar" do
11
+ stub_get("cfops", query: { descricao: "venda" }, body: "[]")
12
+
13
+ expect(recurso.buscar(descricao: "venda")).to be_a(FocusNfe::Modelos::Pagina)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::Recursos::Cnaes do
4
+ it_behaves_like "um recurso listável", "codigos_cnae"
5
+ it_behaves_like "um recurso localizável", "codigos_cnae"
6
+
7
+ include_context "com recurso conectado"
8
+
9
+ it "usa o caminho_base 'codigos_cnae'" do
10
+ expect(recurso.caminho_base).to eq("codigos_cnae")
11
+ end
12
+
13
+ describe "#buscar" do
14
+ it "é um apelido de #listar" do
15
+ stub_get("codigos_cnae", query: { descricao: "software" }, body: "[]")
16
+
17
+ expect(recurso.buscar(descricao: "software")).to be_a(FocusNfe::Modelos::Pagina)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::Recursos::Cnpjs do
4
+ it_behaves_like "um recurso localizável", "cnpjs"
5
+
6
+ include_context "com recurso conectado"
7
+
8
+ it "não expõe listagem" do
9
+ expect(recurso).not_to respond_to(:listar)
10
+ end
11
+ end
@@ -0,0 +1,158 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::Recursos::Concerns::Emitivel do
4
+ let(:client) { FocusNfe::Client.new(token_empresa: "tok", environment: :homologacao) }
5
+ let(:nfe) { FocusNfe::Recursos::Nfe.new(client.connection) }
6
+ let(:nfce) { FocusNfe::Recursos::Nfce.new(client.connection) }
7
+ let(:cte) { FocusNfe::Recursos::Cte.new(client.connection) }
8
+ let(:cte_os) { FocusNfe::Recursos::CteOs.new(client.connection) }
9
+ let(:mdfe) { FocusNfe::Recursos::Mdfe.new(client.connection) }
10
+
11
+ def json = { "Content-Type" => "application/json" }
12
+ def processando = '{"status":"processando_autorizacao"}'
13
+ def homologacao = "https://homologacao.focusnfe.com.br"
14
+
15
+ def stub_emissao(path, status: 202)
16
+ stub_request(:post, "#{homologacao}/v2/#{path}").to_return(status: status, body: processando, headers: json)
17
+ end
18
+
19
+ describe "#emitir com validar: true" do
20
+ context "quando o documento tem schema empacotado" do
21
+ it "levanta ErroDeValidacao sem fazer requisição quando faltam obrigatórios", :aggregate_failures do
22
+ stub = stub_emissao("nfe?ref=pedido-1")
23
+
24
+ expect { nfe.emitir(ref: "pedido-1", dados: {}, validar: true) }
25
+ .to raise_error(FocusNfe::Esquemas::ErroDeValidacao)
26
+ expect(stub).not_to have_been_requested
27
+ end
28
+
29
+ it "emite normalmente quando os dados são válidos" do
30
+ esquema = FocusNfe::Esquemas::Esquema.new([{ "name" => "natureza_operacao", "type" => "String[1-60]" }])
31
+ allow(FocusNfe::Esquemas::Esquema).to receive(:carregar).with("nfe").and_return(esquema)
32
+ stub_emissao("nfe?ref=pedido-1")
33
+
34
+ nfe.emitir(ref: "pedido-1", dados: { "natureza_operacao" => "Venda" }, validar: true)
35
+
36
+ expect(a_request(:post, "#{homologacao}/v2/nfe?ref=pedido-1")).to have_been_made
37
+ end
38
+
39
+ it "não vaza o parâmetro validar para a query string" do
40
+ esquema = FocusNfe::Esquemas::Esquema.new([])
41
+ allow(FocusNfe::Esquemas::Esquema).to receive(:carregar).with("nfe").and_return(esquema)
42
+ stub_emissao("nfe?ref=pedido-1")
43
+
44
+ nfe.emitir(ref: "pedido-1", dados: {}, validar: true)
45
+
46
+ expect(a_request(:post, "#{homologacao}/v2/nfe").with(query: { "ref" => "pedido-1" })).to have_been_made
47
+ end
48
+ end
49
+
50
+ context "quando o documento tem modal condicional (CTe)" do
51
+ def stub_esquema_base_vazio
52
+ vazio = FocusNfe::Esquemas::Esquema.new([])
53
+ allow(FocusNfe::Esquemas::Esquema).to receive(:carregar).and_call_original
54
+ allow(FocusNfe::Esquemas::Esquema).to receive(:carregar).with("cte").and_return(vazio)
55
+ end
56
+
57
+ it "valida o sub-esquema do modal e bloqueia a emissão" do
58
+ stub_esquema_base_vazio
59
+
60
+ expect do
61
+ cte.emitir(ref: "cte-1", dados: { "modal" => "01", "modal_rodoviario" => { "rntrc" => "1" } }, validar: true)
62
+ end.to raise_error(FocusNfe::Esquemas::ErroDeValidacao, /modal_rodoviario/)
63
+ end
64
+
65
+ it "emite quando base e modal são válidos" do
66
+ stub_esquema_base_vazio
67
+ stub_emissao("cte?ref=cte-1")
68
+
69
+ cte.emitir(ref: "cte-1", dados: { "modal" => "01", "modal_rodoviario" => { "rntrc" => "12345678" } },
70
+ validar: true)
71
+
72
+ expect(a_request(:post, "#{homologacao}/v2/cte?ref=cte-1")).to have_been_made
73
+ end
74
+ end
75
+
76
+ context "quando o CTe OS tem modal condicional" do
77
+ def stub_cte_os_base_vazio
78
+ vazio = FocusNfe::Esquemas::Esquema.new([])
79
+ allow(FocusNfe::Esquemas::Esquema).to receive(:carregar).and_call_original
80
+ allow(FocusNfe::Esquemas::Esquema).to receive(:carregar).with("cte_os").and_return(vazio)
81
+ end
82
+
83
+ it "valida o sub-esquema do modal e bloqueia a emissão" do
84
+ stub_cte_os_base_vazio
85
+
86
+ expect do
87
+ cte_os.emitir(ref: "cte-os-1", dados: { "modal" => "01", "modal_rodoviario" => { "placa" => "ABC1234" } },
88
+ validar: true)
89
+ end.to raise_error(FocusNfe::Esquemas::ErroDeValidacao, /modal_rodoviario\.placa/)
90
+ end
91
+
92
+ it "emite quando base e modal são válidos" do
93
+ stub_cte_os_base_vazio
94
+ stub_emissao("cte_os?ref=cte-os-1")
95
+
96
+ cte_os.emitir(ref: "cte-os-1", dados: { "modal" => "01", "modal_rodoviario" => { "placa" => "AB12" } },
97
+ validar: true)
98
+
99
+ expect(a_request(:post, "#{homologacao}/v2/cte_os?ref=cte-os-1")).to have_been_made
100
+ end
101
+
102
+ it "não valida modal quando o campo modal não é 01" do
103
+ stub_cte_os_base_vazio
104
+ stub_emissao("cte_os?ref=cte-os-1")
105
+
106
+ cte_os.emitir(ref: "cte-os-1", dados: { "modal" => "02", "modal_rodoviario" => { "placa" => "ABC1234" } },
107
+ validar: true)
108
+
109
+ expect(a_request(:post, "#{homologacao}/v2/cte_os?ref=cte-os-1")).to have_been_made
110
+ end
111
+ end
112
+
113
+ context "quando a MDFe deduz o modal pela chave presente" do
114
+ def stub_mdfe_base_vazia
115
+ vazio = FocusNfe::Esquemas::Esquema.new([])
116
+ allow(FocusNfe::Esquemas::Esquema).to receive(:carregar).and_call_original
117
+ allow(FocusNfe::Esquemas::Esquema).to receive(:carregar).with("mdfe").and_return(vazio)
118
+ end
119
+
120
+ it "valida o sub-esquema do modal presente e bloqueia a emissão" do
121
+ stub_mdfe_base_vazia
122
+
123
+ expect do
124
+ mdfe.emitir(ref: "mdfe-1", dados: { "modal_rodoviario" => {} }, validar: true)
125
+ end.to raise_error(FocusNfe::Esquemas::ErroDeValidacao, /modal_rodoviario/)
126
+ end
127
+
128
+ it "emite quando nenhuma chave de modal está presente" do
129
+ stub_mdfe_base_vazia
130
+ stub_emissao("mdfe?ref=mdfe-1")
131
+
132
+ mdfe.emitir(ref: "mdfe-1", dados: { "cnpj_emitente" => "12345678000123" }, validar: true)
133
+
134
+ expect(a_request(:post, "#{homologacao}/v2/mdfe?ref=mdfe-1")).to have_been_made
135
+ end
136
+ end
137
+
138
+ context "quando o documento não tem schema (pula silenciosamente)" do
139
+ it "emite sem validar" do
140
+ stub_emissao("nfce?ref=venda-1")
141
+
142
+ nfce.emitir(ref: "venda-1", dados: {}, validar: true)
143
+
144
+ expect(a_request(:post, "#{homologacao}/v2/nfce?ref=venda-1")).to have_been_made
145
+ end
146
+ end
147
+ end
148
+
149
+ describe "#emitir sem validar (padrão)" do
150
+ it "não valida mesmo com dados vazios" do
151
+ stub_emissao("nfe?ref=pedido-1")
152
+
153
+ nfe.emitir(ref: "pedido-1", dados: {})
154
+
155
+ expect(a_request(:post, "#{homologacao}/v2/nfe?ref=pedido-1")).to have_been_made
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::Recursos::CteOs do
4
+ it_behaves_like "um recurso emitível", "cte_os"
5
+ it_behaves_like "um recurso consultável", "cte_os"
6
+ it_behaves_like "um recurso cancelável", "cte_os"
7
+ it_behaves_like "um recurso corrigível por campo", "cte_os"
8
+ it_behaves_like "um recurso notificável", "cte_os"
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::Recursos::Cte do
4
+ it_behaves_like "um recurso emitível", "cte"
5
+ it_behaves_like "um recurso consultável", "cte"
6
+ it_behaves_like "um recurso cancelável", "cte"
7
+ it_behaves_like "um recurso corrigível por campo", "cte"
8
+ it_behaves_like "um recurso notificável", "cte"
9
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::Recursos::CtesRecebidas do
4
+ include_context "com recurso conectado"
5
+
6
+ let(:chave) { "35200114200166000187570010000000201234567890" }
7
+
8
+ it_behaves_like "um recurso listável", "ctes_recebidas"
9
+ it_behaves_like "um recurso baixável", "ctes_recebidas"
10
+ it_behaves_like "um recurso notificável", "ctes_recebidas"
11
+
12
+ describe "#consultar" do
13
+ it "faz GET em /v2/ctes_recebidas/{chave} e devolve o corpo cru" do
14
+ stub_get("ctes_recebidas/#{chave}", body: '{"situacao":"autorizado"}')
15
+
16
+ expect(recurso.consultar(chave)).to eq("situacao" => "autorizado")
17
+ end
18
+ end
19
+
20
+ describe "#desacordo" do
21
+ it "faz POST em /{chave}/desacordo com as observações" do
22
+ corpo = '{"observacoes":"Mercadoria não recebida conforme."}'
23
+ stub = stub_envio(:post, "ctes_recebidas/#{chave}/desacordo", body: corpo)
24
+
25
+ recurso.desacordo(chave, observacoes: "Mercadoria não recebida conforme.")
26
+
27
+ expect(stub).to have_been_requested
28
+ end
29
+ end
30
+
31
+ describe "#consultar_desacordo" do
32
+ it "faz GET em /{chave}/desacordo e devolve o corpo cru" do
33
+ stub_get("ctes_recebidas/#{chave}/desacordo", body: '{"status":"registrado"}')
34
+
35
+ expect(recurso.consultar_desacordo(chave)).to eq("status" => "registrado")
36
+ end
37
+ end
38
+
39
+ describe "#download_xml_cancelamento" do
40
+ it "faz GET em /{chave}/cancelamento.xml e devolve o XML cru" do
41
+ stub_get("ctes_recebidas/#{chave}/cancelamento.xml",
42
+ body: "<procEventoCTe/>", headers: { "Content-Type" => "application/xml" })
43
+
44
+ expect(recurso.download_xml_cancelamento(chave)).to eq("<procEventoCTe/>")
45
+ end
46
+ end
47
+
48
+ describe "#download_xml_carta_correcao" do
49
+ it "faz GET em /{chave}/carta_correcao.xml e devolve o XML cru" do
50
+ stub_get("ctes_recebidas/#{chave}/carta_correcao.xml",
51
+ body: "<procEventoCTe/>", headers: { "Content-Type" => "application/xml" })
52
+
53
+ expect(recurso.download_xml_carta_correcao(chave)).to eq("<procEventoCTe/>")
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::Recursos::Dce do
4
+ it_behaves_like "um recurso emitível", "dce"
5
+ it_behaves_like "um recurso consultável", "dce"
6
+ it_behaves_like "um recurso cancelável", "dce"
7
+ it_behaves_like "um recurso notificável", "dce"
8
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::Recursos::EmailsBloqueados do
4
+ include_context "com recurso conectado"
5
+
6
+ let(:email) { "bloqueado@exemplo.com" }
7
+
8
+ it "usa o caminho_base 'blocked_emails'" do
9
+ expect(recurso.caminho_base).to eq("blocked_emails")
10
+ end
11
+
12
+ describe "#consultar" do
13
+ it "faz GET em /v2/blocked_emails/{email} e devolve o corpo cru" do
14
+ stub_get("blocked_emails/#{email}", body: '{"block_type":"bounce"}')
15
+
16
+ expect(recurso.consultar(email)).to eq("block_type" => "bounce")
17
+ end
18
+ end
19
+
20
+ describe "#desbloquear" do
21
+ it "faz DELETE em /v2/blocked_emails/{email}" do
22
+ stub = stub_envio(:delete, "blocked_emails/#{email}")
23
+
24
+ recurso.desbloquear(email)
25
+
26
+ expect(stub).to have_been_requested
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe FocusNfe::Recursos::Empresas do
4
+ include_context "com recurso conectado"
5
+
6
+ let(:dados) { { "nome" => "Loja", "cnpj" => "12345678000123" } }
7
+
8
+ it_behaves_like "um recurso listável", "empresas"
9
+ it_behaves_like "um recurso localizável", "empresas"
10
+ it_behaves_like "um recurso removível", "empresas"
11
+
12
+ describe "#criar" do
13
+ it "faz POST em /v2/empresas com os dados e devolve o corpo cru" do
14
+ stub_envio(:post, "empresas", body: JSON.generate(dados), resposta: '{"id":1}')
15
+
16
+ expect(recurso.criar(dados: dados)).to eq("id" => 1)
17
+ end
18
+
19
+ it "inclui dry_run=1 na query quando dry_run: true" do
20
+ stub = stub_envio(:post, "empresas", query: { dry_run: 1 }, body: JSON.generate(dados))
21
+
22
+ recurso.criar(dados: dados, dry_run: true)
23
+
24
+ expect(stub).to have_been_requested
25
+ end
26
+
27
+ it "não envia dry_run por padrão" do
28
+ stub = stub_envio(:post, "empresas", body: JSON.generate(dados))
29
+
30
+ recurso.criar(dados: dados)
31
+
32
+ expect(stub).to have_been_requested
33
+ end
34
+ end
35
+
36
+ describe "#atualizar" do
37
+ it "faz PUT em /v2/empresas/{id} com os dados" do
38
+ stub = stub_envio(:put, "empresas/7", body: JSON.generate(dados))
39
+
40
+ recurso.atualizar("7", dados: dados)
41
+
42
+ expect(stub).to have_been_requested
43
+ end
44
+ end
45
+ end