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.
- package/.env.example +24 -15
- package/LICENSE +21 -21
- package/README.md +491 -413
- package/docs/developer-guide.html +421 -421
- package/docs/openapi.yaml +428 -428
- package/examples/dce-homologacao.js +141 -0
- package/examples/emitir-nfce.js +51 -51
- package/examples/emitir-nfe.js +83 -83
- package/examples/emitir-nfse.js +46 -46
- package/examples/endpoints-sefaz.js +14 -14
- package/examples/eventos-nfe.js +49 -49
- package/examples/gerar-chave.js +25 -25
- package/examples/gerar-dace.js +59 -0
- package/examples/gerar-danfe.js +75 -75
- package/examples/gerar-dce.js +58 -0
- package/examples/layouts-nfse.js +51 -51
- package/examples/mock-sefaz.js +52 -52
- package/examples/nfse-eventos.js +57 -57
- package/examples/reforma-ibscbs.js +27 -27
- package/examples/reforma-nfe-xml.js +52 -52
- package/examples/status-sefaz.js +12 -12
- package/examples/validar-xsd.js +48 -48
- package/package.json +59 -68
- package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/consSitDCeTiposBasico_v1.00.xsd +123 -0
- package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/consSitDCe_v1.00.xsd +10 -0
- package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/consStatServDCeTiposBasico_v1.00.xsd +110 -0
- package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/consStatServDCe_v1.00.xsd +10 -0
- package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/dceTiposBasico_v1.00.xsd +1388 -0
- package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/retConsSitDCe_v1.00.xsd +10 -0
- package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/retConsStatServDCe_v1.00.xsd +10 -0
- package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/tiposGeralDCe_v1.00.xsd +712 -0
- package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/xmldsig-core-schema_v1.01.xsd +98 -0
- package/schemas/dce/PL_DCe_100a_NT2024.001/dceTiposBasico_v1.00.xsd +1410 -0
- package/schemas/dce/PL_DCe_100a_NT2024.001/dce_v1.00.xsd +10 -0
- package/schemas/dce/PL_DCe_100a_NT2024.001/evento/evCancDCe_v1.00.xsd +36 -0
- package/schemas/dce/PL_DCe_100a_NT2024.001/evento/eventoDCeTiposBasico_v1.00.xsd +283 -0
- package/schemas/dce/PL_DCe_100a_NT2024.001/evento/eventoDCe_v1.00.xsd +9 -0
- package/schemas/dce/PL_DCe_100a_NT2024.001/evento/procEventoDCe_v1.00.xsd +10 -0
- package/schemas/dce/PL_DCe_100a_NT2024.001/evento/retEventoDCe_v1.00.xsd +9 -0
- package/schemas/dce/PL_DCe_100a_NT2024.001/evento/tiposGeralDCe_v1.00.xsd +712 -0
- package/schemas/dce/PL_DCe_100a_NT2024.001/evento/xmldsig-core-schema_v1.01.xsd +98 -0
- package/schemas/dce/PL_DCe_100a_NT2024.001/procDCe_v1.00.xsd +37 -0
- package/schemas/dce/PL_DCe_100a_NT2024.001/retDCe_v1.00.xsd +9 -0
- package/schemas/dce/PL_DCe_100a_NT2024.001/tiposGeralDCe_v1.00.xsd +712 -0
- package/schemas/dce/PL_DCe_100a_NT2024.001/xmldsig-core-schema_v1.01.xsd +98 -0
- package/schemas/nfe/PL_009_V4/enviNFe_v3.10.xsd +8 -8
- package/schemas/nfe/PL_009_V4/xmldsig-core-schema_v1.01.xsd +97 -97
- package/scripts/download-schemas.js +39 -39
- package/scripts/test-xsd.js +34 -34
- package/src/api/server.js +316 -316
- package/src/constants/sefaz.js +69 -69
- package/src/danfe/DanfeService.js +205 -205
- package/src/danfe/xml-extract.js +69 -69
- package/src/dce/builders/DCeEventoXmlBuilder.js +86 -0
- package/src/dce/builders/DCeXmlBuilder.js +345 -0
- package/src/dce/constants/dce.js +87 -0
- package/src/dce/dace/DaceService.js +356 -0
- package/src/dce/dace/dace-extract.js +114 -0
- package/src/dce/services/DCeService.js +263 -0
- package/src/dce/services/DCeSoapService.js +235 -0
- package/src/dce/services/DCeXmlService.js +67 -0
- package/src/dce/types/DCeDocument.js +106 -0
- package/src/dce/utils/qrcode.utils.js +59 -0
- package/src/errors/GhinfeError.js +13 -13
- package/src/errors/SefazError.js +25 -25
- package/src/errors/XsdValidationError.js +15 -15
- package/src/index.js +84 -60
- package/src/nfce/builders/NFCeXmlBuilder.js +178 -178
- package/src/nfce/constants/qrcode.js +26 -26
- package/src/nfce/services/NFCeService.js +188 -188
- package/src/nfce/services/NFCeXmlService.js +38 -38
- package/src/nfce/types/NFCeDocument.js +42 -42
- package/src/nfce/utils/qrcode.utils.js +92 -92
- package/src/nfe/builders/NFeEventoXmlBuilder.js +110 -110
- package/src/nfe/builders/NFeXmlBuilder.js +215 -215
- package/src/nfe/mocks/sefaz-responses.js +195 -195
- package/src/nfe/services/NFeEventoService.js +95 -95
- package/src/nfe/services/NFeService.js +196 -196
- package/src/nfe/services/NFeSignService.js +127 -111
- package/src/nfe/services/NFeSoapService.js +437 -437
- package/src/nfe/services/NFeXmlService.js +41 -41
- package/src/nfe/services/NFeXsdService.js +56 -56
- package/src/nfe/types/NFeDocument.js +92 -92
- package/src/nfe/types/NFeEvento.js +44 -44
- package/src/nfse/NFSeMunicipioResolver.js +42 -42
- package/src/nfse/builders/NFSeEventoXmlBuilder.js +92 -92
- package/src/nfse/builders/NFSeXmlBuilder.js +16 -16
- package/src/nfse/constants/municipios.js +82 -82
- package/src/nfse/layouts/adapters.js +202 -202
- package/src/nfse/mocks/nfse-responses.js +142 -142
- package/src/nfse/services/NFSeService.js +191 -191
- package/src/nfse/services/NFSeSoapService.js +232 -232
- package/src/nfse/services/NFSeXmlService.js +12 -12
- package/src/nfse/types/NFSeDocument.js +54 -54
- package/src/queue/NFCeQueue.js +63 -63
- package/src/queue/NFSeQueue.js +59 -59
- package/src/queue/NFeQueue.js +76 -76
- package/src/reforma/ibscbs.js +175 -175
- package/src/sefaz/SefazEndpointResolver.js +95 -95
- package/src/sefaz/ca/icp-brasil-raiz-v10.pem +38 -0
- package/src/sefaz/endpoints.registry.js +205 -205
- package/src/sefaz/httpsAgent.js +33 -0
- package/src/shared/fiscal.helpers.js +53 -53
- package/src/utils/certificate.utils.js +69 -47
- package/src/utils/chave.utils.js +205 -117
- package/src/utils/code128.utils.js +67 -0
- package/src/utils/date.utils.js +26 -26
- package/src/utils/xml.utils.js +44 -44
- package/src/validation/XsdValidator.js +133 -114
- 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
|
+
}
|