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,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'uri'
4
+ require 'rqrcode'
5
+ require_relative '../validators/schema_validator'
6
+
7
+ module Nfcom
8
+ module Builder
9
+ # Gera a URL e SVG do QR Code para consulta da NF-COM.
10
+ #
11
+ # Esta classe cria a URL que será codificada em um QR Code, permitindo que os clientes
12
+ # validem a NF-COM diretamente no portal da SEFAZ.
13
+ #
14
+ # A URL gerada contém os seguintes parâmetros:
15
+ # - chNFCom: Chave de acesso da NF-COM
16
+ # - tpAmb: Código do ambiente (1 = produção, 2 = homologação)
17
+ class Qrcode
18
+ # @return [String] chave de acesso da NF-COM
19
+ attr_reader :chave
20
+
21
+ # @return [Symbol] ambiente :producao ou :homologacao
22
+ attr_reader :ambiente
23
+
24
+ # Inicializa o gerador de QR Code
25
+ #
26
+ # @param chave [String] chave de acesso da NF-COM
27
+ # @param ambiente [Symbol] :producao ou :homologacao
28
+ #
29
+ # @raise [ArgumentError] se algum argumento estiver ausente ou inválido
30
+ def initialize(chave, ambiente)
31
+ raise ArgumentError, 'Chave de acesso não pode ser vazia' if chave.nil? || chave.strip.empty?
32
+ unless Nfcom::Validators::SchemaValidator.valido_por_schema?(chave, :er3)
33
+ raise ArgumentError, "Chave de acesso inválida: #{chave}"
34
+ end
35
+
36
+ raise ArgumentError, 'Ambiente deve ser :producao ou :homologacao' unless %i[producao
37
+ homologacao].include?(ambiente)
38
+
39
+ @chave = chave
40
+ @ambiente = ambiente
41
+ end
42
+
43
+ # Gera a URL do QR Code
44
+ #
45
+ # @return [String] URL completa
46
+ def gerar_url
47
+ base_url = 'https://dfe-portal.svrs.rs.gov.br/nfcom/qrcode'
48
+ tp_amb = ambiente == :homologacao ? 2 : 1
49
+
50
+ "#{base_url}?#{URI.encode_www_form(chNFCom: chave, tpAmb: tp_amb)}"
51
+ end
52
+
53
+ # Gera a imagem SVG do QR Code
54
+ #
55
+ # @return [String] SVG do QR Code
56
+ def gerar_qrcode_svg
57
+ qr = RQRCode::QRCode.new(gerar_url)
58
+ qr.as_svg(
59
+ offset: 0,
60
+ color: '000',
61
+ shape_rendering: 'crispEdges',
62
+ module_size: 6,
63
+ standalone: true
64
+ )
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,156 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+ require 'openssl'
5
+ require 'base64'
6
+
7
+ module Nfcom
8
+ module Builder
9
+ class Signature
10
+ ALGORITHMS = {
11
+ c14n: 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315',
12
+ rsa_sha1: 'http://www.w3.org/2000/09/xmldsig#rsa-sha1',
13
+ sha1: 'http://www.w3.org/2000/09/xmldsig#sha1',
14
+ enveloped: 'http://www.w3.org/2000/09/xmldsig#enveloped-signature'
15
+ }.freeze
16
+
17
+ DSIG_NS = 'http://www.w3.org/2000/09/xmldsig#'
18
+
19
+ def initialize(configuration)
20
+ @configuration = configuration
21
+ @certificate = Utils::Certificate.new(
22
+ configuration.certificado_path,
23
+ configuration.certificado_senha
24
+ )
25
+ end
26
+
27
+ def assinar(xml_string)
28
+ # Parse XML
29
+ doc = Nokogiri::XML(xml_string)
30
+
31
+ # Find infNFCom element
32
+ inf_nfcom = doc.at_xpath(
33
+ '//nfcom:infNFCom',
34
+ 'nfcom' => 'http://www.portalfiscal.inf.br/nfcom'
35
+ )
36
+
37
+ raise Errors::XmlError, 'infNFCom element not found' unless inf_nfcom
38
+
39
+ # Get reference URI
40
+ ref_uri = "##{inf_nfcom['Id']}"
41
+
42
+ # Calculate digest of canonicalized infNFCom
43
+ digest_value = calculate_digest(inf_nfcom)
44
+
45
+ # Build SignedInfo
46
+ signed_info = build_signed_info(ref_uri, digest_value)
47
+
48
+ # Calculate signature of canonicalized SignedInfo
49
+ signature_value = calculate_signature(signed_info)
50
+
51
+ # Build complete Signature element
52
+ signature_xml = build_signature_xml(signed_info, signature_value, certificate_value)
53
+
54
+ # Insert Signature into NFCom
55
+ insert_signature(doc, signature_xml)
56
+
57
+ # Return signed XML
58
+ doc.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
59
+ end
60
+
61
+ private
62
+
63
+ def calculate_digest(element)
64
+ # Canonicalize using C14N 1.0
65
+ canonicalized = element.canonicalize(Nokogiri::XML::XML_C14N_1_0)
66
+
67
+ # Calculate SHA1 digest
68
+ digest = OpenSSL::Digest::SHA1.digest(canonicalized)
69
+
70
+ # Return Base64 encoded
71
+ Base64.strict_encode64(digest)
72
+ end
73
+
74
+ def build_signed_info(ref_uri, digest_value)
75
+ builder = Nokogiri::XML::Builder.new do |xml|
76
+ xml.SignedInfo(xmlns: DSIG_NS) do
77
+ # Use exact algorithm URIs required by NFCom
78
+ xml.CanonicalizationMethod(Algorithm: ALGORITHMS[:c14n])
79
+ xml.SignatureMethod(Algorithm: ALGORITHMS[:rsa_sha1])
80
+
81
+ xml.Reference(URI: ref_uri) do
82
+ xml.Transforms do
83
+ xml.Transform(Algorithm: ALGORITHMS[:enveloped])
84
+ xml.Transform(Algorithm: ALGORITHMS[:c14n])
85
+ end
86
+ xml.DigestMethod(Algorithm: ALGORITHMS[:sha1])
87
+ xml.DigestValue digest_value
88
+ end
89
+ end
90
+ end
91
+
92
+ builder.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
93
+ end
94
+
95
+ def calculate_signature(signed_info_xml)
96
+ # Parse SignedInfo
97
+ signed_info_doc = Nokogiri::XML(signed_info_xml)
98
+
99
+ # Canonicalize using C14N 1.0
100
+ canonicalized = signed_info_doc.canonicalize(Nokogiri::XML::XML_C14N_1_0)
101
+
102
+ # Sign with RSA-SHA1
103
+ signature = @certificate.key.sign(OpenSSL::Digest.new('SHA1'), canonicalized)
104
+
105
+ # Return Base64 encoded
106
+ Base64.strict_encode64(signature)
107
+ end
108
+
109
+ def certificate_value
110
+ # Get certificate as DER, then Base64 encode
111
+ # Remove PEM headers/footers, just the certificate data
112
+ cert_der = @certificate.cert.to_der
113
+ Base64.strict_encode64(cert_der)
114
+ end
115
+
116
+ def build_signature_xml(signed_info_xml, signature_value, cert_value)
117
+ # Parse SignedInfo to get the element
118
+ signed_info_doc = Nokogiri::XML(signed_info_xml)
119
+ signed_info_element = signed_info_doc.root
120
+
121
+ builder = Nokogiri::XML::Builder.new do |xml|
122
+ xml.Signature(xmlns: DSIG_NS) do
123
+ # Insert SignedInfo element
124
+ xml.parent << signed_info_element
125
+
126
+ xml.SignatureValue signature_value
127
+
128
+ xml.KeyInfo do
129
+ xml.X509Data do
130
+ xml.X509Certificate cert_value
131
+ end
132
+ end
133
+ end
134
+ end
135
+
136
+ builder.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
137
+ end
138
+
139
+ def insert_signature(doc, signature_xml)
140
+ # Parse signature
141
+ signature_doc = Nokogiri::XML(signature_xml)
142
+ signature_element = signature_doc.root
143
+
144
+ # Find NFCom root element
145
+ nfcom_element = doc.at_xpath(
146
+ '//nfcom:NFCom',
147
+ 'nfcom' => 'http://www.portalfiscal.inf.br/nfcom'
148
+ )
149
+
150
+ raise Errors::XmlError, 'NFCom element not found' unless nfcom_element
151
+
152
+ nfcom_element.add_child(signature_element)
153
+ end
154
+ end
155
+ end
156
+ end
@@ -0,0 +1,362 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+
5
+ module Nfcom
6
+ module Builder
7
+ # Construtor de XML para NFCom (Nota Fiscal de Comunicação)
8
+ #
9
+ # Esta classe é responsável por gerar o XML completo da NFCom
10
+ # seguindo o layout 1.00 do schema oficial da SEFAZ.
11
+ class XmlBuilder
12
+ include Utils::Helpers
13
+
14
+ attr_reader :nota, :configuration
15
+
16
+ VERSAO_LAYOUT = '1.00'
17
+ NAMESPACE = 'http://www.portalfiscal.inf.br/nfcom'
18
+ MODELO_NFCOM = 62
19
+
20
+ # Indicadores de IE do destinatário
21
+ INDIEDEST_CONTRIBUINTE = 1
22
+ INDIEDEST_ISENTO = 2
23
+ INDIEDEST_NAO_CONTRIBUINTE = 9
24
+
25
+ def initialize(nota, configuration)
26
+ @nota = nota
27
+ @configuration = configuration
28
+ end
29
+
30
+ # Gera o XML completo da NFCom
31
+ # @return [String] XML formatado em UTF-8
32
+ def gerar
33
+ builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml|
34
+ xml.NFCom(xmlns: NAMESPACE) do
35
+ xml.infNFCom(versao: VERSAO_LAYOUT, Id: "NFCom#{nota.chave_acesso}") do
36
+ gerar_ide(xml)
37
+ gerar_emit(xml)
38
+ gerar_dest(xml)
39
+ gerar_assinante(xml) if nota.assinante
40
+ gerar_sub(xml) if nota.finalidade == :substituicao
41
+ # gCofat iria aqui (cofaturamento)
42
+ gerar_detalhes(xml)
43
+ gerar_total(xml)
44
+ # gFidelidade iria aqui (programa de fidelidade)
45
+ gerar_fatura(xml) if nota.fatura
46
+ # gFatCentral iria aqui (faturamento centralizado)
47
+ # autXML iria aqui (autorizados para download)
48
+ gerar_info_adicional(xml) if nota.informacoes_adicionais
49
+ # gRespTec iria aqui (responsável técnico)
50
+ end
51
+
52
+ gerar_info_suplementar(xml)
53
+ end
54
+ end
55
+
56
+ builder.to_xml(save_with: Nokogiri::XML::Node::SaveOptions::AS_XML)
57
+ end
58
+
59
+ private
60
+
61
+ # Gera o grupo de identificação da NFCom (tag ide)
62
+ def gerar_ide(xml)
63
+ xml.ide do
64
+ xml.cUF configuration.codigo_uf
65
+ xml.tpAmb configuration.ambiente_codigo
66
+ xml.mod MODELO_NFCOM
67
+ xml.serie nota.serie
68
+ xml.nNF nota.numero
69
+ xml.cNF nota.codigo_verificacao
70
+ xml.cDV nota.chave_acesso[-1].to_i
71
+ xml.dhEmi formatar_data_hora(nota.data_emissao)
72
+ xml.tpEmis nota.tipo_emissao_codigo
73
+ xml.nSiteAutoriz 0 # Zero para autorizador com único site
74
+ xml.cMunFG nota.emitente.endereco.codigo_municipio
75
+ xml.finNFCom nota.finalidade_codigo
76
+ xml.tpFat nota.tipo_faturamento || 0 # 0=Normal, 1=Centralizado, 2=Cofaturamento
77
+ xml.verProc 'Nfcom-Ruby-1.0'
78
+
79
+ # Campos de contingência (se aplicável)
80
+ if nota.tipo_emissao == :contingencia
81
+ xml.dhCont formatar_data_hora(nota.data_contingencia) if nota.data_contingencia
82
+ xml.xJust limitar_texto(nota.justificativa_contingencia, 256) if nota.justificativa_contingencia
83
+ end
84
+ end
85
+ end
86
+
87
+ # Gera o grupo de dados do emitente (tag emit)
88
+ def gerar_emit(xml)
89
+ xml.emit do
90
+ xml.CNPJ apenas_numeros(nota.emitente.cnpj)
91
+ xml.IE apenas_numeros(nota.emitente.inscricao_estadual)
92
+ # IEUFDest - IE virtual na UF destino (opcional para partilha)
93
+ xml.CRT nota.emitente.regime_tributario_codigo || 3 # 1=Simples, 2=Simples excesso, 3=Normal
94
+ xml.xNome limitar_texto(nota.emitente.razao_social, 60)
95
+ xml.xFant limitar_texto(nota.emitente.nome_fantasia, 60) if nota.emitente.nome_fantasia
96
+ gerar_endereco(xml, nota.emitente.endereco, 'emit')
97
+ end
98
+ end
99
+
100
+ # Gera o grupo de dados do destinatário (tag dest)
101
+ def gerar_dest(xml)
102
+ xml.dest do
103
+ xml.xNome limitar_texto(nota.destinatario.razao_social, 60)
104
+
105
+ cpf = apenas_numeros(nota.destinatario.cpf)
106
+ cnpj = apenas_numeros(nota.destinatario.cnpj)
107
+ ie = apenas_numeros(nota.destinatario.inscricao_estadual)
108
+
109
+ # CPF ou CNPJ (mutuamente exclusivo)
110
+ if cnpj.to_s.strip == ''
111
+ xml.CPF cpf
112
+ else
113
+ xml.CNPJ cnpj
114
+ end
115
+
116
+ if cnpj.to_s.strip != '' && ie.to_s.strip != ''
117
+ # PJ com IE → Contribuinte
118
+ xml.indIEDest INDIEDEST_CONTRIBUINTE
119
+ xml.IE ie
120
+ else
121
+ # PF ou PJ sem IE → Não contribuinte
122
+ xml.indIEDest INDIEDEST_NAO_CONTRIBUINTE
123
+ end
124
+
125
+ gerar_endereco(xml, nota.destinatario.endereco, 'dest')
126
+ end
127
+ end
128
+
129
+ # Gera o grupo de dados do assinante (tag assinante)
130
+ def gerar_assinante(xml)
131
+ assinante = nota.assinante
132
+ return unless assinante
133
+
134
+ xml.assinante do
135
+ # Código único do assinante (1-30 caracteres)
136
+ xml.iCodAssinante limitar_texto(assinante.codigo, 30)
137
+
138
+ # Tipo: 1=Comercial, 2=Industrial, 3=Residencial, 4=Rural, 5=Público, 6=Telecom,
139
+ # 7=Diplomático, 8=Religioso, 99=Outros
140
+ xml.tpAssinante assinante.tipo
141
+
142
+ # Tipo serviço: 1=Telefonia, 2=Dados, 3=TV, 4=Internet, 5=Multimídia, 6=Outros, 7=Vários
143
+ xml.tpServUtil assinante.tipo_servico
144
+
145
+ # Dados do contrato (opcional)
146
+ xml.nContrato limitar_texto(assinante.numero_contrato, 20) if assinante.numero_contrato
147
+ xml.dContratoIni formatar_data(assinante.data_inicio_contrato) if assinante.data_inicio_contrato
148
+ xml.dContratoFim formatar_data(assinante.data_fim_contrato) if assinante.data_fim_contrato
149
+
150
+ # Terminal principal (condicional - se informar um, deve informar ambos)
151
+ if assinante.terminal_principal && assinante.uf_terminal_principal
152
+ xml.NroTermPrinc apenas_numeros(assinante.terminal_principal)
153
+ xml.cUFPrinc assinante.uf_terminal_principal
154
+ end
155
+
156
+ # Terminais adicionais (0-n ocorrências)
157
+ assinante.terminais_adicionais&.each do |terminal|
158
+ xml.NroTermAdic apenas_numeros(terminal[:numero])
159
+ xml.cUFAdic terminal[:uf]
160
+ end
161
+ end
162
+ end
163
+
164
+ # Gera o grupo de faturamento (tag gFat)
165
+ # Contém apenas informações de controle de cobrança, NÃO valores
166
+ def gerar_fatura(xml)
167
+ xml.gFat do
168
+ # Competência no formato AAAAMM (ex: 202601)
169
+ xml.CompetFat nota.fatura.competencia
170
+
171
+ # Data de vencimento no formato AAAA-MM-DD
172
+ xml.dVencFat nota.fatura.data_vencimento
173
+
174
+ # Período de uso do serviço (ambos ou nenhum)
175
+ if nota.fatura.periodo_uso_inicio && nota.fatura.periodo_uso_fim
176
+ xml.dPerUsoIni formatar_data(nota.fatura.periodo_uso_inicio)
177
+ xml.dPerUsoFim formatar_data(nota.fatura.periodo_uso_fim)
178
+ end
179
+
180
+ # Linha digitável do código de barras (obrigatório)
181
+ xml.codBarras nota.fatura.codigo_barras
182
+
183
+ # Débito automático (opcional - se informar um, deve informar todos)
184
+ if nota.fatura.codigo_debito_automatico
185
+ xml.codDebAuto nota.fatura.codigo_debito_automatico
186
+ xml.codBanco nota.fatura.codigo_banco
187
+ xml.codAgencia nota.fatura.codigo_agencia
188
+ end
189
+
190
+ # enderCorresp - Endereço de correspondência (opcional)
191
+ # gPIX - Informações do PIX (opcional)
192
+ end
193
+ end
194
+
195
+ # Gera o endereço do emitente ou destinatário
196
+ # @param xml [Nokogiri::XML::Builder] Builder do XML
197
+ # @param endereco [Endereco] Objeto com dados do endereço
198
+ # @param tipo [String] 'emit' ou 'dest'
199
+ def gerar_endereco(xml, endereco, tipo)
200
+ tag_name = tipo == 'emit' ? 'enderEmit' : 'enderDest'
201
+
202
+ xml.send(tag_name) do
203
+ xml.xLgr limitar_texto(endereco.logradouro, 60)
204
+ xml.nro limitar_texto(endereco.numero, 60)
205
+ xml.xCpl limitar_texto(endereco.complemento, 60) if endereco.complemento
206
+ xml.xBairro limitar_texto(endereco.bairro, 60)
207
+ xml.cMun endereco.codigo_municipio
208
+ xml.xMun limitar_texto(endereco.municipio, 60)
209
+ xml.CEP apenas_numeros(endereco.cep)
210
+ xml.UF endereco.uf
211
+
212
+ # País apenas para destinatário
213
+ if tipo == 'dest'
214
+ xml.cPais endereco.codigo_pais || 1058 # 1058 = Brasil (tabela BACEN)
215
+ xml.xPais endereco.pais || 'Brasil'
216
+ end
217
+
218
+ xml.fone apenas_numeros(endereco.telefone) if endereco.telefone
219
+ end
220
+ end
221
+
222
+ # Gera os itens/serviços da NFCom (tag det)
223
+ def gerar_detalhes(xml)
224
+ nota.itens.each do |item|
225
+ xml.det(nItem: item.numero_item) do
226
+ gerar_produto(xml, item)
227
+ gerar_impostos_item(xml, item)
228
+ end
229
+ end
230
+ end
231
+
232
+ # Gera os dados do produto/serviço (tag prod)
233
+ def gerar_produto(xml, item)
234
+ xml.prod do
235
+ xml.cProd item.codigo_servico
236
+ xml.xProd limitar_texto(item.descricao, 120)
237
+ xml.cClass item.classe_consumo # Código de classificação do item (tabela ANATEL)
238
+ xml.CFOP item.cfop
239
+ xml.uMed item.unidade # 1=Minuto, 2=MB, 3=GB, 4=UN
240
+ xml.qFaturada formatar_decimal(item.quantidade, 4)
241
+
242
+ # vItem = valor unitário, vProd = valor total do item
243
+ xml.vItem formatar_decimal(item.valor_unitario)
244
+ xml.vProd formatar_decimal(item.valor_total)
245
+
246
+ # Valores opcionais
247
+ xml.vDesc formatar_decimal(item.valor_desconto) if item.valor_desconto&.positive?
248
+ xml.vOutro formatar_decimal(item.valor_outras_despesas) if item.valor_outras_despesas&.positive?
249
+ end
250
+ end
251
+
252
+ # Gera os impostos do item (tag imposto)
253
+ def gerar_impostos_item(xml, _item)
254
+ xml.imposto do
255
+ # ICMS00 - Tributação normal do ICMS
256
+ # Para ISPs normalmente BC=0 (serviço isento ou não tributado)
257
+ xml.ICMS00 do
258
+ xml.CST '00' # 00 = Tributação normal
259
+ xml.vBC '0.00' # Base de cálculo (0.00 para isentos)
260
+ xml.pICMS '0.00' # Alíquota do ICMS
261
+ xml.vICMS '0.00' # Valor do ICMS
262
+ end
263
+
264
+ # PIS - Programa de Integração Social (se aplicável)
265
+ # xml.PIS do
266
+ # xml.CST '01' # 01=Tributável com alíquota básica
267
+ # xml.vBC formatar_decimal(item.valor_total)
268
+ # xml.pPIS '0.65' # Alíquota padrão 0.65%
269
+ # xml.vPIS formatar_decimal(item.valor_total * 0.0065)
270
+ # end
271
+
272
+ # COFINS - Contribuição para Financiamento da Seguridade Social (se aplicável)
273
+ # xml.COFINS do
274
+ # xml.CST '01' # 01=Tributável com alíquota básica
275
+ # xml.vBC formatar_decimal(item.valor_total)
276
+ # xml.pCOFINS '3.00' # Alíquota padrão 3%
277
+ # xml.vCOFINS formatar_decimal(item.valor_total * 0.03)
278
+ # end
279
+
280
+ # FUST - Fundo de Universalização dos Serviços de Telecomunicações (se aplicável)
281
+ # FUNTTEL - Fundo para o Desenvolvimento Tecnológico (se aplicável)
282
+ end
283
+ end
284
+
285
+ # Gera os totalizadores da NFCom (tag total)
286
+ def gerar_total(xml)
287
+ xml.total do
288
+ # Valor total dos produtos/serviços
289
+ xml.vProd formatar_decimal(nota.total.valor_servicos)
290
+
291
+ # Totais de ICMS
292
+ xml.ICMSTot do
293
+ xml.vBC formatar_decimal(nota.total.icms_base_calculo)
294
+ xml.vICMS formatar_decimal(nota.total.icms_valor)
295
+ xml.vICMSDeson formatar_decimal(nota.total.icms_desonerado || 0)
296
+ xml.vFCP formatar_decimal(nota.total.fcp_valor || 0) # Fundo de Combate à Pobreza
297
+ end
298
+
299
+ # Tributos federais
300
+ xml.vCOFINS formatar_decimal(nota.total.cofins_valor)
301
+ xml.vPIS formatar_decimal(nota.total.pis_valor)
302
+ xml.vFUNTTEL formatar_decimal(nota.total.funttel_valor || 0)
303
+ xml.vFUST formatar_decimal(nota.total.fust_valor || 0)
304
+
305
+ # Retenções na fonte
306
+ xml.vRetTribTot do
307
+ xml.vRetPIS formatar_decimal(nota.total.pis_retido || 0)
308
+ xml.vRetCofins formatar_decimal(nota.total.cofins_retido || 0)
309
+ xml.vRetCSLL formatar_decimal(nota.total.csll_retido || 0)
310
+ xml.vIRRF formatar_decimal(nota.total.irrf_retido || 0)
311
+ end
312
+
313
+ # Descontos e acréscimos
314
+ xml.vDesc formatar_decimal(nota.total.valor_desconto)
315
+ xml.vOutro formatar_decimal(nota.total.valor_outras_despesas)
316
+
317
+ # Valor total da NFCom (deve ser o último campo)
318
+ xml.vNF formatar_decimal(nota.total.valor_total)
319
+ end
320
+ end
321
+
322
+ # Gera informações adicionais (tag infAdic)
323
+ def gerar_info_adicional(xml)
324
+ return unless nota.informacoes_adicionais&.any?
325
+
326
+ xml.infAdic do
327
+ nota.informacoes_adicionais.each do |texto|
328
+ xml.infCpl texto
329
+ end
330
+ end
331
+ end
332
+
333
+ # Gera informações suplementares (tag infNFComSupl)
334
+ # Contém o QR Code para consulta da NFCom
335
+ def gerar_info_suplementar(xml)
336
+ xml.infNFComSupl do
337
+ xml.qrCodNFCom gerar_qrcode
338
+ end
339
+ end
340
+
341
+ # Gera a URL do QR Code para consulta da NFCom
342
+ # @return [String] URL completa do QR Code
343
+ def gerar_qrcode
344
+ base_url = 'https://dfe-portal.svrs.rs.gov.br/nfcom/qrcode'
345
+
346
+ # Formato: URL?chNFCom=CHAVE&tpAmb=AMBIENTE
347
+ "#{base_url}?chNFCom=#{nota.chave_acesso}&tpAmb=#{configuration.ambiente_codigo}"
348
+ end
349
+
350
+ # Gera o grupo de informações da substituição (tag gSub)
351
+ # Obrigatório quando finNFCom = 3 (Substituição)
352
+ def gerar_sub(xml)
353
+ return unless nota.chave_nfcom_substituida
354
+
355
+ xml.gSub do
356
+ xml.chNFCom nota.chave_nfcom_substituida
357
+ xml.motSub nota.motivo_substituicao.to_s.rjust(2, '0')
358
+ end
359
+ end
360
+ end
361
+ end
362
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nfcom
4
+ class Client
5
+ attr_reader :configuration
6
+
7
+ def initialize(config = nil)
8
+ @configuration = config || Nfcom.configuration
9
+ raise Errors::ConfigurationError, 'Nfcom não está configurado' if @configuration.nil?
10
+ end
11
+
12
+ # Autoriza uma nota fiscal
13
+ def autorizar(nota) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength
14
+ validar_configuracao!
15
+
16
+ raise Errors::ValidationError, nota.erros.join(', ') unless nota.valida?
17
+
18
+ # Gera chave de acesso
19
+ nota.gerar_chave_acesso
20
+
21
+ # Constrói XML
22
+ xml_builder = Builder::XmlBuilder.new(nota, configuration)
23
+ xml = xml_builder.gerar
24
+
25
+ # Assina XML
26
+ signature = Builder::Signature.new(configuration)
27
+ xml_assinado = signature.assinar(xml)
28
+
29
+ tentativa = 0
30
+ begin
31
+ ws = Webservices::Autorizacao.new(configuration)
32
+ resposta = ws.enviar(xml_assinado)
33
+
34
+ parser = Parsers::Autorizacao.new(resposta)
35
+ resultado = parser.parse
36
+
37
+ nota.protocolo = resultado[:protocolo]
38
+ nota.data_autorizacao = resultado[:data_autorizacao]
39
+ nota.xml_autorizado = Utils::XmlAuthorized.build_nfcom_proc(
40
+ xml_assinado: xml_assinado,
41
+ xml_protocolo: resultado[:xml]
42
+ )
43
+
44
+ resultado
45
+ rescue Errors::SefazIndisponivel => e
46
+ tentativa += 1
47
+ raise e unless tentativa < configuration.max_tentativas
48
+
49
+ sleep(configuration.tempo_espera_retry**tentativa)
50
+ retry
51
+ end
52
+ end
53
+
54
+ # Consulta uma nota pela chave de acesso
55
+ def consultar_nota(chave:)
56
+ validar_configuracao!
57
+
58
+ ws = Webservices::Consulta.new(configuration)
59
+ resposta = ws.consultar(chave)
60
+
61
+ parser = Parsers::Consulta.new(resposta)
62
+ parser.parse
63
+ end
64
+
65
+ # Verifica status do serviço da SEFAZ
66
+ def status_servico
67
+ validar_configuracao!
68
+
69
+ ws = Webservices::Status.new(configuration)
70
+ resposta = ws.verificar
71
+
72
+ parser = Parsers::Status.new(resposta)
73
+ parser.parse
74
+ end
75
+
76
+ # Inutiliza uma numeração de nota
77
+ def inutilizar(serie:, numero_inicial:, numero_final:, justificativa:)
78
+ validar_configuracao!
79
+
80
+ raise Errors::ValidationError, 'Justificativa deve ter no mínimo 15 caracteres' if justificativa.length < 15
81
+
82
+ ws = Webservices::Inutilizacao.new(configuration)
83
+ resposta = ws.inutilizar(
84
+ serie: serie,
85
+ numero_inicial: numero_inicial,
86
+ numero_final: numero_final,
87
+ justificativa: justificativa
88
+ )
89
+
90
+ parser = Parsers::Inutilizacao.new(resposta)
91
+ parser.parse
92
+ end
93
+
94
+ private
95
+
96
+ def validar_configuracao!
97
+ erros = []
98
+ erros << 'Certificado não configurado' if configuration.certificado_path.nil?
99
+ erros << 'CNPJ não configurado' if configuration.cnpj.nil?
100
+ erros << 'Inscrição Estadual não configurada' if configuration.inscricao_estadual.nil?
101
+ erros << 'Estado não configurado' if configuration.estado.nil?
102
+
103
+ raise Errors::ConfigurationError, erros.join(', ') unless erros.empty?
104
+ end
105
+ end
106
+ end