ghinfe 0.6.0
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 +15 -0
- package/LICENSE +21 -0
- package/README.md +413 -0
- package/docs/developer-guide.html +421 -0
- package/docs/openapi.yaml +428 -0
- package/examples/emitir-nfce.js +51 -0
- package/examples/emitir-nfe.js +83 -0
- package/examples/emitir-nfse.js +46 -0
- package/examples/endpoints-sefaz.js +14 -0
- package/examples/eventos-nfe.js +49 -0
- package/examples/gerar-chave.js +25 -0
- package/examples/gerar-danfe.js +75 -0
- package/examples/layouts-nfse.js +51 -0
- package/examples/mock-sefaz.js +52 -0
- package/examples/nfse-eventos.js +57 -0
- package/examples/reforma-ibscbs.js +27 -0
- package/examples/reforma-nfe-xml.js +52 -0
- package/examples/status-sefaz.js +12 -0
- package/examples/validar-xsd.js +48 -0
- package/package.json +68 -0
- package/schemas/nfe/PL_009_V4/consReciNFe_v4.00.xsd +9 -0
- package/schemas/nfe/PL_009_V4/consSitNFe_v4.00.xsd +9 -0
- package/schemas/nfe/PL_009_V4/consStatServ_v4.00.xsd +9 -0
- package/schemas/nfe/PL_009_V4/enviNFe_v3.10.xsd +9 -0
- package/schemas/nfe/PL_009_V4/enviNFe_v4.00.xsd +9 -0
- package/schemas/nfe/PL_009_V4/inutNFe_v4.00.xsd +9 -0
- package/schemas/nfe/PL_009_V4/leiauteNFe_v4.00.xsd +7339 -0
- package/schemas/nfe/PL_009_V4/nfe_v4.00.xsd +9 -0
- package/schemas/nfe/PL_009_V4/tiposBasico_v4.00.xsd +598 -0
- package/schemas/nfe/PL_009_V4/xmldsig-core-schema_v1.01.xsd +98 -0
- package/scripts/download-schemas.js +39 -0
- package/scripts/test-xsd.js +34 -0
- package/src/api/server.js +316 -0
- package/src/constants/sefaz.js +69 -0
- package/src/danfe/DanfeService.js +205 -0
- package/src/danfe/xml-extract.js +69 -0
- package/src/errors/GhinfeError.js +13 -0
- package/src/errors/SefazError.js +25 -0
- package/src/errors/XsdValidationError.js +15 -0
- package/src/index.js +60 -0
- package/src/nfce/builders/NFCeXmlBuilder.js +178 -0
- package/src/nfce/constants/qrcode.js +26 -0
- package/src/nfce/services/NFCeService.js +188 -0
- package/src/nfce/services/NFCeXmlService.js +38 -0
- package/src/nfce/types/NFCeDocument.js +42 -0
- package/src/nfce/utils/qrcode.utils.js +92 -0
- package/src/nfe/builders/NFeEventoXmlBuilder.js +110 -0
- package/src/nfe/builders/NFeXmlBuilder.js +215 -0
- package/src/nfe/mocks/sefaz-responses.js +195 -0
- package/src/nfe/services/NFeEventoService.js +95 -0
- package/src/nfe/services/NFeService.js +196 -0
- package/src/nfe/services/NFeSignService.js +111 -0
- package/src/nfe/services/NFeSoapService.js +437 -0
- package/src/nfe/services/NFeXmlService.js +41 -0
- package/src/nfe/services/NFeXsdService.js +56 -0
- package/src/nfe/types/NFeDocument.js +92 -0
- package/src/nfe/types/NFeEvento.js +44 -0
- package/src/nfse/NFSeMunicipioResolver.js +42 -0
- package/src/nfse/builders/NFSeEventoXmlBuilder.js +92 -0
- package/src/nfse/builders/NFSeXmlBuilder.js +16 -0
- package/src/nfse/constants/municipios.js +82 -0
- package/src/nfse/layouts/adapters.js +202 -0
- package/src/nfse/mocks/nfse-responses.js +142 -0
- package/src/nfse/services/NFSeService.js +191 -0
- package/src/nfse/services/NFSeSoapService.js +232 -0
- package/src/nfse/services/NFSeXmlService.js +12 -0
- package/src/nfse/types/NFSeDocument.js +54 -0
- package/src/queue/NFCeQueue.js +63 -0
- package/src/queue/NFSeQueue.js +59 -0
- package/src/queue/NFeQueue.js +76 -0
- package/src/reforma/ibscbs.js +175 -0
- package/src/sefaz/SefazEndpointResolver.js +95 -0
- package/src/sefaz/endpoints.registry.js +205 -0
- package/src/shared/fiscal.helpers.js +53 -0
- package/src/utils/certificate.utils.js +47 -0
- package/src/utils/chave.utils.js +117 -0
- package/src/utils/date.utils.js +26 -0
- package/src/utils/xml.utils.js +44 -0
- package/src/validation/XsdValidator.js +114 -0
- package/src/validation/schemas.js +14 -0
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { NFeXmlBuilder } from '../builders/NFeXmlBuilder.js';
|
|
2
|
+
import { NFeXsdService } from './NFeXsdService.js';
|
|
3
|
+
import { reformaHabilitada } from '../../reforma/ibscbs.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Serviço responsável pela geração e validação XSD do XML da NF-e.
|
|
7
|
+
*/
|
|
8
|
+
export class NFeXmlService {
|
|
9
|
+
/**
|
|
10
|
+
* @param {{ validarXsd?: boolean }} [options]
|
|
11
|
+
*/
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
this.builder = new NFeXmlBuilder();
|
|
14
|
+
this.xsdService = new NFeXsdService();
|
|
15
|
+
this.validarXsd = options.validarXsd ?? false;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {import('../types/NFeDocument.js').NFeDocument} documento
|
|
20
|
+
* @param {{ validarXsd?: boolean }} [options]
|
|
21
|
+
*/
|
|
22
|
+
gerarXml(documento, options = {}) {
|
|
23
|
+
const xml = this.builder.build(documento);
|
|
24
|
+
const deveValidar = options.validarXsd ?? this.validarXsd;
|
|
25
|
+
|
|
26
|
+
// PL_009 não conhece IBSCBS — não validar XSD quando reforma está ativa
|
|
27
|
+
if (deveValidar && !reformaHabilitada(documento)) {
|
|
28
|
+
this.xsdService.validarNFe(xml, { preAssinatura: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return xml;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} xml
|
|
36
|
+
* @param {{ preAssinatura?: boolean }} [options]
|
|
37
|
+
*/
|
|
38
|
+
validarXml(xml, options = {}) {
|
|
39
|
+
return this.xsdService.validarNFe(xml, options);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { XsdValidator } from '../../validation/XsdValidator.js';
|
|
2
|
+
import { XSD_SCHEMAS } from '../../validation/schemas.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Validação XSD específica para NF-e / NFC-e (layout 4.00).
|
|
6
|
+
*/
|
|
7
|
+
export class NFeXsdService {
|
|
8
|
+
/**
|
|
9
|
+
* Valida XML da NF-e (modelo 55) contra schema oficial.
|
|
10
|
+
* @param {string} xml
|
|
11
|
+
* @param {{ preAssinatura?: boolean }} [options]
|
|
12
|
+
* preAssinatura=true injeta stub estrutural (Signature) para validar infNFe antes de assinar
|
|
13
|
+
*/
|
|
14
|
+
validarNFe(xml, options = {}) {
|
|
15
|
+
return XsdValidator.validar(xml, XSD_SCHEMAS.NFE, {
|
|
16
|
+
injetarAssinaturaStub: options.preAssinatura ?? !xml.includes('</Signature>'),
|
|
17
|
+
modelo: '55',
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Valida XML da NFC-e (modelo 65) contra schema oficial.
|
|
23
|
+
* @param {string} xml
|
|
24
|
+
* @param {{ preAssinatura?: boolean }} [options]
|
|
25
|
+
*/
|
|
26
|
+
validarNFCe(xml, options = {}) {
|
|
27
|
+
return XsdValidator.validar(xml, XSD_SCHEMAS.NFE, {
|
|
28
|
+
injetarAssinaturaStub: options.preAssinatura ?? !xml.includes('</Signature>'),
|
|
29
|
+
modelo: '65',
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* @param {string} xml
|
|
35
|
+
* @param {'55'|'65'} [modelo]
|
|
36
|
+
*/
|
|
37
|
+
tentarValidar(xml, modelo = '55') {
|
|
38
|
+
return modelo === '65' ? this.tentarValidarNFCe(xml) : this.tentarValidarNFe(xml);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @param {string} xml */
|
|
42
|
+
tentarValidarNFe(xml) {
|
|
43
|
+
return XsdValidator.tentarValidar(xml, XSD_SCHEMAS.NFE, {
|
|
44
|
+
injetarAssinaturaStub: !xml.includes('</Signature>'),
|
|
45
|
+
modelo: '55',
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** @param {string} xml */
|
|
50
|
+
tentarValidarNFCe(xml) {
|
|
51
|
+
return XsdValidator.tentarValidar(xml, XSD_SCHEMAS.NFE, {
|
|
52
|
+
injetarAssinaturaStub: !xml.includes('</Signature>'),
|
|
53
|
+
modelo: '65',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {Object} NFeEndereco
|
|
3
|
+
* @property {string} logradouro
|
|
4
|
+
* @property {string} numero
|
|
5
|
+
* @property {string} [complemento]
|
|
6
|
+
* @property {string} bairro
|
|
7
|
+
* @property {string} codigoMunicipio IBGE 7 dígitos
|
|
8
|
+
* @property {string} municipio
|
|
9
|
+
* @property {string} uf
|
|
10
|
+
* @property {string} cep
|
|
11
|
+
* @property {string} [telefone]
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} NFeEmitente
|
|
16
|
+
* @property {string} cnpj
|
|
17
|
+
* @property {string} razaoSocial
|
|
18
|
+
* @property {string} [nomeFantasia]
|
|
19
|
+
* @property {string} ie
|
|
20
|
+
* @property {string} crt 1=Simples, 2=Simples excesso, 3=Normal
|
|
21
|
+
* @property {NFeEndereco} endereco
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {Object} NFeDestinatario
|
|
26
|
+
* @property {string} [cnpj]
|
|
27
|
+
* @property {string} [cpf]
|
|
28
|
+
* @property {string} razaoSocial
|
|
29
|
+
* @property {string} [ie]
|
|
30
|
+
* @property {number} [indIEDest] 1=Contribuinte, 2=Isento, 9=Não contribuinte
|
|
31
|
+
* @property {NFeEndereco} endereco
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @typedef {Object} NFeItem
|
|
36
|
+
* @property {string} codigo
|
|
37
|
+
* @property {string} descricao
|
|
38
|
+
* @property {string} ncm
|
|
39
|
+
* @property {string} cfop
|
|
40
|
+
* @property {string} unidade
|
|
41
|
+
* @property {number} quantidade
|
|
42
|
+
* @property {number} valorUnitario
|
|
43
|
+
* @property {number} valorTotal
|
|
44
|
+
* @property {string} [ean]
|
|
45
|
+
* @property {string} [cst] CST ICMS (ex: "00")
|
|
46
|
+
* @property {number} [aliquotaIcms]
|
|
47
|
+
* @property {number} [valorIcms]
|
|
48
|
+
* @property {import('../../reforma/ibscbs.js').TributacaoRTC} [reforma] IBS/CBS (PL_010)
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* @typedef {Object} NFeFrete
|
|
53
|
+
* @property {number} modalidade 0=Emitente, 1=Destinatário, 9=Sem frete
|
|
54
|
+
* @property {number} [valor]
|
|
55
|
+
* @property {string} [transportadora]
|
|
56
|
+
* @property {string} [cnpjTransportadora]
|
|
57
|
+
*/
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @typedef {Object} NFePagamento
|
|
61
|
+
* @property {string} forma 01=Dinheiro, 03=Cartão crédito, etc.
|
|
62
|
+
* @property {number} valor
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* @typedef {Object} NFeIde
|
|
67
|
+
* @property {string} uf Sigla UF (ex: "SP")
|
|
68
|
+
* @property {string} codigoMunicipio IBGE
|
|
69
|
+
* @property {number} [serie]
|
|
70
|
+
* @property {number} numero
|
|
71
|
+
* @property {number} [tipoEmissao] 1=Normal
|
|
72
|
+
* @property {number} [finalidade] 1=Normal
|
|
73
|
+
* @property {number} [consumidorFinal] 0=Não, 1=Sim
|
|
74
|
+
* @property {number} [presenca] 1=Presencial
|
|
75
|
+
* @property {number} [naturezaOperacao]
|
|
76
|
+
*/
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* @typedef {Object} NFeDocumento
|
|
80
|
+
* @property {string} [chaveAcesso] 44 dígitos — gerada automaticamente se omitida
|
|
81
|
+
* @property {number} [ambiente] 1=Produção, 2=Homologação
|
|
82
|
+
* @property {boolean} [reformaTributaria] força totais IBS/CBS mesmo sem item.reforma
|
|
83
|
+
* @property {NFeIde} ide
|
|
84
|
+
* @property {NFeEmitente} emitente
|
|
85
|
+
* @property {NFeDestinatario} destinatario
|
|
86
|
+
* @property {NFeItem[]} itens
|
|
87
|
+
* @property {NFeFrete} [frete]
|
|
88
|
+
* @property {NFePagamento[]} [pagamentos]
|
|
89
|
+
* @property {string} [informacoesComplementares]
|
|
90
|
+
*/
|
|
91
|
+
|
|
92
|
+
export {};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Tipos de evento NF-e conforme tabela SEFAZ. */
|
|
2
|
+
export const NFE_EVENTOS = {
|
|
3
|
+
CARTA_CORRECAO: '110110',
|
|
4
|
+
CANCELAMENTO: '110111',
|
|
5
|
+
CIENCIA_OPERACAO: '210210',
|
|
6
|
+
CONFIRMACAO_OPERACAO: '210200',
|
|
7
|
+
DESCONHECIMENTO_OPERACAO: '210220',
|
|
8
|
+
OPERACAO_NAO_REALIZADA: '210240',
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/** Texto fixo exigido na CC-e. */
|
|
12
|
+
export const CCE_COND_USO =
|
|
13
|
+
'A Carta de Correcao e disciplinada pelo paragrafo 1o-A do art. 7o do Convenio S/N, ' +
|
|
14
|
+
'de 15 de dezembro de 1970 e pode ser utilizada para regularizacao de erro ocorrido ' +
|
|
15
|
+
'na emissao de documento fiscal, desde que o erro nao esteja relacionado com: ' +
|
|
16
|
+
'I - as variaveis que determinam o valor do imposto tais como: base de calculo, ' +
|
|
17
|
+
'aliquota, diferenca de preco, quantidade, valor da operacao ou da prestacao; ' +
|
|
18
|
+
'II - a correcao de dados cadastrais que implique mudanca do remetente ou do destinatario; ' +
|
|
19
|
+
'III - a data de emissao ou de saida.';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {Object} NFeEventoBase
|
|
23
|
+
* @property {string} chaveAcesso Chave da NF-e (44 dígitos)
|
|
24
|
+
* @property {string} cnpj CNPJ do autor do evento
|
|
25
|
+
* @property {string} uf Sigla UF
|
|
26
|
+
* @property {number} [ambiente] 1=Produção, 2=Homologação
|
|
27
|
+
* @property {number} [sequencia] nSeqEvento (default 1)
|
|
28
|
+
* @property {string} [idLote] ID do lote de eventos
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @typedef {NFeEventoBase & {
|
|
33
|
+
* protocolo: string,
|
|
34
|
+
* justificativa: string,
|
|
35
|
+
* }} NFeCancelamento
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* @typedef {NFeEventoBase & {
|
|
40
|
+
* correcao: string,
|
|
41
|
+
* }} NFeCartaCorrecao
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
export {};
|