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,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nfcom
4
+ module Models
5
+ # Representa o endereço do emitente ou destinatário da NF-COM
6
+ #
7
+ # Esta classe é utilizada tanto pelo emitente quanto pelo destinatário
8
+ # para armazenar as informações completas de endereço.
9
+ #
10
+ # @example Criar endereço completo
11
+ # endereco = Nfcom::Models::Endereco.new(
12
+ # logradouro: 'Rua das Flores',
13
+ # numero: '123',
14
+ # complemento: 'Sala 101',
15
+ # bairro: 'Centro',
16
+ # municipio: 'Recife',
17
+ # codigo_municipio: '2611606',
18
+ # uf: 'PE',
19
+ # cep: '50000-000',
20
+ # telefone: '(81) 3333-4444'
21
+ # )
22
+ #
23
+ # @example Validar endereço
24
+ # if endereco.valido?
25
+ # puts "Endereço válido"
26
+ # else
27
+ # puts "Erros: #{endereco.erros.join(', ')}"
28
+ # end
29
+ #
30
+ # Atributos obrigatórios:
31
+ # - logradouro (rua, avenida, etc) - 2-60 caracteres (ER47)
32
+ # - numero (número do imóvel) - 1-60 caracteres (ER47)
33
+ # - bairro (bairro/distrito) - 2-60 caracteres (ER47)
34
+ # - municipio (nome do município) - 2-60 caracteres (ER47)
35
+ # - codigo_municipio (código IBGE do município - 7 dígitos) (ER2)
36
+ # - uf (sigla do estado - 2 letras) (D5)
37
+ # - cep (8 dígitos) (ER67)
38
+ #
39
+ # Atributos opcionais:
40
+ # - complemento (apartamento, sala, bloco, etc) - 1-60 caracteres (ER47)
41
+ # - codigo_pais (padrão: 1058 para Brasil)
42
+ # - pais (padrão: 'Brasil')
43
+ # - telefone (7-12 dígitos) (ER61)
44
+ # - email (ER72)
45
+ #
46
+ # @note O código do município (IBGE) pode ser consultado em:
47
+ # https://www.ibge.gov.br/explica/codigos-dos-municipios.php
48
+ class Endereco
49
+ include Utils::Helpers
50
+
51
+ attr_accessor :logradouro, :numero, :complemento, :bairro,
52
+ :codigo_municipio, :municipio, :uf, :cep,
53
+ :codigo_pais, :pais, :telefone, :email
54
+
55
+ def initialize(attributes = {})
56
+ attributes.each do |key, value|
57
+ send("#{key}=", value) if respond_to?("#{key}=")
58
+ end
59
+ end
60
+
61
+ def valido?
62
+ erros.empty?
63
+ end
64
+
65
+ def erros # rubocop:disable Metrics/MethodLength
66
+ errors = []
67
+
68
+ # Validações de campos obrigatórios
69
+ errors << 'Logradouro é obrigatório' if logradouro.to_s.strip.empty?
70
+ errors << 'Número é obrigatório' if numero.to_s.strip.empty?
71
+ errors << 'Bairro é obrigatório' if bairro.to_s.strip.empty?
72
+ errors << 'Município é obrigatório' if municipio.to_s.strip.empty?
73
+ errors << 'Código do município é obrigatório' if codigo_municipio.to_s.strip.empty?
74
+ errors << 'UF é obrigatório' if uf.to_s.strip.empty?
75
+ errors << 'CEP é obrigatório' if cep.to_s.strip.empty?
76
+
77
+ # Validações declarativas de formato/schema
78
+ campos = {}
79
+
80
+ # Campos obrigatórios - validar formato apenas se não estiverem vazios
81
+ unless logradouro.to_s.strip.empty?
82
+ campos[:logradouro] = { valor: logradouro, validador: :er47, nome: 'Logradouro', max: 60 }
83
+ end
84
+
85
+ campos[:numero] = { valor: numero, validador: :er47, nome: 'Número', max: 60 } unless numero.to_s.strip.empty?
86
+
87
+ campos[:bairro] = { valor: bairro, validador: :er47, nome: 'Bairro', max: 60 } unless bairro.to_s.strip.empty?
88
+
89
+ unless municipio.to_s.strip.empty?
90
+ campos[:municipio] = { valor: municipio, validador: :er47, nome: 'Município', max: 60 }
91
+ end
92
+
93
+ unless codigo_municipio.to_s.strip.empty?
94
+ campos[:codigo_municipio] = { valor: codigo_municipio, validador: :er2, nome: 'Código do município' }
95
+ end
96
+
97
+ campos[:uf] = { valor: uf, validador: :d5, nome: 'UF' } unless uf.to_s.strip.empty?
98
+
99
+ unless cep.to_s.strip.empty?
100
+ cep_limpo = apenas_numeros(cep)
101
+ campos[:cep] = { valor: cep_limpo, validador: :er67, nome: 'CEP' }
102
+ end
103
+
104
+ # Campos opcionais - validar formato apenas se informados
105
+ if complemento && !complemento.to_s.strip.empty?
106
+ campos[:complemento] = { valor: complemento, validador: :er47, nome: 'Complemento', max: 60 }
107
+ end
108
+
109
+ if telefone && !telefone.to_s.strip.empty?
110
+ telefone_limpo = apenas_numeros(telefone)
111
+ campos[:telefone] = { valor: telefone_limpo, validador: :er61, nome: 'Telefone' }
112
+ end
113
+
114
+ campos[:email] = { valor: email, validador: :er72, nome: 'Email' } if email && !email.to_s.strip.empty?
115
+
116
+ # Executar validações declarativas
117
+ errors.concat(Validators::SchemaValidator.validar_campos(campos))
118
+
119
+ errors
120
+ end
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nfcom
4
+ module Models
5
+ class Fatura
6
+ class CodigoDeBarras
7
+ class Formato44
8
+ def initialize(valor)
9
+ @valor = valor
10
+ validar!
11
+ end
12
+
13
+ def linha_digitavel
14
+ [
15
+ campo(@valor[0, 4] + @valor[19, 5]),
16
+ campo(@valor[24, 10]),
17
+ campo(@valor[34, 10]),
18
+ @valor[4],
19
+ @valor[5, 14]
20
+ ].join(' ')
21
+ end
22
+
23
+ private
24
+
25
+ def campo(valor)
26
+ "#{valor[0, 5]}.#{valor[5, 5]}#{modulo10(valor)}"
27
+ end
28
+
29
+ def modulo10(numero)
30
+ soma = 0
31
+ multiplicador = 2
32
+
33
+ numero.reverse.each_char do |char|
34
+ v = char.to_i * multiplicador
35
+ v -= 9 if v > 9
36
+ soma += v
37
+ multiplicador = multiplicador == 2 ? 1 : 2
38
+ end
39
+
40
+ (10 - (soma % 10)) % 10
41
+ end
42
+
43
+ def validar!
44
+ return if @valor.match?(/\A\d{44}\z/)
45
+
46
+ raise ArgumentError, 'Código de barras (44) inválido'
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nfcom
4
+ module Models
5
+ class Fatura
6
+ class CodigoDeBarras
7
+ attr_reader :valor
8
+
9
+ def initialize(valor)
10
+ @valor = valor.to_s.gsub(/\D/, '')
11
+ validar!
12
+ end
13
+
14
+ def tamanho
15
+ valor.length
16
+ end
17
+
18
+ def formato
19
+ case tamanho
20
+ when 44 then :formato_44
21
+ when 48 then :formato_48
22
+ else :desconhecido
23
+ end
24
+ end
25
+
26
+ def linha_digitavel
27
+ handler.linha_digitavel
28
+ end
29
+
30
+ def valido?
31
+ true
32
+ rescue ArgumentError
33
+ false
34
+ end
35
+
36
+ private
37
+
38
+ def handler
39
+ @handler ||= case formato
40
+ when :formato_44
41
+ CodigoDeBarras::Formato44.new(valor)
42
+ when :formato_48
43
+ CodigoDeBarras::Formato48.new(valor)
44
+ else
45
+ raise ArgumentError, 'Formato de código de barras não suportado'
46
+ end
47
+ end
48
+
49
+ def validar!
50
+ return if valor.match?(/\A\d+\z/)
51
+
52
+ raise ArgumentError, 'Código de barras deve conter apenas números'
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nfcom
4
+ module Models
5
+ # Representa as informações de faturamento da NF-COM (grupo gFat)
6
+ #
7
+ # Este grupo contém as informações sobre o período de faturamento,
8
+ # vencimento e valores da fatura.
9
+ #
10
+ # @example Criar fatura básica para ISP
11
+ # fatura = Nfcom::Models::Fatura.new(
12
+ # competencia: '2026-01', # Aceita YYYY-MM ou AAAAMM
13
+ # data_vencimento: '2026-02-15', # Data de vencimento
14
+ # codigo_barras: '23793381286000000099901234567890123456789012',
15
+ # valor_fatura: 99.90
16
+ # )
17
+ #
18
+ # @example Fatura com período de uso
19
+ # fatura = Nfcom::Models::Fatura.new(
20
+ # competencia: '202601',
21
+ # data_vencimento: '2026-02-15',
22
+ # codigo_barras: '23793381286000000099901234567890123456789012',
23
+ # periodo_uso_inicio: '2026-01-01',
24
+ # periodo_uso_fim: '2026-01-31',
25
+ # valor_fatura: 99.90
26
+ # )
27
+ #
28
+ # Atributos obrigatórios:
29
+ # - competencia (formato AAAAMM, ex: '202601' ou aceita '2026-01')
30
+ # - data_vencimento (formato YYYY-MM-DD, ex: '2026-02-15')
31
+ # - codigo_barras (linha digitável do boleto, 1-48 caracteres)
32
+ # - valor_fatura (valor total da fatura)
33
+ #
34
+ # Atributos opcionais:
35
+ # - valor_liquido (valor líquido após descontos)
36
+ # - periodo_uso_inicio (início do período de uso - YYYY-MM-DD)
37
+ # - periodo_uso_fim (fim do período de uso - YYYY-MM-DD)
38
+ # - codigo_debito_automatico (código de autorização débito em conta)
39
+ # - codigo_banco (número do banco - se houver débito automático)
40
+ # - codigo_agencia (número da agência - se houver débito automático)
41
+ class Fatura
42
+ include Utils::Helpers
43
+
44
+ attr_accessor :data_vencimento, # dVencFat - YYYY-MM-DD
45
+ :codigo_barras, # codBarras - OBRIGATÓRIO
46
+ :valor_fatura, # vFat - Valor total
47
+ :valor_liquido, # vLiqFat - Valor líquido (opcional)
48
+ :periodo_uso_inicio, # dPerUsoIni (opcional)
49
+ :periodo_uso_fim, # dPerUsoFim (opcional)
50
+ :codigo_debito_automatico, # codDebAuto (opcional)
51
+ :codigo_banco, # codBanco (opcional)
52
+ :codigo_agencia # codAgencia (opcional)
53
+
54
+ attr_reader :competencia # CompetFat - AAAAMM
55
+
56
+ def initialize(attributes = {})
57
+ attributes.each do |key, value|
58
+ send("#{key}=", value) if respond_to?("#{key}=")
59
+ end
60
+
61
+ # Se valor_liquido não foi informado, usa o mesmo da fatura
62
+ @valor_liquido ||= @valor_fatura
63
+ end
64
+
65
+ # Define a competência, aceitando tanto YYYY-MM quanto AAAAMM
66
+ # @param value [String] Competência no formato YYYY-MM ou AAAAMM
67
+ def competencia=(value)
68
+ return if value.nil?
69
+
70
+ # Se vier no formato YYYY-MM, converter para AAAAMM
71
+ @competencia = if value.to_s.include?('-')
72
+ value.to_s.gsub('-', '')
73
+ else
74
+ value.to_s
75
+ end
76
+ end
77
+
78
+ def valido?
79
+ erros.empty?
80
+ end
81
+
82
+ def erros # rubocop:disable Metrics/MethodLength
83
+ errors = []
84
+
85
+ # Validar competência
86
+ if competencia.to_s.strip.empty?
87
+ errors << 'Competência é obrigatória'
88
+ elsif !competencia_valida?
89
+ errors << 'Competência deve estar no formato AAAAMM (ex: 202601)'
90
+ end
91
+
92
+ # Validar data de vencimento
93
+ if data_vencimento.to_s.strip.empty?
94
+ errors << 'Data de vencimento é obrigatória'
95
+ elsif !data_vencimento_valida?
96
+ errors << 'Data de vencimento deve estar no formato YYYY-MM-DD (ex: 2026-02-15)'
97
+ end
98
+
99
+ # Validar código de barras (OBRIGATÓRIO)
100
+ if codigo_barras.to_s.strip.empty?
101
+ errors << 'Código de barras é obrigatório'
102
+ elsif codigo_barras.to_s.length > 48
103
+ errors << 'Código de barras deve ter no máximo 48 caracteres'
104
+ end
105
+
106
+ # Validar valor da fatura
107
+ if valor_fatura.nil?
108
+ errors << 'Valor da fatura é obrigatório'
109
+ elsif valor_fatura.to_f <= 0
110
+ errors << 'Valor da fatura deve ser maior que zero'
111
+ end
112
+
113
+ # Validar períodos de uso (se informados)
114
+ if periodo_uso_inicio && periodo_uso_fim
115
+ inicio = safe_to_date(periodo_uso_inicio)
116
+ fim = safe_to_date(periodo_uso_fim)
117
+
118
+ if inicio.nil? || fim.nil?
119
+ errors << 'Período de uso: datas inválidas'
120
+ elsif inicio > fim
121
+ errors << 'Período de uso: data inicial não pode ser posterior à data final'
122
+ end
123
+ elsif periodo_uso_inicio || periodo_uso_fim
124
+ errors << 'Período de uso: ambas as datas (início e fim) devem ser informadas'
125
+ end
126
+
127
+ # Validar débito automático (se informado)
128
+ if codigo_debito_automatico
129
+ errors << 'Código do banco é obrigatório quando há débito automático' if codigo_banco.to_s.strip.empty?
130
+ errors << 'Código da agência é obrigatório quando há débito automático' if codigo_agencia.to_s.strip.empty?
131
+ end
132
+
133
+ errors
134
+ end
135
+
136
+ def linha_digitavel
137
+ CodigoDeBarras.new(codigo_barras).linha_digitavel
138
+ rescue StandardError
139
+ ''
140
+ end
141
+
142
+ private
143
+
144
+ def competencia_valida?
145
+ return false unless competencia
146
+
147
+ # Formato: AAAAMM (6 dígitos)
148
+ return false unless competencia.to_s.match?(/^\d{6}$/)
149
+
150
+ # Validar se o mês é válido (01-12)
151
+ mes = competencia[-2..].to_i
152
+ mes.between?(1, 12)
153
+ end
154
+
155
+ def data_vencimento_valida?
156
+ return false unless data_vencimento
157
+
158
+ # Se já é um Date, valida diretamente
159
+ return true if data_vencimento.is_a?(Date)
160
+
161
+ # Se é String, valida formato YYYY-MM-DD
162
+ return false unless data_vencimento.to_s.match?(/^\d{4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])$/)
163
+
164
+ # Tentar fazer parse para validar se é uma data real
165
+ Date.parse(data_vencimento.to_s)
166
+ true
167
+ rescue ArgumentError
168
+ false
169
+ end
170
+ end
171
+ end
172
+ end