ghinfe 0.6.0 → 0.6.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 (110) hide show
  1. package/.env.example +24 -15
  2. package/LICENSE +21 -21
  3. package/README.md +491 -413
  4. package/docs/developer-guide.html +421 -421
  5. package/docs/openapi.yaml +428 -428
  6. package/examples/dce-homologacao.js +141 -0
  7. package/examples/emitir-nfce.js +51 -51
  8. package/examples/emitir-nfe.js +83 -83
  9. package/examples/emitir-nfse.js +46 -46
  10. package/examples/endpoints-sefaz.js +14 -14
  11. package/examples/eventos-nfe.js +49 -49
  12. package/examples/gerar-chave.js +25 -25
  13. package/examples/gerar-dace.js +59 -0
  14. package/examples/gerar-danfe.js +75 -75
  15. package/examples/gerar-dce.js +58 -0
  16. package/examples/layouts-nfse.js +51 -51
  17. package/examples/mock-sefaz.js +52 -52
  18. package/examples/nfse-eventos.js +57 -57
  19. package/examples/reforma-ibscbs.js +27 -27
  20. package/examples/reforma-nfe-xml.js +52 -52
  21. package/examples/status-sefaz.js +12 -12
  22. package/examples/validar-xsd.js +48 -48
  23. package/package.json +59 -68
  24. package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/consSitDCeTiposBasico_v1.00.xsd +123 -0
  25. package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/consSitDCe_v1.00.xsd +10 -0
  26. package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/consStatServDCeTiposBasico_v1.00.xsd +110 -0
  27. package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/consStatServDCe_v1.00.xsd +10 -0
  28. package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/dceTiposBasico_v1.00.xsd +1388 -0
  29. package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/retConsSitDCe_v1.00.xsd +10 -0
  30. package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/retConsStatServDCe_v1.00.xsd +10 -0
  31. package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/tiposGeralDCe_v1.00.xsd +712 -0
  32. package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/xmldsig-core-schema_v1.01.xsd +98 -0
  33. package/schemas/dce/PL_DCe_100a_NT2024.001/dceTiposBasico_v1.00.xsd +1410 -0
  34. package/schemas/dce/PL_DCe_100a_NT2024.001/dce_v1.00.xsd +10 -0
  35. package/schemas/dce/PL_DCe_100a_NT2024.001/evento/evCancDCe_v1.00.xsd +36 -0
  36. package/schemas/dce/PL_DCe_100a_NT2024.001/evento/eventoDCeTiposBasico_v1.00.xsd +283 -0
  37. package/schemas/dce/PL_DCe_100a_NT2024.001/evento/eventoDCe_v1.00.xsd +9 -0
  38. package/schemas/dce/PL_DCe_100a_NT2024.001/evento/procEventoDCe_v1.00.xsd +10 -0
  39. package/schemas/dce/PL_DCe_100a_NT2024.001/evento/retEventoDCe_v1.00.xsd +9 -0
  40. package/schemas/dce/PL_DCe_100a_NT2024.001/evento/tiposGeralDCe_v1.00.xsd +712 -0
  41. package/schemas/dce/PL_DCe_100a_NT2024.001/evento/xmldsig-core-schema_v1.01.xsd +98 -0
  42. package/schemas/dce/PL_DCe_100a_NT2024.001/procDCe_v1.00.xsd +37 -0
  43. package/schemas/dce/PL_DCe_100a_NT2024.001/retDCe_v1.00.xsd +9 -0
  44. package/schemas/dce/PL_DCe_100a_NT2024.001/tiposGeralDCe_v1.00.xsd +712 -0
  45. package/schemas/dce/PL_DCe_100a_NT2024.001/xmldsig-core-schema_v1.01.xsd +98 -0
  46. package/schemas/nfe/PL_009_V4/enviNFe_v3.10.xsd +8 -8
  47. package/schemas/nfe/PL_009_V4/xmldsig-core-schema_v1.01.xsd +97 -97
  48. package/scripts/download-schemas.js +39 -39
  49. package/scripts/test-xsd.js +34 -34
  50. package/src/api/server.js +316 -316
  51. package/src/constants/sefaz.js +69 -69
  52. package/src/danfe/DanfeService.js +205 -205
  53. package/src/danfe/xml-extract.js +69 -69
  54. package/src/dce/builders/DCeEventoXmlBuilder.js +86 -0
  55. package/src/dce/builders/DCeXmlBuilder.js +345 -0
  56. package/src/dce/constants/dce.js +87 -0
  57. package/src/dce/dace/DaceService.js +356 -0
  58. package/src/dce/dace/dace-extract.js +114 -0
  59. package/src/dce/services/DCeService.js +263 -0
  60. package/src/dce/services/DCeSoapService.js +235 -0
  61. package/src/dce/services/DCeXmlService.js +67 -0
  62. package/src/dce/types/DCeDocument.js +106 -0
  63. package/src/dce/utils/qrcode.utils.js +59 -0
  64. package/src/errors/GhinfeError.js +13 -13
  65. package/src/errors/SefazError.js +25 -25
  66. package/src/errors/XsdValidationError.js +15 -15
  67. package/src/index.js +84 -60
  68. package/src/nfce/builders/NFCeXmlBuilder.js +178 -178
  69. package/src/nfce/constants/qrcode.js +26 -26
  70. package/src/nfce/services/NFCeService.js +188 -188
  71. package/src/nfce/services/NFCeXmlService.js +38 -38
  72. package/src/nfce/types/NFCeDocument.js +42 -42
  73. package/src/nfce/utils/qrcode.utils.js +92 -92
  74. package/src/nfe/builders/NFeEventoXmlBuilder.js +110 -110
  75. package/src/nfe/builders/NFeXmlBuilder.js +215 -215
  76. package/src/nfe/mocks/sefaz-responses.js +195 -195
  77. package/src/nfe/services/NFeEventoService.js +95 -95
  78. package/src/nfe/services/NFeService.js +196 -196
  79. package/src/nfe/services/NFeSignService.js +127 -111
  80. package/src/nfe/services/NFeSoapService.js +437 -437
  81. package/src/nfe/services/NFeXmlService.js +41 -41
  82. package/src/nfe/services/NFeXsdService.js +56 -56
  83. package/src/nfe/types/NFeDocument.js +92 -92
  84. package/src/nfe/types/NFeEvento.js +44 -44
  85. package/src/nfse/NFSeMunicipioResolver.js +42 -42
  86. package/src/nfse/builders/NFSeEventoXmlBuilder.js +92 -92
  87. package/src/nfse/builders/NFSeXmlBuilder.js +16 -16
  88. package/src/nfse/constants/municipios.js +82 -82
  89. package/src/nfse/layouts/adapters.js +202 -202
  90. package/src/nfse/mocks/nfse-responses.js +142 -142
  91. package/src/nfse/services/NFSeService.js +191 -191
  92. package/src/nfse/services/NFSeSoapService.js +232 -232
  93. package/src/nfse/services/NFSeXmlService.js +12 -12
  94. package/src/nfse/types/NFSeDocument.js +54 -54
  95. package/src/queue/NFCeQueue.js +63 -63
  96. package/src/queue/NFSeQueue.js +59 -59
  97. package/src/queue/NFeQueue.js +76 -76
  98. package/src/reforma/ibscbs.js +175 -175
  99. package/src/sefaz/SefazEndpointResolver.js +95 -95
  100. package/src/sefaz/ca/icp-brasil-raiz-v10.pem +38 -0
  101. package/src/sefaz/endpoints.registry.js +205 -205
  102. package/src/sefaz/httpsAgent.js +33 -0
  103. package/src/shared/fiscal.helpers.js +53 -53
  104. package/src/utils/certificate.utils.js +69 -47
  105. package/src/utils/chave.utils.js +205 -117
  106. package/src/utils/code128.utils.js +67 -0
  107. package/src/utils/date.utils.js +26 -26
  108. package/src/utils/xml.utils.js +44 -44
  109. package/src/validation/XsdValidator.js +133 -114
  110. package/src/validation/schemas.js +18 -14
@@ -1,437 +1,437 @@
1
- import axios from 'axios';
2
- import { SEFAZ_STATUS } from '../../constants/sefaz.js';
3
- import { SefazError } from '../../errors/SefazError.js';
4
- import { GhinfeError } from '../../errors/GhinfeError.js';
5
- import {
6
- SEFAZ_MOCK_RESPONSES,
7
- buildMockProtNFeXml,
8
- buildMockRetEnvEventoXml,
9
- buildMockRetConsStatServXml,
10
- wrapSoapResponse,
11
- wrapSoapEventoResponse,
12
- wrapSoapStatusResponse,
13
- } from '../mocks/sefaz-responses.js';
14
- import { UF_IBGE } from '../../constants/sefaz.js';
15
- import { SEFAZ_SERVICO, SefazEndpointResolver } from '../../sefaz/SefazEndpointResolver.js';
16
-
17
- /**
18
- * Extrai cStat e xMotivo de um XML de retorno SEFAZ.
19
- * @param {string} xml
20
- */
21
- function parseRetorno(xml) {
22
- const cStatMatch = xml.match(/<cStat>(\d+)<\/cStat>/);
23
- const xMotivoMatch = xml.match(/<xMotivo>([^<]+)<\/xMotivo>/);
24
- const nProtMatch = xml.match(/<nProt>(\d+)<\/nProt>/);
25
- const chNFeMatch = xml.match(/<chNFe>(\d{44})<\/chNFe>/);
26
-
27
- return {
28
- cStat: cStatMatch?.[1] ?? '999',
29
- xMotivo: xMotivoMatch?.[1] ?? 'Retorno não identificado',
30
- nProt: nProtMatch?.[1],
31
- chNFe: chNFeMatch?.[1],
32
- xmlRetorno: xml,
33
- };
34
- }
35
-
36
- /**
37
- * Serviço de comunicação SOAP com a SEFAZ.
38
- * Suporta modo mock para desenvolvimento sem certificado/homologação.
39
- */
40
- export class NFeSoapService {
41
- /**
42
- * @param {{ mock?: boolean, endpoint?: string, eventoEndpoint?: string, timeout?: number, mockScenario?: string, mockEventoScenario?: string }} [options]
43
- */
44
- constructor(options = {}) {
45
- this.mock = options.mock ?? true;
46
- this.endpoint = options.endpoint;
47
- this.eventoEndpoint = options.eventoEndpoint;
48
- this.statusEndpoint = options.statusEndpoint;
49
- this.timeout = options.timeout ?? 30000;
50
- this.mockScenario = options.mockScenario ?? 'autorizado';
51
- this.mockEventoScenario = options.mockEventoScenario ?? 'registrado';
52
- this.mockStatusScenario = options.mockStatusScenario ?? 'ok';
53
- }
54
-
55
- /**
56
- * Monta envelope SOAP para NFeAutorizacao4.
57
- * @param {string} xmlNFeAssinado
58
- * @param {string} idLote
59
- */
60
- buildAutorizacaoEnvelope(xmlNFeAssinado, idLote) {
61
- const nfeBase64 = Buffer.from(xmlNFeAssinado, 'utf-8').toString('base64');
62
-
63
- return `<?xml version="1.0" encoding="utf-8"?>
64
- <soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
65
- xmlns:xsd="http://www.w3.org/2001/XMLSchema"
66
- xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
67
- <soap12:Body>
68
- <nfeDadosMsg xmlns="http://www.portalfiscal.inf.br/nfe/wsdl/NFeAutorizacao4">
69
- <enviNFe versao="4.00" xmlns="http://www.portalfiscal.inf.br/nfe">
70
- <idLote>${idLote}</idLote>
71
- <indSinc>1</indSinc>
72
- <NFe>${nfeBase64}</NFe>
73
- </enviNFe>
74
- </nfeDadosMsg>
75
- </soap12:Body>
76
- </soap12:Envelope>`;
77
- }
78
-
79
- /**
80
- * Envia NF-e para autorização na SEFAZ (ou mock).
81
- * @param {string} xmlNFeAssinado
82
- * @param {string} chaveAcesso
83
- * @param {string} [idLote]
84
- */
85
- async autorizar(xmlNFeAssinado, chaveAcesso, idLote = '1') {
86
- if (this.mock) {
87
- return this.#mockAutorizar(chaveAcesso);
88
- }
89
-
90
- if (!this.endpoint) {
91
- throw new GhinfeError(
92
- 'Endpoint SEFAZ não configurado. Defina options.endpoint ou use mock: true',
93
- 'SEFAZ_ENDPOINT_MISSING'
94
- );
95
- }
96
-
97
- const envelope = this.buildAutorizacaoEnvelope(xmlNFeAssinado, idLote);
98
-
99
- try {
100
- const response = await axios.post(this.endpoint, envelope, {
101
- headers: {
102
- 'Content-Type': 'application/soap+xml; charset=utf-8',
103
- },
104
- timeout: this.timeout,
105
- responseType: 'text',
106
- });
107
-
108
- const bodyMatch = response.data.match(/<nfeResultMsg[^>]*>([\s\S]*?)<\/nfeResultMsg>/);
109
- const innerXml = bodyMatch?.[1] ?? response.data;
110
- return this.#processarRetorno(innerXml);
111
- } catch (error) {
112
- if (axios.isAxiosError(error)) {
113
- throw new GhinfeError(
114
- `Falha na comunicação SOAP com SEFAZ: ${error.message}`,
115
- 'SEFAZ_HTTP_ERROR',
116
- { status: error.response?.status, data: error.response?.data }
117
- );
118
- }
119
- throw error;
120
- }
121
- }
122
-
123
- /**
124
- * Simula envio à SEFAZ com cenários configuráveis.
125
- * @param {string} chaveAcesso
126
- */
127
- async #mockAutorizar(chaveAcesso) {
128
- await new Promise((r) => setTimeout(r, 300));
129
-
130
- const scenarioMap = {
131
- autorizado: SEFAZ_MOCK_RESPONSES.autorizado,
132
- duplicidade: SEFAZ_MOCK_RESPONSES.rejeicaoDuplicidade,
133
- schema: SEFAZ_MOCK_RESPONSES.rejeicaoSchema,
134
- certificado: SEFAZ_MOCK_RESPONSES.rejeicaoCertificado,
135
- paralisado: SEFAZ_MOCK_RESPONSES.servicoParalisado,
136
- };
137
-
138
- const resultado = scenarioMap[this.mockScenario] ?? SEFAZ_MOCK_RESPONSES.autorizado;
139
- const innerXml = buildMockProtNFeXml(chaveAcesso, resultado);
140
- const soapXml = wrapSoapResponse(innerXml);
141
-
142
- return this.#processarRetorno(innerXml, { soapXml, mock: true });
143
- }
144
-
145
- /**
146
- * @param {string} xml
147
- * @param {Record<string, unknown>} [meta]
148
- */
149
- #processarRetorno(xml, meta = {}) {
150
- const retorno = parseRetorno(xml);
151
-
152
- if (retorno.cStat === SEFAZ_STATUS.AUTORIZADO) {
153
- return {
154
- sucesso: true,
155
- autorizado: true,
156
- cStat: retorno.cStat,
157
- xMotivo: retorno.xMotivo,
158
- protocolo: retorno.nProt,
159
- chaveAcesso: retorno.chNFe,
160
- xmlProtocolo: xml,
161
- ...meta,
162
- };
163
- }
164
-
165
- if (retorno.cStat.startsWith('1')) {
166
- return {
167
- sucesso: true,
168
- autorizado: false,
169
- cStat: retorno.cStat,
170
- xMotivo: retorno.xMotivo,
171
- xmlProtocolo: xml,
172
- ...meta,
173
- };
174
- }
175
-
176
- throw new SefazError(retorno.cStat, retorno.xMotivo, { xmlRetorno: xml, ...meta });
177
- }
178
-
179
- /**
180
- * Define cenário do mock em runtime.
181
- * @param {'autorizado'|'duplicidade'|'schema'|'certificado'|'paralisado'} scenario
182
- */
183
- setMockScenario(scenario) {
184
- this.mockScenario = scenario;
185
- }
186
-
187
- /**
188
- * Monta envelope SOAP para NFeRecepcaoEvento4.
189
- * @param {string} xmlEventoAssinado
190
- * @param {string} idLote
191
- */
192
- buildEventoEnvelope(xmlEventoAssinado, idLote = '1') {
193
- return `<?xml version="1.0" encoding="utf-8"?>
194
- <soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
195
- xmlns:xsd="http://www.w3.org/2001/XMLSchema"
196
- xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
197
- <soap12:Body>
198
- <nfeDadosMsg xmlns="http://www.portalfiscal.inf.br/nfe/wsdl/NFeRecepcaoEvento4">
199
- ${xmlEventoAssinado}
200
- </nfeDadosMsg>
201
- </soap12:Body>
202
- </soap12:Envelope>`;
203
- }
204
-
205
- /**
206
- * Envia evento NF-e (cancelamento, CC-e) para SEFAZ.
207
- * @param {string} xmlEventoAssinado
208
- * @param {string} chaveAcesso
209
- * @param {string} tpEvento
210
- * @param {string} [idLote]
211
- */
212
- async enviarEvento(xmlEventoAssinado, chaveAcesso, tpEvento, idLote = '1') {
213
- if (this.mock) {
214
- return this.#mockEnviarEvento(chaveAcesso, tpEvento);
215
- }
216
-
217
- const endpoint = this.eventoEndpoint ?? this.endpoint;
218
- if (!endpoint) {
219
- throw new GhinfeError(
220
- 'Endpoint SEFAZ de eventos não configurado. Defina eventoEndpoint ou use mock: true',
221
- 'SEFAZ_EVENTO_ENDPOINT_MISSING'
222
- );
223
- }
224
-
225
- const envelope = this.buildEventoEnvelope(xmlEventoAssinado, idLote);
226
-
227
- try {
228
- const response = await axios.post(endpoint, envelope, {
229
- headers: { 'Content-Type': 'application/soap+xml; charset=utf-8' },
230
- timeout: this.timeout,
231
- responseType: 'text',
232
- });
233
-
234
- const bodyMatch = response.data.match(/<nfeResultMsg[^>]*>([\s\S]*?)<\/nfeResultMsg>/);
235
- const innerXml = bodyMatch?.[1] ?? response.data;
236
- return this.#processarRetornoEvento(innerXml);
237
- } catch (error) {
238
- if (axios.isAxiosError(error)) {
239
- throw new GhinfeError(
240
- `Falha na comunicação SOAP (evento) com SEFAZ: ${error.message}`,
241
- 'SEFAZ_HTTP_ERROR',
242
- { status: error.response?.status, data: error.response?.data }
243
- );
244
- }
245
- throw error;
246
- }
247
- }
248
-
249
- /**
250
- * @param {string} chaveAcesso
251
- * @param {string} tpEvento
252
- */
253
- async #mockEnviarEvento(chaveAcesso, tpEvento) {
254
- await new Promise((r) => setTimeout(r, 200));
255
-
256
- const scenarioMap = {
257
- registrado: SEFAZ_MOCK_RESPONSES.eventoRegistrado,
258
- rejeitado: SEFAZ_MOCK_RESPONSES.eventoRejeitado,
259
- };
260
-
261
- const resultado = scenarioMap[this.mockEventoScenario] ?? SEFAZ_MOCK_RESPONSES.eventoRegistrado;
262
- const innerXml = buildMockRetEnvEventoXml(chaveAcesso, tpEvento, resultado);
263
- const soapXml = wrapSoapEventoResponse(innerXml);
264
-
265
- return this.#processarRetornoEvento(innerXml, { soapXml, mock: true });
266
- }
267
-
268
- /**
269
- * @param {string} xml
270
- * @param {Record<string, unknown>} [meta]
271
- */
272
- #processarRetornoEvento(xml, meta = {}) {
273
- const cStatMatches = [...xml.matchAll(/<cStat>(\d+)<\/cStat>/g)];
274
- const xMotivoMatches = [...xml.matchAll(/<xMotivo>([^<]+)<\/xMotivo>/g)];
275
- const nProtMatch = xml.match(/<nProt>(\d+)<\/nProt>/);
276
- const tpEventoMatch = xml.match(/<tpEvento>(\d+)<\/tpEvento>/);
277
-
278
- const cStatLote = cStatMatches[0]?.[1] ?? '999';
279
- const cStatEvento = cStatMatches[1]?.[1] ?? cStatLote;
280
- const xMotivo = xMotivoMatches.at(-1)?.[1] ?? 'Retorno não identificado';
281
-
282
- if (cStatEvento === SEFAZ_STATUS.EVENTO_REGISTRADO) {
283
- return {
284
- sucesso: true,
285
- registrado: true,
286
- cStat: cStatEvento,
287
- xMotivo,
288
- protocolo: nProtMatch?.[1],
289
- tpEvento: tpEventoMatch?.[1],
290
- xmlRetorno: xml,
291
- ...meta,
292
- };
293
- }
294
-
295
- if (cStatLote.startsWith('1') && cStatEvento.startsWith('1')) {
296
- return {
297
- sucesso: true,
298
- registrado: false,
299
- cStat: cStatEvento,
300
- xMotivo,
301
- xmlRetorno: xml,
302
- ...meta,
303
- };
304
- }
305
-
306
- throw new SefazError(cStatEvento, xMotivo, { xmlRetorno: xml, ...meta });
307
- }
308
-
309
- /**
310
- * @param {'registrado'|'rejeitado'} scenario
311
- */
312
- setMockEventoScenario(scenario) {
313
- this.mockEventoScenario = scenario;
314
- }
315
-
316
- /**
317
- * Monta XML de consulta de status do serviço.
318
- * @param {string} uf
319
- * @param {number} [ambiente]
320
- */
321
- buildStatusServicoXml(uf, ambiente = 2) {
322
- const cUF = UF_IBGE[uf?.toUpperCase()] ?? '35';
323
- return `<?xml version="1.0" encoding="UTF-8"?>
324
- <consStatServ xmlns="http://www.portalfiscal.inf.br/nfe" versao="4.00">
325
- <tpAmb>${ambiente}</tpAmb>
326
- <cUF>${cUF}</cUF>
327
- <xServ>STATUS</xServ>
328
- </consStatServ>`;
329
- }
330
-
331
- /**
332
- * Consulta status do webservice SEFAZ (NfeStatusServico4).
333
- * @param {string} uf
334
- * @param {number} [ambiente]
335
- */
336
- async consultarStatus(uf, ambiente = 2) {
337
- if (this.mock) {
338
- return this.#mockConsultarStatus(uf);
339
- }
340
-
341
- const statusEndpoint =
342
- this.statusEndpoint ??
343
- (() => {
344
- try {
345
- return SefazEndpointResolver.resolver({
346
- uf,
347
- ambiente,
348
- servico: SEFAZ_SERVICO.STATUS,
349
- });
350
- } catch {
351
- return null;
352
- }
353
- })();
354
-
355
- if (!statusEndpoint) {
356
- throw new GhinfeError(
357
- `Endpoint de status SEFAZ não disponível para UF ${uf}`,
358
- 'SEFAZ_STATUS_ENDPOINT_MISSING'
359
- );
360
- }
361
-
362
- const dados = this.buildStatusServicoXml(uf, ambiente);
363
- const envelope = `<?xml version="1.0" encoding="utf-8"?>
364
- <soap12:Envelope xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
365
- <soap12:Body>
366
- <nfeDadosMsg xmlns="http://www.portalfiscal.inf.br/nfe/wsdl/NFeStatusServico4">
367
- ${dados}
368
- </nfeDadosMsg>
369
- </soap12:Body>
370
- </soap12:Envelope>`;
371
-
372
- try {
373
- const response = await axios.post(statusEndpoint, envelope, {
374
- headers: { 'Content-Type': 'application/soap+xml; charset=utf-8' },
375
- timeout: this.timeout,
376
- responseType: 'text',
377
- });
378
- const bodyMatch = response.data.match(/<nfeResultMsg[^>]*>([\s\S]*?)<\/nfeResultMsg>/);
379
- return this.#processarRetornoStatus(bodyMatch?.[1] ?? response.data);
380
- } catch (error) {
381
- if (axios.isAxiosError(error)) {
382
- throw new GhinfeError(
383
- `Falha na consulta de status SEFAZ: ${error.message}`,
384
- 'SEFAZ_HTTP_ERROR',
385
- { status: error.response?.status }
386
- );
387
- }
388
- throw error;
389
- }
390
- }
391
-
392
- /** @param {string} uf */
393
- async #mockConsultarStatus(uf) {
394
- await new Promise((r) => setTimeout(r, 150));
395
- const scenarioMap = {
396
- ok: SEFAZ_MOCK_RESPONSES.statusOk,
397
- paralisado: SEFAZ_MOCK_RESPONSES.statusParalisado,
398
- };
399
- const resultado = scenarioMap[this.mockStatusScenario ?? 'ok'] ?? SEFAZ_MOCK_RESPONSES.statusOk;
400
- const cUF = UF_IBGE[uf?.toUpperCase()] ?? '35';
401
- const innerXml = buildMockRetConsStatServXml(resultado, cUF);
402
- return this.#processarRetornoStatus(innerXml, {
403
- soapXml: wrapSoapStatusResponse(innerXml),
404
- mock: true,
405
- });
406
- }
407
-
408
- /**
409
- * @param {string} xml
410
- * @param {Record<string, unknown>} [meta]
411
- */
412
- #processarRetornoStatus(xml, meta = {}) {
413
- const cStat = xml.match(/<cStat>(\d+)<\/cStat>/)?.[1] ?? '999';
414
- const xMotivo = xml.match(/<xMotivo>([^<]+)<\/xMotivo>/)?.[1] ?? 'Retorno não identificado';
415
- const tMed = xml.match(/<tMed>([^<]+)<\/tMed>/)?.[1];
416
- const cUF = xml.match(/<cUF>(\d+)<\/cUF>/)?.[1];
417
- const emOperacao = cStat === SEFAZ_STATUS.SERVICO_EM_OPERACAO;
418
-
419
- return {
420
- sucesso: emOperacao,
421
- emOperacao,
422
- cStat,
423
- xMotivo,
424
- tMed,
425
- cUF,
426
- xmlRetorno: xml,
427
- ...meta,
428
- };
429
- }
430
-
431
- /**
432
- * @param {'ok'|'paralisado'} scenario
433
- */
434
- setMockStatusScenario(scenario) {
435
- this.mockStatusScenario = scenario;
436
- }
437
- }
1
+ import axios from 'axios';
2
+ import { SEFAZ_STATUS } from '../../constants/sefaz.js';
3
+ import { SefazError } from '../../errors/SefazError.js';
4
+ import { GhinfeError } from '../../errors/GhinfeError.js';
5
+ import {
6
+ SEFAZ_MOCK_RESPONSES,
7
+ buildMockProtNFeXml,
8
+ buildMockRetEnvEventoXml,
9
+ buildMockRetConsStatServXml,
10
+ wrapSoapResponse,
11
+ wrapSoapEventoResponse,
12
+ wrapSoapStatusResponse,
13
+ } from '../mocks/sefaz-responses.js';
14
+ import { UF_IBGE } from '../../constants/sefaz.js';
15
+ import { SEFAZ_SERVICO, SefazEndpointResolver } from '../../sefaz/SefazEndpointResolver.js';
16
+
17
+ /**
18
+ * Extrai cStat e xMotivo de um XML de retorno SEFAZ.
19
+ * @param {string} xml
20
+ */
21
+ function parseRetorno(xml) {
22
+ const cStatMatch = xml.match(/<cStat>(\d+)<\/cStat>/);
23
+ const xMotivoMatch = xml.match(/<xMotivo>([^<]+)<\/xMotivo>/);
24
+ const nProtMatch = xml.match(/<nProt>(\d+)<\/nProt>/);
25
+ const chNFeMatch = xml.match(/<chNFe>(\d{44})<\/chNFe>/);
26
+
27
+ return {
28
+ cStat: cStatMatch?.[1] ?? '999',
29
+ xMotivo: xMotivoMatch?.[1] ?? 'Retorno não identificado',
30
+ nProt: nProtMatch?.[1],
31
+ chNFe: chNFeMatch?.[1],
32
+ xmlRetorno: xml,
33
+ };
34
+ }
35
+
36
+ /**
37
+ * Serviço de comunicação SOAP com a SEFAZ.
38
+ * Suporta modo mock para desenvolvimento sem certificado/homologação.
39
+ */
40
+ export class NFeSoapService {
41
+ /**
42
+ * @param {{ mock?: boolean, endpoint?: string, eventoEndpoint?: string, timeout?: number, mockScenario?: string, mockEventoScenario?: string }} [options]
43
+ */
44
+ constructor(options = {}) {
45
+ this.mock = options.mock ?? true;
46
+ this.endpoint = options.endpoint;
47
+ this.eventoEndpoint = options.eventoEndpoint;
48
+ this.statusEndpoint = options.statusEndpoint;
49
+ this.timeout = options.timeout ?? 30000;
50
+ this.mockScenario = options.mockScenario ?? 'autorizado';
51
+ this.mockEventoScenario = options.mockEventoScenario ?? 'registrado';
52
+ this.mockStatusScenario = options.mockStatusScenario ?? 'ok';
53
+ }
54
+
55
+ /**
56
+ * Monta envelope SOAP para NFeAutorizacao4.
57
+ * @param {string} xmlNFeAssinado
58
+ * @param {string} idLote
59
+ */
60
+ buildAutorizacaoEnvelope(xmlNFeAssinado, idLote) {
61
+ const nfeBase64 = Buffer.from(xmlNFeAssinado, 'utf-8').toString('base64');
62
+
63
+ return `<?xml version="1.0" encoding="utf-8"?>
64
+ <soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
65
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema"
66
+ xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
67
+ <soap12:Body>
68
+ <nfeDadosMsg xmlns="http://www.portalfiscal.inf.br/nfe/wsdl/NFeAutorizacao4">
69
+ <enviNFe versao="4.00" xmlns="http://www.portalfiscal.inf.br/nfe">
70
+ <idLote>${idLote}</idLote>
71
+ <indSinc>1</indSinc>
72
+ <NFe>${nfeBase64}</NFe>
73
+ </enviNFe>
74
+ </nfeDadosMsg>
75
+ </soap12:Body>
76
+ </soap12:Envelope>`;
77
+ }
78
+
79
+ /**
80
+ * Envia NF-e para autorização na SEFAZ (ou mock).
81
+ * @param {string} xmlNFeAssinado
82
+ * @param {string} chaveAcesso
83
+ * @param {string} [idLote]
84
+ */
85
+ async autorizar(xmlNFeAssinado, chaveAcesso, idLote = '1') {
86
+ if (this.mock) {
87
+ return this.#mockAutorizar(chaveAcesso);
88
+ }
89
+
90
+ if (!this.endpoint) {
91
+ throw new GhinfeError(
92
+ 'Endpoint SEFAZ não configurado. Defina options.endpoint ou use mock: true',
93
+ 'SEFAZ_ENDPOINT_MISSING'
94
+ );
95
+ }
96
+
97
+ const envelope = this.buildAutorizacaoEnvelope(xmlNFeAssinado, idLote);
98
+
99
+ try {
100
+ const response = await axios.post(this.endpoint, envelope, {
101
+ headers: {
102
+ 'Content-Type': 'application/soap+xml; charset=utf-8',
103
+ },
104
+ timeout: this.timeout,
105
+ responseType: 'text',
106
+ });
107
+
108
+ const bodyMatch = response.data.match(/<nfeResultMsg[^>]*>([\s\S]*?)<\/nfeResultMsg>/);
109
+ const innerXml = bodyMatch?.[1] ?? response.data;
110
+ return this.#processarRetorno(innerXml);
111
+ } catch (error) {
112
+ if (axios.isAxiosError(error)) {
113
+ throw new GhinfeError(
114
+ `Falha na comunicação SOAP com SEFAZ: ${error.message}`,
115
+ 'SEFAZ_HTTP_ERROR',
116
+ { status: error.response?.status, data: error.response?.data }
117
+ );
118
+ }
119
+ throw error;
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Simula envio à SEFAZ com cenários configuráveis.
125
+ * @param {string} chaveAcesso
126
+ */
127
+ async #mockAutorizar(chaveAcesso) {
128
+ await new Promise((r) => setTimeout(r, 300));
129
+
130
+ const scenarioMap = {
131
+ autorizado: SEFAZ_MOCK_RESPONSES.autorizado,
132
+ duplicidade: SEFAZ_MOCK_RESPONSES.rejeicaoDuplicidade,
133
+ schema: SEFAZ_MOCK_RESPONSES.rejeicaoSchema,
134
+ certificado: SEFAZ_MOCK_RESPONSES.rejeicaoCertificado,
135
+ paralisado: SEFAZ_MOCK_RESPONSES.servicoParalisado,
136
+ };
137
+
138
+ const resultado = scenarioMap[this.mockScenario] ?? SEFAZ_MOCK_RESPONSES.autorizado;
139
+ const innerXml = buildMockProtNFeXml(chaveAcesso, resultado);
140
+ const soapXml = wrapSoapResponse(innerXml);
141
+
142
+ return this.#processarRetorno(innerXml, { soapXml, mock: true });
143
+ }
144
+
145
+ /**
146
+ * @param {string} xml
147
+ * @param {Record<string, unknown>} [meta]
148
+ */
149
+ #processarRetorno(xml, meta = {}) {
150
+ const retorno = parseRetorno(xml);
151
+
152
+ if (retorno.cStat === SEFAZ_STATUS.AUTORIZADO) {
153
+ return {
154
+ sucesso: true,
155
+ autorizado: true,
156
+ cStat: retorno.cStat,
157
+ xMotivo: retorno.xMotivo,
158
+ protocolo: retorno.nProt,
159
+ chaveAcesso: retorno.chNFe,
160
+ xmlProtocolo: xml,
161
+ ...meta,
162
+ };
163
+ }
164
+
165
+ if (retorno.cStat.startsWith('1')) {
166
+ return {
167
+ sucesso: true,
168
+ autorizado: false,
169
+ cStat: retorno.cStat,
170
+ xMotivo: retorno.xMotivo,
171
+ xmlProtocolo: xml,
172
+ ...meta,
173
+ };
174
+ }
175
+
176
+ throw new SefazError(retorno.cStat, retorno.xMotivo, { xmlRetorno: xml, ...meta });
177
+ }
178
+
179
+ /**
180
+ * Define cenário do mock em runtime.
181
+ * @param {'autorizado'|'duplicidade'|'schema'|'certificado'|'paralisado'} scenario
182
+ */
183
+ setMockScenario(scenario) {
184
+ this.mockScenario = scenario;
185
+ }
186
+
187
+ /**
188
+ * Monta envelope SOAP para NFeRecepcaoEvento4.
189
+ * @param {string} xmlEventoAssinado
190
+ * @param {string} idLote
191
+ */
192
+ buildEventoEnvelope(xmlEventoAssinado, idLote = '1') {
193
+ return `<?xml version="1.0" encoding="utf-8"?>
194
+ <soap12:Envelope xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
195
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema"
196
+ xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
197
+ <soap12:Body>
198
+ <nfeDadosMsg xmlns="http://www.portalfiscal.inf.br/nfe/wsdl/NFeRecepcaoEvento4">
199
+ ${xmlEventoAssinado}
200
+ </nfeDadosMsg>
201
+ </soap12:Body>
202
+ </soap12:Envelope>`;
203
+ }
204
+
205
+ /**
206
+ * Envia evento NF-e (cancelamento, CC-e) para SEFAZ.
207
+ * @param {string} xmlEventoAssinado
208
+ * @param {string} chaveAcesso
209
+ * @param {string} tpEvento
210
+ * @param {string} [idLote]
211
+ */
212
+ async enviarEvento(xmlEventoAssinado, chaveAcesso, tpEvento, idLote = '1') {
213
+ if (this.mock) {
214
+ return this.#mockEnviarEvento(chaveAcesso, tpEvento);
215
+ }
216
+
217
+ const endpoint = this.eventoEndpoint ?? this.endpoint;
218
+ if (!endpoint) {
219
+ throw new GhinfeError(
220
+ 'Endpoint SEFAZ de eventos não configurado. Defina eventoEndpoint ou use mock: true',
221
+ 'SEFAZ_EVENTO_ENDPOINT_MISSING'
222
+ );
223
+ }
224
+
225
+ const envelope = this.buildEventoEnvelope(xmlEventoAssinado, idLote);
226
+
227
+ try {
228
+ const response = await axios.post(endpoint, envelope, {
229
+ headers: { 'Content-Type': 'application/soap+xml; charset=utf-8' },
230
+ timeout: this.timeout,
231
+ responseType: 'text',
232
+ });
233
+
234
+ const bodyMatch = response.data.match(/<nfeResultMsg[^>]*>([\s\S]*?)<\/nfeResultMsg>/);
235
+ const innerXml = bodyMatch?.[1] ?? response.data;
236
+ return this.#processarRetornoEvento(innerXml);
237
+ } catch (error) {
238
+ if (axios.isAxiosError(error)) {
239
+ throw new GhinfeError(
240
+ `Falha na comunicação SOAP (evento) com SEFAZ: ${error.message}`,
241
+ 'SEFAZ_HTTP_ERROR',
242
+ { status: error.response?.status, data: error.response?.data }
243
+ );
244
+ }
245
+ throw error;
246
+ }
247
+ }
248
+
249
+ /**
250
+ * @param {string} chaveAcesso
251
+ * @param {string} tpEvento
252
+ */
253
+ async #mockEnviarEvento(chaveAcesso, tpEvento) {
254
+ await new Promise((r) => setTimeout(r, 200));
255
+
256
+ const scenarioMap = {
257
+ registrado: SEFAZ_MOCK_RESPONSES.eventoRegistrado,
258
+ rejeitado: SEFAZ_MOCK_RESPONSES.eventoRejeitado,
259
+ };
260
+
261
+ const resultado = scenarioMap[this.mockEventoScenario] ?? SEFAZ_MOCK_RESPONSES.eventoRegistrado;
262
+ const innerXml = buildMockRetEnvEventoXml(chaveAcesso, tpEvento, resultado);
263
+ const soapXml = wrapSoapEventoResponse(innerXml);
264
+
265
+ return this.#processarRetornoEvento(innerXml, { soapXml, mock: true });
266
+ }
267
+
268
+ /**
269
+ * @param {string} xml
270
+ * @param {Record<string, unknown>} [meta]
271
+ */
272
+ #processarRetornoEvento(xml, meta = {}) {
273
+ const cStatMatches = [...xml.matchAll(/<cStat>(\d+)<\/cStat>/g)];
274
+ const xMotivoMatches = [...xml.matchAll(/<xMotivo>([^<]+)<\/xMotivo>/g)];
275
+ const nProtMatch = xml.match(/<nProt>(\d+)<\/nProt>/);
276
+ const tpEventoMatch = xml.match(/<tpEvento>(\d+)<\/tpEvento>/);
277
+
278
+ const cStatLote = cStatMatches[0]?.[1] ?? '999';
279
+ const cStatEvento = cStatMatches[1]?.[1] ?? cStatLote;
280
+ const xMotivo = xMotivoMatches.at(-1)?.[1] ?? 'Retorno não identificado';
281
+
282
+ if (cStatEvento === SEFAZ_STATUS.EVENTO_REGISTRADO) {
283
+ return {
284
+ sucesso: true,
285
+ registrado: true,
286
+ cStat: cStatEvento,
287
+ xMotivo,
288
+ protocolo: nProtMatch?.[1],
289
+ tpEvento: tpEventoMatch?.[1],
290
+ xmlRetorno: xml,
291
+ ...meta,
292
+ };
293
+ }
294
+
295
+ if (cStatLote.startsWith('1') && cStatEvento.startsWith('1')) {
296
+ return {
297
+ sucesso: true,
298
+ registrado: false,
299
+ cStat: cStatEvento,
300
+ xMotivo,
301
+ xmlRetorno: xml,
302
+ ...meta,
303
+ };
304
+ }
305
+
306
+ throw new SefazError(cStatEvento, xMotivo, { xmlRetorno: xml, ...meta });
307
+ }
308
+
309
+ /**
310
+ * @param {'registrado'|'rejeitado'} scenario
311
+ */
312
+ setMockEventoScenario(scenario) {
313
+ this.mockEventoScenario = scenario;
314
+ }
315
+
316
+ /**
317
+ * Monta XML de consulta de status do serviço.
318
+ * @param {string} uf
319
+ * @param {number} [ambiente]
320
+ */
321
+ buildStatusServicoXml(uf, ambiente = 2) {
322
+ const cUF = UF_IBGE[uf?.toUpperCase()] ?? '35';
323
+ return `<?xml version="1.0" encoding="UTF-8"?>
324
+ <consStatServ xmlns="http://www.portalfiscal.inf.br/nfe" versao="4.00">
325
+ <tpAmb>${ambiente}</tpAmb>
326
+ <cUF>${cUF}</cUF>
327
+ <xServ>STATUS</xServ>
328
+ </consStatServ>`;
329
+ }
330
+
331
+ /**
332
+ * Consulta status do webservice SEFAZ (NfeStatusServico4).
333
+ * @param {string} uf
334
+ * @param {number} [ambiente]
335
+ */
336
+ async consultarStatus(uf, ambiente = 2) {
337
+ if (this.mock) {
338
+ return this.#mockConsultarStatus(uf);
339
+ }
340
+
341
+ const statusEndpoint =
342
+ this.statusEndpoint ??
343
+ (() => {
344
+ try {
345
+ return SefazEndpointResolver.resolver({
346
+ uf,
347
+ ambiente,
348
+ servico: SEFAZ_SERVICO.STATUS,
349
+ });
350
+ } catch {
351
+ return null;
352
+ }
353
+ })();
354
+
355
+ if (!statusEndpoint) {
356
+ throw new GhinfeError(
357
+ `Endpoint de status SEFAZ não disponível para UF ${uf}`,
358
+ 'SEFAZ_STATUS_ENDPOINT_MISSING'
359
+ );
360
+ }
361
+
362
+ const dados = this.buildStatusServicoXml(uf, ambiente);
363
+ const envelope = `<?xml version="1.0" encoding="utf-8"?>
364
+ <soap12:Envelope xmlns:soap12="http://www.w3.org/2003/05/soap-envelope">
365
+ <soap12:Body>
366
+ <nfeDadosMsg xmlns="http://www.portalfiscal.inf.br/nfe/wsdl/NFeStatusServico4">
367
+ ${dados}
368
+ </nfeDadosMsg>
369
+ </soap12:Body>
370
+ </soap12:Envelope>`;
371
+
372
+ try {
373
+ const response = await axios.post(statusEndpoint, envelope, {
374
+ headers: { 'Content-Type': 'application/soap+xml; charset=utf-8' },
375
+ timeout: this.timeout,
376
+ responseType: 'text',
377
+ });
378
+ const bodyMatch = response.data.match(/<nfeResultMsg[^>]*>([\s\S]*?)<\/nfeResultMsg>/);
379
+ return this.#processarRetornoStatus(bodyMatch?.[1] ?? response.data);
380
+ } catch (error) {
381
+ if (axios.isAxiosError(error)) {
382
+ throw new GhinfeError(
383
+ `Falha na consulta de status SEFAZ: ${error.message}`,
384
+ 'SEFAZ_HTTP_ERROR',
385
+ { status: error.response?.status }
386
+ );
387
+ }
388
+ throw error;
389
+ }
390
+ }
391
+
392
+ /** @param {string} uf */
393
+ async #mockConsultarStatus(uf) {
394
+ await new Promise((r) => setTimeout(r, 150));
395
+ const scenarioMap = {
396
+ ok: SEFAZ_MOCK_RESPONSES.statusOk,
397
+ paralisado: SEFAZ_MOCK_RESPONSES.statusParalisado,
398
+ };
399
+ const resultado = scenarioMap[this.mockStatusScenario ?? 'ok'] ?? SEFAZ_MOCK_RESPONSES.statusOk;
400
+ const cUF = UF_IBGE[uf?.toUpperCase()] ?? '35';
401
+ const innerXml = buildMockRetConsStatServXml(resultado, cUF);
402
+ return this.#processarRetornoStatus(innerXml, {
403
+ soapXml: wrapSoapStatusResponse(innerXml),
404
+ mock: true,
405
+ });
406
+ }
407
+
408
+ /**
409
+ * @param {string} xml
410
+ * @param {Record<string, unknown>} [meta]
411
+ */
412
+ #processarRetornoStatus(xml, meta = {}) {
413
+ const cStat = xml.match(/<cStat>(\d+)<\/cStat>/)?.[1] ?? '999';
414
+ const xMotivo = xml.match(/<xMotivo>([^<]+)<\/xMotivo>/)?.[1] ?? 'Retorno não identificado';
415
+ const tMed = xml.match(/<tMed>([^<]+)<\/tMed>/)?.[1];
416
+ const cUF = xml.match(/<cUF>(\d+)<\/cUF>/)?.[1];
417
+ const emOperacao = cStat === SEFAZ_STATUS.SERVICO_EM_OPERACAO;
418
+
419
+ return {
420
+ sucesso: emOperacao,
421
+ emOperacao,
422
+ cStat,
423
+ xMotivo,
424
+ tMed,
425
+ cUF,
426
+ xmlRetorno: xml,
427
+ ...meta,
428
+ };
429
+ }
430
+
431
+ /**
432
+ * @param {'ok'|'paralisado'} scenario
433
+ */
434
+ setMockStatusScenario(scenario) {
435
+ this.mockStatusScenario = scenario;
436
+ }
437
+ }