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,316 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nfcom
4
+ module Validators
5
+ # Validadores baseados no Schema NFCom v1.00
6
+ # Expressões Regulares (ER) conforme documentação oficial SEFAZ
7
+ module SchemaValidator # rubocop:disable Metrics/ModuleLength
8
+ # Expressões Regulares do Schema NFCom
9
+ REGEX_PATTERNS = {
10
+ # ER1 - Data/hora no formato AAAA-MM-DDTHH:MM:SS+HH:MM
11
+ er1: /
12
+ \A
13
+ (
14
+ (20(([02468][048])|([13579][26]))-02-29) |
15
+ (20[0-9][0-9])-
16
+ (
17
+ (((0[1-9])|(1[0-2]))-((0[1-9])|(1\d)|(2[0-8]))) |
18
+ ((((0[13578])|(1[02]))-31)) |
19
+ (((0[1,3-9])|(1[0-2]))-(29|30))
20
+ )
21
+ )
22
+ T
23
+ (20|21|22|23|[0-1]\d):
24
+ [0-5]\d:
25
+ [0-5]\d
26
+ (
27
+ [-,+](0[0-9]|10|11):00 |
28
+ (\+(12):00)
29
+ )
30
+ \z
31
+ /x,
32
+
33
+ # ER2 - 7 dígitos (cNF, cMun, etc)
34
+ er2: /\A[0-9]{7}\z/,
35
+
36
+ # ER3 - Chave de acesso (44 dígitos)
37
+ er3: /\A[0-9]{6}[A-Z0-9]{12}[0-9]{26}\z/,
38
+
39
+ # ER7 - CNPJ (14 dígitos)
40
+ er7: /\A[A-Z0-9]{12}[0-9]{2}\z/,
41
+
42
+ # ER8 - CNPJ opcional (0 ou 14 dígitos)
43
+ er8: /\A([0-9]{0}|[A-Z0-9]{12}[0-9]{2})\z/,
44
+
45
+ # ER9 - CPF (11 dígitos)
46
+ er9: /\A[0-9]{11}\z/,
47
+
48
+ # ER11 - Alíquota ICMS (3,2)
49
+ er11: /\A(0|0\.[0-9]{2}|[1-9]{1}[0-9]{0,2}(\.[0-9]{2})?)\z/,
50
+
51
+ # ER16 - Percentual (3,2-4)
52
+ er16: /\A(0|0\.[0-9]{2,4}|[1-9]{1}[0-9]{0,2}(\.[0-9]{2,4})?)\z/,
53
+
54
+ # ER31 - Quantidade (11,0-4)
55
+ er31: /\A[0-9]{1,11}(\.[0-9]{2,4})?\z/,
56
+
57
+ # ER36 - Valor (13,2)
58
+ er36: /\A(0|0\.[0-9]{2}|[1-9]{1}[0-9]{0,12}(\.[0-9]{2})?)\z/,
59
+
60
+ # ER37 - Valor (13,2) - pode ser zero
61
+ er37: /\A0\.[0-9]{2}|[1-9]{1}[0-9]{0,12}(\.[0-9]{2})?\z/,
62
+
63
+ # ER39 - Valor (13,2-8)
64
+ er39: /\A[0-9]{1,13}(\.[0-9]{2,8})?\z/,
65
+
66
+ # ER41 - IE (0-14 dígitos ou ISENTO)
67
+ er41: /\A([0-9]{0,14}|ISENTO)\z/,
68
+
69
+ # ER42 - IE (2-14 dígitos)
70
+ er42: /\A[0-9]{2,14}\z/,
71
+
72
+ # ER43 - Número NF (1-9 dígitos, não pode começar com zero)
73
+ er43: /\A[1-9]{1}[0-9]{0,8}\z/,
74
+
75
+ # ER44 - Série (0 ou 1-999)
76
+ er44: /\A(0|[1-9]{1}[0-9]{0,2})\z/,
77
+
78
+ # ER47 - Texto geral (1-infinito caracteres, não pode ter apenas espaços)
79
+ er47: /\A[^\r\n\t]*[!-ÿ][^\r\n\t]*\z/,
80
+
81
+ # ER48 - Data AAAA-MM-DD
82
+ er48: /
83
+ \A
84
+ (
85
+ (20|19|18)(([02468][048])|([13579][26]))-02-29 |
86
+ (20|19|18)[0-9][0-9]-
87
+ (
88
+ (((0[1-9])|(1[0-2]))-((0[1-9])|(1\d)|(2[0-8]))) |
89
+ ((((0[13578])|(1[02]))-31)) |
90
+ (((0[1,3-9])|(1[0-2]))-(29|30))
91
+ )
92
+ )
93
+ \z
94
+ /x,
95
+
96
+ # ER57 - 1 dígito
97
+ er57: /\A[0-9]{1}\z/,
98
+
99
+ # ER58 - Texto (0 ou 2-20 caracteres)
100
+ er58: /\A([!-ÿ]{0}|[!-ÿ]{2,20})?\z/,
101
+
102
+ # ER59 - Texto (0 ou 1-30 caracteres)
103
+ er59: /\A([!-ÿ]{0}|[!-ÿ]{1,30})?\z/,
104
+
105
+ # ER60 - Texto (0 ou 1-20 caracteres)
106
+ er60: /\A([!-ÿ]{0}|[!-ÿ]{1,20})?\z/,
107
+
108
+ # ER61 - Telefone (7-12 dígitos)
109
+ er61: /\A[0-9]{7,12}\z/,
110
+
111
+ # ER62 - Número item (1-9999)
112
+ er62: /\A[1-9]{1}[0-9]{0,3}\z/,
113
+
114
+ # ER63 - Número (1-20 dígitos)
115
+ er63: /\A[0-9]{1,20}\z/,
116
+
117
+ # ER64 - Código barras (1-48 dígitos)
118
+ er64: /\A[0-9]{1,48}\z/,
119
+
120
+ # ER65 - ID com prefixo NFCom
121
+ er65: /\ANFCom[0-9]{6}[A-Z0-9]{12}[0-9]{26}\z/,
122
+
123
+ # ER67 - CEP (8 dígitos)
124
+ er67: /\A[0-9]{8}\z/,
125
+
126
+ # ER70 - Versão (1.00)
127
+ er70: /\A1\.00\z/,
128
+
129
+ # ER72 - Email
130
+ er72: /\A[^@]+@[^.]+\..+\z/,
131
+
132
+ # ER73 - CFOP
133
+ er73: /\A[123567][0-9]([0-9][1-9]|[1-9][0-9])\z/,
134
+
135
+ # ER74 - Competência (1-6 dígitos)
136
+ er74: /\A[0-9]{1,6}\z/
137
+ }.freeze
138
+
139
+ DOMAINS = {
140
+ # D1 - Códigos UF (IBGE)
141
+ d1: [11, 12, 13, 14, 15, 16, 17, 21, 22, 23, 24, 25, 26, 27, 28, 29,
142
+ 31, 32, 33, 35, 41, 42, 43, 50, 51, 52, 53],
143
+
144
+ # D4 - Modelo NFCom
145
+ d4: [62],
146
+
147
+ # D5 - Siglas UF
148
+ d5: %w[AC AL AM AP BA CE DF ES GO MA MG MS MT PA PB PE PI PR RJ RN RO RR RS SC SE SP TO],
149
+
150
+ # D7 - Tipo de Ambiente (tpAmb)
151
+ d7: [1, 2], # 1=Produção, 2=Homologação
152
+
153
+ # D8 - Valores 1-4 (usado em vários campos como uMed)
154
+ d8: [1, 2, 3, 4],
155
+
156
+ # D10 - Indicador booleano
157
+ d10: [1],
158
+
159
+ # D11 - CST ICMS - Tributação normal
160
+ d11: ['00'],
161
+
162
+ # D12 - CST ICMS - Tributação com redução de BC
163
+ d12: ['20'],
164
+
165
+ # D13 - CST ICMS - Isenta/Não tributada
166
+ d13: %w[40 41],
167
+
168
+ # D14 - CST ICMS - Diferimento
169
+ d14: ['51'],
170
+
171
+ # D15 - CST ICMS - Outros
172
+ d15: ['90'],
173
+
174
+ # D16 - CST PIS/COFINS
175
+ d16: %w[01 02 06 07 08 09 49],
176
+
177
+ # D18 - Tipos de assinante
178
+ d18: [1, 2, 3, 4, 5, 6, 7, 8, 99],
179
+
180
+ # D19 - Finalidade da NFCom (finNFCom)
181
+ d19: [0, 3, 4], # 0=Normal, 3=Substituição, 4=Ajuste
182
+
183
+ # D20 - Tipo de Faturamento (tpFat)
184
+ d20: [0, 1, 2], # 0=Normal, 1=Centralizado, 2=Cofaturamento
185
+
186
+ # D22 - Indicador IE Destinatário (indIEDest)
187
+ d22: [1, 2, 9], # 1=Contribuinte, 2=Isento, 9=Não Contribuinte
188
+
189
+ # D23 - Código Regime Tributário (CRT)
190
+ d23: [1, 2, 3], # 1=Simples Nacional, 2=Simples Excesso, 3=Normal
191
+
192
+ # D24 - Tipos de serviço utilizado
193
+ d24: [1, 2, 3, 4, 5, 6, 7],
194
+
195
+ # D25 - Modelo documento (NF21/22)
196
+ d25: [21, 22],
197
+
198
+ # D26 - Motivo de substituição
199
+ d26: %w[01 02 03 04 05]
200
+ }.freeze
201
+
202
+ def self.valido_por_schema?(valor, chave_regex)
203
+ return false if valor.nil?
204
+
205
+ pattern = REGEX_PATTERNS[chave_regex]
206
+ return false unless pattern
207
+
208
+ valor.to_s.match?(pattern)
209
+ end
210
+
211
+ # Valida um valor contra um domínio
212
+ def self.valido_por_dominio?(valor, chave_dominio, converter_para_int: false)
213
+ return false if valor.nil?
214
+
215
+ dominio = DOMAINS[chave_dominio]
216
+ return false unless dominio
217
+
218
+ valor_comparar = converter_para_int ? valor.to_i : valor.to_s.upcase
219
+ dominio.include?(valor_comparar)
220
+ end
221
+
222
+ # Validadores complexos que fazem mais do que checar pattern/domain
223
+
224
+ def self.cnpj_formato_valido?(cnpj)
225
+ cnpj_limpo = cnpj.to_s.gsub(/\D/, '')
226
+ cnpj_limpo.length == 14 && cnpj_limpo.match?(/\A[0-9]{14}\z/)
227
+ end
228
+
229
+ def self.cpf_formato_valido?(cpf)
230
+ cpf_limpo = cpf.to_s.gsub(/\D/, '')
231
+ valido_por_schema?(cpf_limpo, :er9)
232
+ end
233
+
234
+ def self.cep_valido?(cep)
235
+ cep_limpo = cep.to_s.gsub(/\D/, '')
236
+ valido_por_schema?(cep_limpo, :er67)
237
+ end
238
+
239
+ def self.telefone_valido?(telefone)
240
+ return true if telefone.nil? || telefone.to_s.strip.empty?
241
+
242
+ telefone_limpo = telefone.to_s.gsub(/\D/, '')
243
+ valido_por_schema?(telefone_limpo, :er61)
244
+ end
245
+
246
+ def self.email_valido?(email)
247
+ return true if email.nil? || email.to_s.strip.empty?
248
+
249
+ valido_por_schema?(email.to_s, :er72)
250
+ end
251
+
252
+ def self.texto_valido?(texto, tamanho_max = nil)
253
+ return false if texto.nil? || texto.to_s.strip.empty?
254
+ return false if tamanho_max && texto.to_s.length > tamanho_max
255
+ # Verifica espaços no início ou fim (ER47 não permite)
256
+ return false if texto != texto.strip
257
+
258
+ valido_por_schema?(texto.to_s, :er47)
259
+ end
260
+
261
+ def self.data_valida?(data)
262
+ return false if data.nil? || data.to_s.strip.empty?
263
+
264
+ valido_por_schema?(data.to_s, :er48)
265
+ end
266
+
267
+ def self.cst_icms_valido?(cst)
268
+ cst_str = cst.to_s.rjust(2, '0')
269
+ DOMAINS[:d11].include?(cst_str) ||
270
+ DOMAINS[:d12].include?(cst_str) ||
271
+ DOMAINS[:d13].include?(cst_str) ||
272
+ DOMAINS[:d14].include?(cst_str) ||
273
+ DOMAINS[:d15].include?(cst_str)
274
+ end
275
+
276
+ # Executa validações múltiplas e retorna mensagens de erro
277
+ def self.validar_campos(campos) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
278
+ erros = []
279
+
280
+ campos.each do |campo, config|
281
+ valor = config[:valor]
282
+ validador = config[:validador]
283
+ nome = config[:nome] || campo.to_s
284
+
285
+ valido = if validador.to_s.start_with?('er')
286
+ # ER pattern: :er48, :er59, etc.
287
+ valido_por_schema?(valor, validador)
288
+ elsif validador.to_s.start_with?('d')
289
+ # Domain: :d18, :d24, etc.
290
+ # Assume integer domains unless the domain contains strings
291
+ sample = DOMAINS[validador]&.first
292
+ converter_para_int = sample.is_a?(Integer)
293
+ valido_por_dominio?(valor, validador, converter_para_int: converter_para_int)
294
+ else
295
+ # Named validator methods
296
+ case validador
297
+ when :cnpj then cnpj_formato_valido?(valor)
298
+ when :cpf then cpf_formato_valido?(valor)
299
+ when :cep then cep_valido?(valor)
300
+ when :telefone then telefone_valido?(valor)
301
+ when :email then email_valido?(valor)
302
+ when :texto then texto_valido?(valor, config[:max])
303
+ when :data then data_valida?(valor)
304
+ when :cst_icms then cst_icms_valido?(valor)
305
+ else true
306
+ end
307
+ end
308
+
309
+ erros << "#{nome} inválido: '#{valor}'" unless valido
310
+ end
311
+
312
+ erros
313
+ end
314
+ end
315
+ end
316
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nfcom
4
+ module Validators
5
+ class XmlValidator
6
+ SCHEMA_PATH = File.join(__dir__, '../../schemas/nfcom_v1.00.xsd')
7
+
8
+ def validar(xml)
9
+ # TODO: Implementar validação contra XSD
10
+ # Por enquanto, apenas valida se é XML válido
11
+ doc = Nokogiri::XML(xml)
12
+
13
+ if doc.errors.any?
14
+ erros = doc.errors.map(&:message).join(', ')
15
+ raise Errors::ValidationError, "XML inválido: #{erros}"
16
+ end
17
+
18
+ # TODO: Validar contra schema XSD quando disponível
19
+ # xsd = Nokogiri::XML::Schema(File.read(SCHEMA_PATH))
20
+ # erros = xsd.validate(doc)
21
+ # raise Errors::ValidationError, erros.map(&:message).join(', ') if erros.any?
22
+
23
+ true
24
+ rescue Nokogiri::XML::SyntaxError => e
25
+ raise Errors::XmlError, "Erro de sintaxe no XML: #{e.message}"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nfcom
4
+ VERSION = '0.1.2'
5
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nfcom
4
+ module Webservices
5
+ class Autorizacao < Base
6
+ def enviar(xml_assinado)
7
+ url = configuration.webservice_url(:recepcao)
8
+ unless url
9
+ raise Errors::ConfigurationError,
10
+ "URL de recepção não configurada para #{configuration.estado}"
11
+ end
12
+
13
+ xml_limpo = Utils::XmlCleaner.clean(xml_assinado)
14
+ configuration.logger&.debug("XML da nota:\n#{xml_limpo}") if configuration.log_level == :debug
15
+
16
+ xml_comprimido = Utils::Compressor.gzip_base64(xml_limpo)
17
+
18
+ body_xml = build_nfcom_body(xml_comprimido)
19
+ envelope = montar_envelope(body_xml)
20
+
21
+ action = 'http://www.portalfiscal.inf.br/nfcom/wsdl/NFComRecepcao/nfcomRecepcao'
22
+ post_soap(url: url, action: action, xml: envelope)
23
+ end
24
+
25
+ private
26
+
27
+ def build_nfcom_body(xml_comprimido)
28
+ <<~XML
29
+ <nfcomDadosMsg xmlns="http://www.portalfiscal.inf.br/nfcom/wsdl/NFComRecepcao">
30
+ #{xml_comprimido}
31
+ </nfcomDadosMsg>
32
+ XML
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nfcom
4
+ module Webservices
5
+ class Base
6
+ attr_reader :configuration, :certificate
7
+
8
+ def initialize(configuration)
9
+ @configuration = configuration
10
+ @certificate = Utils::Certificate.new(
11
+ configuration.certificado_path,
12
+ configuration.certificado_senha
13
+ )
14
+ end
15
+
16
+ protected
17
+
18
+ def post_soap(url:, action:, xml:)
19
+ uri = URI.parse(url)
20
+ http = configure_http_client(uri)
21
+ req = build_http_request(uri, action, xml)
22
+
23
+ log_request(xml)
24
+ response = http.request(req)
25
+ log_response(response)
26
+
27
+ validate_http_response(response)
28
+
29
+ response.body
30
+ rescue ::Timeout::Error
31
+ raise Errors::TimeoutError, 'Timeout na comunicação com SEFAZ'
32
+ rescue OpenSSL::SSL::SSLError => e
33
+ raise Errors::SefazError, "Erro SSL: #{e.message}"
34
+ rescue StandardError => e
35
+ raise Errors::SefazError, "Erro SOAP: #{e.message}"
36
+ end
37
+
38
+ def montar_envelope(body_xml)
39
+ xml = '<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope">' \
40
+ '<soap:Body>' \
41
+ "#{body_xml}" \
42
+ '</soap:Body>' \
43
+ '</soap:Envelope>'
44
+ Utils::XmlCleaner.clean(xml)
45
+ end
46
+
47
+ private
48
+
49
+ def configure_http_client(uri)
50
+ http = Net::HTTP.new(uri.host, uri.port)
51
+ http.use_ssl = uri.scheme == 'https'
52
+ http.verify_mode = OpenSSL::SSL::VERIFY_NONE
53
+ http.open_timeout = configuration.timeout
54
+ http.read_timeout = configuration.timeout
55
+
56
+ cert = certificate.to_pem
57
+ http.cert = OpenSSL::X509::Certificate.new(cert[:cert])
58
+ http.key = OpenSSL::PKey::RSA.new(cert[:key])
59
+
60
+ http
61
+ end
62
+
63
+ def build_http_request(uri, action, xml)
64
+ path = uri.request_uri
65
+ path = '/' if path.nil? || path.empty?
66
+
67
+ request = Net::HTTP::Post.new(path)
68
+ request['Content-Type'] =
69
+ %(application/soap+xml;charset=UTF-8;action="#{action}")
70
+ request.body = xml
71
+ request
72
+ end
73
+
74
+ def validate_http_response(response)
75
+ return if response.is_a?(Net::HTTPSuccess)
76
+
77
+ raise Errors::SefazError,
78
+ "Erro HTTP #{response.code}: #{response.message}"
79
+ end
80
+
81
+ def log_request(xml)
82
+ return unless configuration.log_level == :debug
83
+
84
+ configuration.logger&.debug("SOAP Request:\n#{xml}")
85
+ end
86
+
87
+ def log_response(response)
88
+ return unless configuration.log_level == :debug
89
+
90
+ configuration.logger&.debug(
91
+ "SOAP Response (raw):\n#{response.body}"
92
+ )
93
+ end
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nfcom
4
+ module Webservices
5
+ # Consulta situação de uma NFCom na SEFAZ
6
+ #
7
+ # Implementa a operação "Consulta Protocolo", utilizada para verificar
8
+ # a situação de uma NFCom já transmitida.
9
+ class Consulta < Base
10
+ # Consulta a situação de uma NFCom pela chave de acesso
11
+ #
12
+ # @param chave_acesso [String] Chave de acesso da NFCom (44 dígitos)
13
+ # @return [String] Resposta SOAP bruta da SEFAZ
14
+ # @raise [Errors::ConfigurationError] se a URL não estiver configurada
15
+ # @raise [Errors::SefazError] se houver erro na comunicação
16
+ def consultar(chave_acesso)
17
+ url = url_consulta!
18
+
19
+ body_xml = build_consulta_body(chave_acesso)
20
+ envelope = montar_envelope(body_xml)
21
+
22
+ post_soap(
23
+ url: url,
24
+ action: soap_action,
25
+ xml: envelope
26
+ )
27
+ rescue StandardError => e
28
+ configuration.logger&.error("Erro ao consultar NFCom: #{e.message}")
29
+ raise
30
+ end
31
+
32
+ private
33
+
34
+ def url_consulta!
35
+ configuration.webservice_url(:consulta) ||
36
+ raise(
37
+ Errors::ConfigurationError,
38
+ "URL de consulta não configurada para #{configuration.estado}"
39
+ )
40
+ end
41
+
42
+ def soap_action
43
+ 'http://www.portalfiscal.inf.br/nfcom/wsdl/NFComConsulta/nfcomConsultaNF'
44
+ end
45
+
46
+ def build_consulta_body(chave_acesso)
47
+ <<~XML.strip
48
+ <nfcomDadosMsg xmlns="http://www.portalfiscal.inf.br/nfcom/wsdl/NFComConsulta">
49
+ <consSitNFCom xmlns="http://www.portalfiscal.inf.br/nfcom" versao="1.00">
50
+ <tpAmb>#{configuration.ambiente_codigo}</tpAmb>
51
+ <xServ>CONSULTAR</xServ>
52
+ <chNFCom>#{chave_acesso}</chNFCom>
53
+ </consSitNFCom>
54
+ </nfcomDadosMsg>
55
+ XML
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nfcom
4
+ module Webservices
5
+ class Inutilizacao < Base
6
+ # Solicita inutilização de faixa de numeração de NFCom
7
+ #
8
+ # @return [String] XML SOAP bruto retornado pela SEFAZ
9
+ def inutilizar(serie:, numero_inicial:, numero_final:, justificativa:)
10
+ url = url_inutilizacao!
11
+
12
+ body_xml = build_inutilizacao_body(
13
+ serie: serie,
14
+ numero_inicial: numero_inicial,
15
+ numero_final: numero_final,
16
+ justificativa: justificativa
17
+ )
18
+
19
+ envelope = montar_envelope(body_xml)
20
+
21
+ post_soap(
22
+ url: url,
23
+ action: soap_action,
24
+ xml: envelope
25
+ )
26
+ rescue StandardError => e
27
+ configuration.logger&.error("Erro ao inutilizar NFCom: #{e.message}")
28
+ raise
29
+ end
30
+
31
+ private
32
+
33
+ def url_inutilizacao!
34
+ configuration.webservice_url(:inutilizacao) ||
35
+ raise(
36
+ Errors::ConfigurationError,
37
+ "URL de inutilização não configurada para #{configuration.estado}"
38
+ )
39
+ end
40
+
41
+ def soap_action
42
+ 'http://www.portalfiscal.inf.br/nfcom/wsdl/nfcomInutilizacao'
43
+ end
44
+
45
+ # Monta o XML da inutilização conforme schema NFCom
46
+ #
47
+ # @return [String]
48
+ def build_inutilizacao_body(serie:, numero_inicial:, numero_final:, justificativa:)
49
+ <<~XML
50
+ <nfcomInutilizacaoNF xmlns="http://www.portalfiscal.inf.br/nfcom/wsdl/nfcomInutilizacao">
51
+ <NFComDadosMsg>
52
+ <inutNFCom xmlns="http://www.portalfiscal.inf.br/nfcom" versao="1.00">
53
+ <infInut>
54
+ <tpAmb>#{configuration.ambiente_codigo}</tpAmb>
55
+ <cUF>#{configuration.codigo_uf}</cUF>
56
+ <ano>#{Time.now.strftime('%y')}</ano>
57
+ <CNPJ>#{configuration.cnpj.gsub(/\D/, '')}</CNPJ>
58
+ <mod>62</mod>
59
+ <serie>#{serie}</serie>
60
+ <nNFIni>#{numero_inicial}</nNFIni>
61
+ <nNFFin>#{numero_final}</nNFFin>
62
+ <xJust>#{justificativa}</xJust>
63
+ </infInut>
64
+ </inutNFCom>
65
+ </NFComDadosMsg>
66
+ </nfcomInutilizacaoNF>
67
+ XML
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Nfcom
4
+ module Webservices
5
+ # Consulta o status do serviço NFCom na SEFAZ
6
+ #
7
+ # Implementa a operação "Status do Serviço", utilizada para verificar
8
+ # se o ambiente da SEFAZ está disponível.
9
+ class Status < Base
10
+ # Executa a consulta de status do serviço NFCom
11
+ #
12
+ # @return [String] Resposta SOAP bruta da SEFAZ
13
+ # @raise [Errors::ConfigurationError] se a URL não estiver configurada
14
+ # @raise [Errors::SefazError] se houver erro na comunicação
15
+ def verificar
16
+ url = url_status!
17
+
18
+ body_xml = build_status_body
19
+ envelope = montar_envelope(body_xml)
20
+
21
+ post_soap(
22
+ url: url,
23
+ action: soap_action,
24
+ xml: envelope
25
+ )
26
+ rescue StandardError => e
27
+ configuration.logger&.error("Erro ao consultar Status NFCom: #{e.message}")
28
+ raise
29
+ end
30
+
31
+ private
32
+
33
+ def url_status!
34
+ configuration.webservice_url(:status) ||
35
+ raise(
36
+ Errors::ConfigurationError,
37
+ "URL de status não configurada para #{configuration.estado}"
38
+ )
39
+ end
40
+
41
+ def soap_action
42
+ 'http://www.portalfiscal.inf.br/nfcom/wsdl/NFComStatusServico/nfcomStatusServicoNF'
43
+ end
44
+
45
+ # Monta o XML da consulta de status do serviço
46
+ #
47
+ # Importante:
48
+ # - A mensagem NÃO deve ser compactada
49
+ # - Deve seguir exatamente o schema NFCom v1.00
50
+ #
51
+ # @return [String]
52
+ def build_status_body
53
+ <<~XML.strip
54
+ <nfcomDadosMsg xmlns="http://www.portalfiscal.inf.br/nfcom/wsdl/NFComStatusServico">
55
+ <consStatServNFCom xmlns="http://www.portalfiscal.inf.br/nfcom" versao="1.00">
56
+ <tpAmb>#{configuration.ambiente_codigo}</tpAmb>
57
+ <xServ>STATUS</xServ>
58
+ </consStatServNFCom>
59
+ </nfcomDadosMsg>
60
+ XML
61
+ end
62
+ end
63
+ end
64
+ end