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.
- checksums.yaml +7 -0
- data/.rubocop.yml +115 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +66 -0
- data/LICENSE +21 -0
- data/README.md +280 -0
- data/Rakefile +22 -0
- data/examples/.env.example +41 -0
- data/examples/emitir_nota.rb +91 -0
- data/examples/rails_initializer.rb +72 -0
- data/lib/nfcom/builder/danfe_com.rb +564 -0
- data/lib/nfcom/builder/qrcode.rb +68 -0
- data/lib/nfcom/builder/signature.rb +156 -0
- data/lib/nfcom/builder/xml_builder.rb +362 -0
- data/lib/nfcom/client.rb +106 -0
- data/lib/nfcom/configuration.rb +134 -0
- data/lib/nfcom/errors.rb +27 -0
- data/lib/nfcom/helpers/consulta.rb +28 -0
- data/lib/nfcom/models/assinante.rb +146 -0
- data/lib/nfcom/models/destinatario.rb +138 -0
- data/lib/nfcom/models/emitente.rb +105 -0
- data/lib/nfcom/models/endereco.rb +123 -0
- data/lib/nfcom/models/fatura/codigo_de_barras/formato_44.rb +52 -0
- data/lib/nfcom/models/fatura/codigo_de_barras.rb +57 -0
- data/lib/nfcom/models/fatura.rb +172 -0
- data/lib/nfcom/models/item.rb +353 -0
- data/lib/nfcom/models/nota.rb +398 -0
- data/lib/nfcom/models/total.rb +60 -0
- data/lib/nfcom/parsers/autorizacao.rb +28 -0
- data/lib/nfcom/parsers/base.rb +30 -0
- data/lib/nfcom/parsers/consulta.rb +34 -0
- data/lib/nfcom/parsers/inutilizacao.rb +23 -0
- data/lib/nfcom/parsers/status.rb +23 -0
- data/lib/nfcom/utils/certificate.rb +109 -0
- data/lib/nfcom/utils/compressor.rb +47 -0
- data/lib/nfcom/utils/helpers.rb +141 -0
- data/lib/nfcom/utils/response_decompressor.rb +47 -0
- data/lib/nfcom/utils/xml_authorized.rb +29 -0
- data/lib/nfcom/utils/xml_cleaner.rb +68 -0
- data/lib/nfcom/validators/business_rules.rb +45 -0
- data/lib/nfcom/validators/schema_validator.rb +316 -0
- data/lib/nfcom/validators/xml_validator.rb +29 -0
- data/lib/nfcom/version.rb +5 -0
- data/lib/nfcom/webservices/autorizacao.rb +36 -0
- data/lib/nfcom/webservices/base.rb +96 -0
- data/lib/nfcom/webservices/consulta.rb +59 -0
- data/lib/nfcom/webservices/inutilizacao.rb +71 -0
- data/lib/nfcom/webservices/status.rb +64 -0
- data/lib/nfcom.rb +98 -0
- data/nfcom.gemspec +42 -0
- 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,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
|