nfcom 0.1.2

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 (51) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +115 -0
  3. data/.ruby-version +1 -0
  4. data/CHANGELOG.md +66 -0
  5. data/LICENSE +21 -0
  6. data/README.md +280 -0
  7. data/Rakefile +22 -0
  8. data/examples/.env.example +41 -0
  9. data/examples/emitir_nota.rb +91 -0
  10. data/examples/rails_initializer.rb +72 -0
  11. data/lib/nfcom/builder/danfe_com.rb +564 -0
  12. data/lib/nfcom/builder/qrcode.rb +68 -0
  13. data/lib/nfcom/builder/signature.rb +156 -0
  14. data/lib/nfcom/builder/xml_builder.rb +362 -0
  15. data/lib/nfcom/client.rb +106 -0
  16. data/lib/nfcom/configuration.rb +134 -0
  17. data/lib/nfcom/errors.rb +27 -0
  18. data/lib/nfcom/helpers/consulta.rb +28 -0
  19. data/lib/nfcom/models/assinante.rb +146 -0
  20. data/lib/nfcom/models/destinatario.rb +138 -0
  21. data/lib/nfcom/models/emitente.rb +105 -0
  22. data/lib/nfcom/models/endereco.rb +123 -0
  23. data/lib/nfcom/models/fatura/codigo_de_barras/formato_44.rb +52 -0
  24. data/lib/nfcom/models/fatura/codigo_de_barras.rb +57 -0
  25. data/lib/nfcom/models/fatura.rb +172 -0
  26. data/lib/nfcom/models/item.rb +353 -0
  27. data/lib/nfcom/models/nota.rb +398 -0
  28. data/lib/nfcom/models/total.rb +60 -0
  29. data/lib/nfcom/parsers/autorizacao.rb +28 -0
  30. data/lib/nfcom/parsers/base.rb +30 -0
  31. data/lib/nfcom/parsers/consulta.rb +34 -0
  32. data/lib/nfcom/parsers/inutilizacao.rb +23 -0
  33. data/lib/nfcom/parsers/status.rb +23 -0
  34. data/lib/nfcom/utils/certificate.rb +109 -0
  35. data/lib/nfcom/utils/compressor.rb +47 -0
  36. data/lib/nfcom/utils/helpers.rb +141 -0
  37. data/lib/nfcom/utils/response_decompressor.rb +47 -0
  38. data/lib/nfcom/utils/xml_authorized.rb +29 -0
  39. data/lib/nfcom/utils/xml_cleaner.rb +68 -0
  40. data/lib/nfcom/validators/business_rules.rb +45 -0
  41. data/lib/nfcom/validators/schema_validator.rb +316 -0
  42. data/lib/nfcom/validators/xml_validator.rb +29 -0
  43. data/lib/nfcom/version.rb +5 -0
  44. data/lib/nfcom/webservices/autorizacao.rb +36 -0
  45. data/lib/nfcom/webservices/base.rb +96 -0
  46. data/lib/nfcom/webservices/consulta.rb +59 -0
  47. data/lib/nfcom/webservices/inutilizacao.rb +71 -0
  48. data/lib/nfcom/webservices/status.rb +64 -0
  49. data/lib/nfcom.rb +98 -0
  50. data/nfcom.gemspec +42 -0
  51. metadata +242 -0
@@ -0,0 +1,398 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Nfcom
6
+ module Models
7
+ # Representa uma Nota Fiscal de Comunicação (NF-COM) modelo 62.
8
+ #
9
+ # A Nota é o objeto principal da gem, responsável por agregar todas as
10
+ # informações necessárias para a emissão da NF-COM:
11
+ # emitente, destinatário, fatura, itens/serviços, totais e metadados fiscais.
12
+ #
13
+ # @example Criar uma nota completa
14
+ # nota = Nfcom::Models::Nota.new do |n|
15
+ # n.serie = 1
16
+ # n.numero = 1
17
+ #
18
+ # # Emitente (provedor)
19
+ # n.emitente = Nfcom::Models::Emitente.new(
20
+ # cnpj: '12345678000100',
21
+ # razao_social: 'Provedor Internet LTDA',
22
+ # inscricao_estadual: '0123456789',
23
+ # endereco: { ... }
24
+ # )
25
+ #
26
+ # # Destinatário (cliente)
27
+ # n.destinatario = Nfcom::Models::Destinatario.new(
28
+ # cpf: '12345678901',
29
+ # razao_social: 'João da Silva',
30
+ # endereco: { ... }
31
+ # )
32
+ #
33
+ # # Fatura (obrigatória)
34
+ # n.fatura = Nfcom::Models::Fatura.new(
35
+ # valor_liquido: 99.90,
36
+ # data_vencimento: Date.today + 10
37
+ # )
38
+ #
39
+ # # Adicionar serviços
40
+ # n.add_item(
41
+ # codigo_servico: '0303',
42
+ # descricao: 'Plano Fibra 100MB',
43
+ # classe_consumo: '0303',
44
+ # cfop: '5307',
45
+ # valor_unitario: 99.90
46
+ # )
47
+ # end
48
+ #
49
+ # @example Validar nota antes de emitir
50
+ # if nota.valida?
51
+ # puts 'Nota válida, pronta para emissão'
52
+ # else
53
+ # puts 'Erros encontrados:'
54
+ # nota.erros.each { |erro| puts " - #{erro}" }
55
+ # end
56
+ #
57
+ # @example Emitir nota
58
+ # # A chave de acesso é gerada automaticamente
59
+ # nota.gerar_chave_acesso
60
+ #
61
+ # # Enviar para a SEFAZ
62
+ # client = Nfcom::Client.new
63
+ # resultado = client.autorizar(nota)
64
+ #
65
+ # if resultado[:autorizada]
66
+ # puts 'Nota autorizada!'
67
+ # puts "Chave: #{nota.chave_acesso}"
68
+ # puts "Protocolo: #{nota.protocolo}"
69
+ # end
70
+ #
71
+ # @example Adicionar múltiplos serviços
72
+ # nota.add_item(codigo_servico: '0303', descricao: 'Internet', valor_unitario: 99.90)
73
+ # nota.add_item(codigo_servico: '0304', descricao: 'TV', valor_unitario: 79.90)
74
+ # # O total é recalculado automaticamente
75
+ #
76
+ # @example Nota com informações adicionais
77
+ # nota.informacoes_adicionais = 'Cliente isento de ICMS conforme decreto XYZ'
78
+ #
79
+ # Tipos de Emissão:
80
+ # - :normal (1) - Emissão normal (padrão)
81
+ # - :contingencia (2) - Emissão em contingência (offline)
82
+ #
83
+ # Finalidades:
84
+ # - :normal (0) - Nota fiscal normal (padrão)
85
+ # - :substituicao (3) - Nota de substituição
86
+ # - :ajuste (4) - Nota de ajuste
87
+ #
88
+ # Tipos de Faturamento:
89
+ # - :normal (0) - Faturamento padrão
90
+ # - :centralizado (1) - Faturamento centralizado
91
+ # - :cofaturamento (2) - Cofaturamento
92
+ #
93
+ # Atributos obrigatórios:
94
+ # - serie - Série da nota (padrão: 1)
95
+ # - numero - Número sequencial da nota
96
+ # - emitente - Provedor / empresa emissora
97
+ # - destinatario - Cliente / tomador do serviço
98
+ # - fatura - Informações de cobrança (obrigatória)
99
+ # - itens - Pelo menos um item/serviço
100
+ #
101
+ # Atributos opcionais:
102
+ # - data_emissao - Data/hora de emissão (padrão: Time.now)
103
+ # - tipo_emissao - Tipo de emissão (padrão: :normal)
104
+ # - finalidade - Finalidade da nota (padrão: :normal)
105
+ # - informacoes_adicionais - Texto livre (até 5.000 caracteres)
106
+ #
107
+ # Atributos preenchidos após autorização:
108
+ # - chave_acesso - Chave de acesso (44 dígitos)
109
+ # - codigo_verificacao - Código numérico (cNF, 7 dígitos – usado no XML)
110
+ # - protocolo - Número do protocolo SEFAZ
111
+ # - data_autorizacao - Data/hora da autorização
112
+ # - xml_autorizado - XML completo autorizado pela SEFAZ
113
+ #
114
+ # Funcionalidades automáticas:
115
+ # - Numeração sequencial dos itens
116
+ # - Recalculo automático dos totais
117
+ # - Geração da chave de acesso com dígito verificador
118
+ # - Validação completa de todos os campos obrigatórios
119
+ # - Validação em cascata (emitente, destinatário, fatura e itens)
120
+ #
121
+ # Validações realizadas:
122
+ # - Presença de série, número, emitente, destinatário e fatura
123
+ # - Existência de pelo menos um item
124
+ # - Validação completa do emitente (CNPJ, IE, endereço)
125
+ # - Validação completa do destinatário (CPF/CNPJ, endereço)
126
+ # - Validação da fatura
127
+ # - Validação individual de cada item
128
+ #
129
+ # @note Formato da chave de acesso (44 dígitos):
130
+ # UF (2) + AAMM (4) + CNPJ (14) + Modelo (2) + Série (3) +
131
+ # Número (9) + Tipo Emissão (1) + Código Numérico (8) + DV (1)
132
+ #
133
+ # @note Importante:
134
+ # - O campo cNF no XML possui 7 dígitos
135
+ # - Na chave de acesso, o código numérico é representado com 8 dígitos
136
+ class Nota # rubocop:disable Metrics/ClassLength
137
+ attr_accessor :serie, :numero, :data_emissao, :tipo_emissao, :fatura,
138
+ :finalidade, :emitente, :destinatario, :assinante, :itens, :total,
139
+ :chave_acesso, :codigo_verificacao,
140
+ :protocolo, :data_autorizacao, :xml_autorizado,
141
+ :competencia_fatura, :data_vencimento, :valor_liquido_fatura,
142
+ :chave_nfcom_substituida, :motivo_substituicao
143
+
144
+ attr_reader :metodo_pagamento, :tipo_faturamento, :informacoes_adicionais
145
+
146
+ MAX_INF_CPL = 5
147
+
148
+ def metodo_pagamento=(value)
149
+ if value.is_a?(Symbol)
150
+ unless METODOS_PAGAMENTO.key?(value)
151
+ raise Errors::ValidationError,
152
+ "Método de pagamento inválido: #{value.inspect}. " \
153
+ "Valores válidos: #{METODOS_PAGAMENTO.keys.join(', ')}"
154
+ end
155
+ @metodo_pagamento = METODOS_PAGAMENTO[value]
156
+ else
157
+ value_str = value.to_s
158
+ unless METODOS_PAGAMENTO.values.include?(value_str)
159
+ raise Errors::ValidationError,
160
+ "Método de pagamento inválido: #{value.inspect}. " \
161
+ "Valores válidos: #{METODOS_PAGAMENTO.values.join(', ')}"
162
+ end
163
+ @metodo_pagamento = value_str
164
+ end
165
+ end
166
+
167
+ def tipo_faturamento=(value)
168
+ if value.is_a?(Symbol)
169
+ unless TIPO_FATURAMENTO.key?(value)
170
+ raise Errors::ValidationError,
171
+ "Tipo de faturamento inválido: #{value.inspect}. " \
172
+ "Valores válidos: #{TIPO_FATURAMENTO.keys.join(', ')}"
173
+ end
174
+ @tipo_faturamento = TIPO_FATURAMENTO[value]
175
+ elsif value.is_a?(Integer) || value.is_a?(String)
176
+ value_int = value.to_i
177
+ unless TIPO_FATURAMENTO.values.include?(value_int)
178
+ raise Errors::ValidationError,
179
+ "Tipo de faturamento inválido: #{value.inspect}. " \
180
+ "Valores válidos: #{TIPO_FATURAMENTO.values.join(', ')}"
181
+ end
182
+ @tipo_faturamento = value_int
183
+ else
184
+ raise Errors::ValidationError,
185
+ 'Tipo de faturamento deve ser Symbol, Integer ou String'
186
+ end
187
+ end
188
+
189
+ METODOS_PAGAMENTO = {
190
+ dinheiro: '01',
191
+ cheque: '02',
192
+ cartao_credito: '03',
193
+ cartao_debito: '04',
194
+ credito_loja: '05',
195
+ vale_alimentacao: '10',
196
+ vale_refeicao: '11',
197
+ vale_presente: '12',
198
+ vale_combustivel: '13',
199
+ boleto_bancario: '15',
200
+ deposito_bancario: '16',
201
+ pix: '17',
202
+ transferencia_bancaria: '18',
203
+ programa_fidelidade: '19',
204
+ sem_pagamento: '90',
205
+ outros: '99'
206
+ }.freeze
207
+
208
+ TIPO_EMISSAO = {
209
+ normal: 1,
210
+ contingencia: 2
211
+ }.freeze
212
+
213
+ FINALIDADE = {
214
+ normal: 0,
215
+ substituicao: 3,
216
+ ajuste: 4
217
+ }.freeze
218
+
219
+ TIPO_FATURAMENTO = {
220
+ normal: 0,
221
+ centralizado: 1,
222
+ cofaturamento: 2
223
+ }.freeze
224
+
225
+ def initialize(attributes = {})
226
+ @serie = Nfcom.configuration.serie_padrao || 1
227
+ @data_emissao = Time.now
228
+ @tipo_emissao = :normal
229
+ send(:tipo_faturamento=, :normal)
230
+ @finalidade = :normal
231
+ @itens = []
232
+ @total = Total.new
233
+
234
+ attributes.each do |key, value|
235
+ if key == :emitente && value.is_a?(Hash)
236
+ @emitente = Emitente.new(value)
237
+ elsif key == :fatura && value.is_a?(Hash)
238
+ @fatura = Fatura.new(value)
239
+ elsif key == :destinatario && value.is_a?(Hash)
240
+ @destinatario = Destinatario.new(value)
241
+ elsif respond_to?("#{key}=")
242
+ send("#{key}=", value)
243
+ end
244
+ end
245
+
246
+ yield self if block_given?
247
+ end
248
+
249
+ def add_item(attributes)
250
+ item = if attributes.is_a?(Item)
251
+ attributes
252
+ else
253
+ Item.new(attributes)
254
+ end
255
+
256
+ item.numero_item = itens.length + 1
257
+ itens << item
258
+ recalcular_totais
259
+ item
260
+ end
261
+
262
+ def recalcular_totais
263
+ @total.valor_servicos = itens.sum { |i| i.valor_total.to_f }
264
+ @total.valor_desconto = itens.sum { |i| i.valor_desconto.to_f }
265
+ @total.valor_outras_despesas = itens.sum { |i| i.valor_outras_despesas.to_f }
266
+ @total.calcular_total
267
+ end
268
+
269
+ def gerar_chave_acesso
270
+ # IMPORTANTE: Discrepância no schema NFCom:
271
+ # - Campo cNF no XML: 7 dígitos (ER2)
272
+ # - cNF na chave de acesso: 8 dígitos (para chave ter 44 dígitos no total)
273
+ # Formato da chave: UFAnoMesCNPJModSerieNumTpEmissCodNumDV
274
+ # UF(2) + AAMM(4) + CNPJ(14) + Mod(2) + Serie(3) + Num(9) + TpEmiss(1) + CodNum(8) + DV(1) = 44
275
+ # Exemplo: 26 2601 07159053000107 62 001 000009670 1 01234567 9
276
+
277
+ config = Nfcom.configuration
278
+ uf = config.codigo_uf
279
+ ano_mes = data_emissao.strftime('%y%m')
280
+ cnpj = emitente.cnpj.gsub(/\D/, '')
281
+ modelo = '62'
282
+ serie_fmt = serie.to_s.rjust(3, '0')
283
+ numero_fmt = numero.to_s.rjust(9, '0')
284
+ tipo_emiss = tipo_emissao_codigo.to_s
285
+
286
+ # Gera cNF com 7 dígitos (para o campo XML)
287
+ codigo_numerico_7 = SecureRandom.random_number(10_000_000).to_s.rjust(7, '0')
288
+
289
+ # Mas na chave usa 8 dígitos (padding à esquerda com zero)
290
+ codigo_numerico_8 = codigo_numerico_7.rjust(8, '0')
291
+
292
+ chave_sem_dv = "#{uf}#{ano_mes}#{cnpj}#{modelo}#{serie_fmt}#{numero_fmt}#{tipo_emiss}#{codigo_numerico_8}"
293
+ dv = calcular_digito_verificador(chave_sem_dv)
294
+
295
+ # Armazena o cNF de 7 dígitos (para o XML)
296
+ @codigo_verificacao = codigo_numerico_7
297
+ # Chave completa com 44 dígitos
298
+ @chave_acesso = "#{chave_sem_dv}#{dv}"
299
+ end
300
+
301
+ def tipo_emissao_codigo
302
+ TIPO_EMISSAO[tipo_emissao] || TIPO_EMISSAO[:normal]
303
+ end
304
+
305
+ def finalidade_codigo
306
+ FINALIDADE[finalidade] || FINALIDADE[:normal]
307
+ end
308
+
309
+ def valida?
310
+ erros.empty?
311
+ end
312
+
313
+ def erros # rubocop:disable Metrics/MethodLength
314
+ errors = []
315
+ errors << 'Série é obrigatória' if serie.nil?
316
+ errors << 'Número é obrigatório' if numero.nil?
317
+ errors << 'Emitente é obrigatório' if emitente.nil?
318
+ errors << 'Destinatário é obrigatório' if destinatario.nil?
319
+ errors << 'Deve haver pelo menos um item' if itens.empty?
320
+
321
+ # Validações de schema (formato)
322
+ errors << 'Série inválida (deve ser 0-999)' if serie && !serie.to_s.match?(/\A(0|[1-9]{1}[0-9]{0,2})\z/)
323
+
324
+ if numero && !numero.to_s.match?(/\A[1-9]{1}[0-9]{0,8}\z/)
325
+ errors << 'Número inválido (1-999999999, não pode começar com zero)'
326
+ end
327
+
328
+ if codigo_verificacao && codigo_verificacao.to_s.length != 7
329
+ errors << "cNF inválido: deve ter exatamente 7 dígitos (campo XML), tem #{codigo_verificacao.to_s.length}"
330
+ end
331
+
332
+ if chave_acesso && !chave_acesso.to_s.match?(/\A[0-9]{44}\z/)
333
+ errors << "Chave de acesso inválida (deve ter 44 dígitos, tem #{chave_acesso.to_s.length})"
334
+ end
335
+
336
+ errors.concat(emitente.erros.map { |e| "Emitente: #{e}" }) if emitente && !emitente.valido?
337
+ errors.concat(destinatario.erros.map { |e| "Destinatário: #{e}" }) if destinatario && !destinatario.valido?
338
+
339
+ itens.each_with_index do |item, i|
340
+ errors.concat(item.erros.map { |e| "Item #{i + 1}: #{e}" }) unless item.valido?
341
+ end
342
+
343
+ if fatura.nil?
344
+ errors << 'Fatura é obrigatória'
345
+ elsif !fatura.valido?
346
+ errors.concat(fatura.erros.map { |e| "Fatura: #{e}" })
347
+ end
348
+
349
+ informacoes_adicionais&.each_with_index do |texto, i|
350
+ errors << "Informação adicional #{i + 1} inválida." unless Validators::SchemaValidator.texto_valido?(texto)
351
+ end
352
+
353
+ if finalidade == :substituicao
354
+ if chave_nfcom_substituida.nil?
355
+ errors << 'Chave da NFCom substituída é obrigatória para notas de substituição'
356
+ end
357
+ errors << 'Motivo da substituição é obrigatório' if motivo_substituicao.nil?
358
+ unless chave_nfcom_substituida.nil? || chave_nfcom_substituida.to_s.match?(/\A[0-9]{44}\z/)
359
+ errors << 'Chave da NFCom substituída inválida (deve ter 44 dígitos)'
360
+ end
361
+ end
362
+
363
+ errors
364
+ end
365
+
366
+ def autorizada?
367
+ !protocolo.nil? && !data_autorizacao.nil?
368
+ end
369
+
370
+ def informacoes_adicionais=(value)
371
+ @informacoes_adicionais =
372
+ value
373
+ .to_s
374
+ .split(/\r?\n/)
375
+ .map(&:strip)
376
+ .reject(&:empty?)
377
+ .first(MAX_INF_CPL)
378
+ end
379
+
380
+ private
381
+
382
+ def calcular_digito_verificador(chave)
383
+ # Módulo 11
384
+ multiplicadores = [2, 3, 4, 5, 6, 7, 8, 9]
385
+ soma = 0
386
+
387
+ chave.reverse.chars.each_with_index do |digito, i|
388
+ soma += digito.to_i * multiplicadores[i % 8]
389
+ end
390
+
391
+ resto = soma % 11
392
+ dv = 11 - resto
393
+ dv = 0 if dv >= 10
394
+ dv
395
+ end
396
+ end
397
+ end
398
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nfcom
4
+ module Models
5
+ # Representa os valores totalizadores da NF-COM
6
+ #
7
+ # Esta classe agrega todos os valores da nota fiscal, incluindo valores
8
+ # de serviços, descontos, impostos e o valor total da nota.
9
+ #
10
+ # Responsabilidades:
11
+ # - Armazenar os valores totalizadores da nota
12
+ # - Calcular o valor total da NF-COM
13
+ # - Manter valores de impostos (preenchidos externamente ou derivados)
14
+ #
15
+ # Observações importantes:
16
+ # - Esta classe NÃO calcula impostos por regra fiscal
17
+ # - ICMS, PIS e COFINS devem ser informados pela camada de tributação
18
+ # - O Total apenas consolida os valores
19
+ #
20
+ class Total
21
+ attr_accessor :valor_servicos, :valor_desconto, :valor_outras_despesas,
22
+ :valor_total, :icms_base_calculo, :icms_valor,
23
+ :icms_desonerado, :fcp_valor,
24
+ :pis_valor, :cofins_valor,
25
+ :funttel_valor, :fust_valor,
26
+ :pis_retido, :cofins_retido, :csll_retido, :irrf_retido
27
+
28
+ def initialize(attributes = {})
29
+ @valor_desconto = 0.0
30
+ @valor_outras_despesas = 0.0
31
+ @icms_base_calculo = 0.0
32
+ @icms_valor = 0.0
33
+ @icms_desonerado = 0.0
34
+ @fcp_valor = 0.0
35
+ @pis_valor = 0.0
36
+ @cofins_valor = 0.0
37
+ @funttel_valor = 0.0
38
+ @fust_valor = 0.0
39
+ @pis_retido = 0.0
40
+ @cofins_retido = 0.0
41
+ @csll_retido = 0.0
42
+ @irrf_retido = 0.0
43
+
44
+ attributes.each do |key, value|
45
+ send("#{key}=", value) if respond_to?("#{key}=")
46
+ end
47
+ end
48
+
49
+ def calcular_total
50
+ @valor_total = valor_servicos.to_f -
51
+ valor_desconto.to_f +
52
+ valor_outras_despesas.to_f
53
+ end
54
+
55
+ def valor_liquido
56
+ valor_total.to_f
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nfcom
4
+ module Parsers
5
+ class Autorizacao < Base
6
+ def parse
7
+ ret = find('//nfcom:retNFCom')
8
+ raise Errors::NotaRejeitada.new('000', 'Resposta inválida') unless ret
9
+
10
+ c_stat = xpath(ret, './/nfcom:cStat')
11
+ x_motivo = xpath(ret, './/nfcom:xMotivo')
12
+
13
+ raise Errors::NotaRejeitada.new(c_stat, x_motivo) unless c_stat == '100'
14
+
15
+ prot = ret.at_xpath('.//nfcom:protNFCom', nfcom_namespaces)
16
+
17
+ {
18
+ autorizada: true,
19
+ protocolo: xpath(prot, './/nfcom:nProt'),
20
+ chave: xpath(prot, './/nfcom:chNFCom'),
21
+ data_autorizacao: xpath(prot, './/nfcom:dhRecbto'),
22
+ xml: prot&.to_xml,
23
+ mensagem: x_motivo
24
+ }
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nfcom
4
+ module Parsers
5
+ class Base
6
+ attr_reader :document
7
+
8
+ def initialize(http_response)
9
+ @document = Nokogiri::XML(http_response)
10
+ end
11
+
12
+ private
13
+
14
+ def nfcom_namespaces
15
+ {
16
+ 'soap' => 'http://www.w3.org/2003/05/soap-envelope',
17
+ 'nfcom' => 'http://www.portalfiscal.inf.br/nfcom'
18
+ }
19
+ end
20
+
21
+ def xpath(node, path)
22
+ node.at_xpath(path, nfcom_namespaces)&.text
23
+ end
24
+
25
+ def find(path)
26
+ document.at_xpath(path, nfcom_namespaces)
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nfcom
4
+ module Parsers
5
+ class Consulta < Base
6
+ def parse
7
+ ret = find('//nfcom:retConsSitNFCom')
8
+ raise Errors::SefazError, 'Resposta de consulta inválida' unless ret
9
+
10
+ codigo = xpath(ret, './/nfcom:cStat')
11
+ motivo = xpath(ret, './/nfcom:xMotivo')
12
+
13
+ {
14
+ codigo: codigo,
15
+ motivo: motivo,
16
+ situacao: interpretar_situacao(codigo),
17
+ protocolo: xpath(ret, './/nfcom:protNFCom/nfcom:infProt/nfcom:nProt'),
18
+ data_autorizacao: xpath(ret, './/nfcom:protNFCom/nfcom:infProt/nfcom:dhRecbto')
19
+ }
20
+ end
21
+
22
+ private
23
+
24
+ def interpretar_situacao(codigo)
25
+ case codigo.to_s
26
+ when '100', '150' then 'Autorizada'
27
+ when '110', '301', '302' then 'Denegada'
28
+ when '101', '151', '155' then 'Cancelada'
29
+ else 'Desconhecida'
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nfcom
4
+ module Parsers
5
+ class Inutilizacao < Base
6
+ def parse
7
+ ret = find('//nfcom:retInutNFCom')
8
+ raise Errors::SefazError, 'Resposta de inutilização inválida' unless ret
9
+
10
+ inf = ret.at_xpath('.//nfcom:infInut', nfcom_namespaces)
11
+ codigo = xpath(inf, './/nfcom:cStat')
12
+ motivo = xpath(inf, './/nfcom:xMotivo')
13
+
14
+ {
15
+ inutilizada: codigo.to_s == '102',
16
+ codigo: codigo,
17
+ motivo: motivo,
18
+ protocolo: xpath(inf, './/nfcom:nProt')
19
+ }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nfcom
4
+ module Parsers
5
+ class Status < Base
6
+ def parse
7
+ ret = find('//nfcom:retConsStatServNFCom')
8
+ raise Errors::SefazError, 'Resposta de status inválida' unless ret
9
+
10
+ codigo = xpath(ret, './/nfcom:cStat')
11
+ motivo = xpath(ret, './/nfcom:xMotivo')
12
+
13
+ {
14
+ online: codigo.to_s == '107',
15
+ codigo: codigo,
16
+ motivo: motivo,
17
+ tempo_medio: xpath(ret, './/nfcom:tMed'),
18
+ data_hora: xpath(ret, './/nfcom:dhRecbto')
19
+ }
20
+ end
21
+ end
22
+ end
23
+ end