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,195 @@
|
|
|
1
|
+
import { SEFAZ_STATUS } from '../../constants/sefaz.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Respostas simuladas da SEFAZ para desenvolvimento e testes.
|
|
5
|
+
* Baseadas nos cStat reais do Manual de Orientação do Contribuinte.
|
|
6
|
+
*/
|
|
7
|
+
export const SEFAZ_MOCK_RESPONSES = {
|
|
8
|
+
autorizado: {
|
|
9
|
+
cStat: SEFAZ_STATUS.AUTORIZADO,
|
|
10
|
+
xMotivo: 'Autorizado o uso da NF-e',
|
|
11
|
+
nProt: '135260000000001',
|
|
12
|
+
dhRecbto: new Date().toISOString(),
|
|
13
|
+
},
|
|
14
|
+
loteRecebido: {
|
|
15
|
+
cStat: SEFAZ_STATUS.LOTE_RECEBIDO,
|
|
16
|
+
xMotivo: 'Lote recebido com sucesso',
|
|
17
|
+
nRec: '135260000000001',
|
|
18
|
+
tMed: '1',
|
|
19
|
+
},
|
|
20
|
+
rejeicaoDuplicidade: {
|
|
21
|
+
cStat: SEFAZ_STATUS.REJEICAO_DUPLICIDADE,
|
|
22
|
+
xMotivo: 'Rejeição: Duplicidade de NF-e',
|
|
23
|
+
},
|
|
24
|
+
rejeicaoSchema: {
|
|
25
|
+
cStat: SEFAZ_STATUS.REJEICAO_SCHEMA,
|
|
26
|
+
xMotivo: 'Rejeição: Falha no Schema XML da NFe',
|
|
27
|
+
},
|
|
28
|
+
rejeicaoCertificado: {
|
|
29
|
+
cStat: SEFAZ_STATUS.REJEICAO_CERTIFICADO,
|
|
30
|
+
xMotivo: 'Rejeição: Certificado Transmissor inválido',
|
|
31
|
+
},
|
|
32
|
+
servicoParalisado: {
|
|
33
|
+
cStat: SEFAZ_STATUS.SERVICO_PARALISADO,
|
|
34
|
+
xMotivo: 'Serviço Paralisado momentaneamente (curto prazo)',
|
|
35
|
+
},
|
|
36
|
+
eventoRegistrado: {
|
|
37
|
+
cStat: SEFAZ_STATUS.EVENTO_REGISTRADO,
|
|
38
|
+
xMotivo: 'Evento registrado e vinculado a NF-e',
|
|
39
|
+
nProt: '135260000000002',
|
|
40
|
+
},
|
|
41
|
+
eventoRejeitado: {
|
|
42
|
+
cStat: SEFAZ_STATUS.EVENTO_REJEICAO,
|
|
43
|
+
xMotivo: 'Rejeição: Chave de Acesso inexistente',
|
|
44
|
+
},
|
|
45
|
+
statusOk: {
|
|
46
|
+
cStat: '107',
|
|
47
|
+
xMotivo: 'Serviço em Operação',
|
|
48
|
+
tMed: '1',
|
|
49
|
+
},
|
|
50
|
+
statusParalisado: {
|
|
51
|
+
cStat: SEFAZ_STATUS.SERVICO_PARALISADO,
|
|
52
|
+
xMotivo: 'Serviço Paralisado momentaneamente (curto prazo)',
|
|
53
|
+
tMed: '5',
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Gera XML de retorno de autorização simulado (protNFe).
|
|
59
|
+
* @param {string} chaveAcesso
|
|
60
|
+
* @param {{ cStat: string, xMotivo: string, nProt?: string }} resultado
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
export function buildMockProtNFeXml(chaveAcesso, resultado) {
|
|
64
|
+
const nProt = resultado.nProt ?? '';
|
|
65
|
+
const dhRecbto = new Date().toISOString().replace('Z', '-03:00');
|
|
66
|
+
|
|
67
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
68
|
+
<retEnviNFe xmlns="http://www.portalfiscal.inf.br/nfe" versao="4.00">
|
|
69
|
+
<tpAmb>2</tpAmb>
|
|
70
|
+
<verAplic>MOCK_GHINFE_1.0</verAplic>
|
|
71
|
+
<cStat>${resultado.cStat}</cStat>
|
|
72
|
+
<xMotivo>${resultado.xMotivo}</xMotivo>
|
|
73
|
+
<cUF>35</cUF>
|
|
74
|
+
<dhRecbto>${dhRecbto}</dhRecbto>
|
|
75
|
+
${
|
|
76
|
+
resultado.cStat === SEFAZ_STATUS.AUTORIZADO
|
|
77
|
+
? `<protNFe versao="4.00">
|
|
78
|
+
<infProt>
|
|
79
|
+
<tpAmb>2</tpAmb>
|
|
80
|
+
<verAplic>MOCK_GHINFE_1.0</verAplic>
|
|
81
|
+
<chNFe>${chaveAcesso}</chNFe>
|
|
82
|
+
<dhRecbto>${dhRecbto}</dhRecbto>
|
|
83
|
+
<nProt>${nProt}</nProt>
|
|
84
|
+
<digVal>mock-digest-value</digVal>
|
|
85
|
+
<cStat>${resultado.cStat}</cStat>
|
|
86
|
+
<xMotivo>${resultado.xMotivo}</xMotivo>
|
|
87
|
+
</infProt>
|
|
88
|
+
</protNFe>`
|
|
89
|
+
: ''
|
|
90
|
+
}
|
|
91
|
+
</retEnviNFe>`;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Gera envelope SOAP de retorno simulado.
|
|
96
|
+
* @param {string} innerXml
|
|
97
|
+
* @returns {string}
|
|
98
|
+
*/
|
|
99
|
+
export function wrapSoapResponse(innerXml) {
|
|
100
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
101
|
+
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
|
102
|
+
<soap:Body>
|
|
103
|
+
<nfeResultMsg xmlns="http://www.portalfiscal.inf.br/nfe/wsdl/NFeAutorizacao4">
|
|
104
|
+
${innerXml}
|
|
105
|
+
</nfeResultMsg>
|
|
106
|
+
</soap:Body>
|
|
107
|
+
</soap:Envelope>`;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Gera XML de retorno de evento simulado (retEnvEvento).
|
|
112
|
+
* @param {string} chaveAcesso
|
|
113
|
+
* @param {string} tpEvento
|
|
114
|
+
* @param {{ cStat: string, xMotivo: string, nProt?: string }} resultado
|
|
115
|
+
* @returns {string}
|
|
116
|
+
*/
|
|
117
|
+
export function buildMockRetEnvEventoXml(chaveAcesso, tpEvento, resultado) {
|
|
118
|
+
const dhReg = new Date().toISOString().replace('Z', '-03:00');
|
|
119
|
+
const loteOk = resultado.cStat === SEFAZ_STATUS.EVENTO_REGISTRADO;
|
|
120
|
+
|
|
121
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
122
|
+
<retEnvEvento xmlns="http://www.portalfiscal.inf.br/nfe" versao="1.00">
|
|
123
|
+
<idLote>1</idLote>
|
|
124
|
+
<tpAmb>2</tpAmb>
|
|
125
|
+
<verAplic>MOCK_GHINFE_1.0</verAplic>
|
|
126
|
+
<cStat>${loteOk ? SEFAZ_STATUS.EVENTO_LOTE_PROCESSADO : '104'}</cStat>
|
|
127
|
+
<xMotivo>${loteOk ? 'Lote de evento processado' : resultado.xMotivo}</xMotivo>
|
|
128
|
+
${
|
|
129
|
+
loteOk
|
|
130
|
+
? `<retEvento versao="1.00">
|
|
131
|
+
<infEvento>
|
|
132
|
+
<tpAmb>2</tpAmb>
|
|
133
|
+
<verAplic>MOCK_GHINFE_1.0</verAplic>
|
|
134
|
+
<cOrgao>35</cOrgao>
|
|
135
|
+
<cStat>${resultado.cStat}</cStat>
|
|
136
|
+
<xMotivo>${resultado.xMotivo}</xMotivo>
|
|
137
|
+
<chNFe>${chaveAcesso}</chNFe>
|
|
138
|
+
<tpEvento>${tpEvento}</tpEvento>
|
|
139
|
+
<nSeqEvento>1</nSeqEvento>
|
|
140
|
+
<dhRegEvento>${dhReg}</dhRegEvento>
|
|
141
|
+
<nProt>${resultado.nProt ?? '135260000000002'}</nProt>
|
|
142
|
+
</infEvento>
|
|
143
|
+
</retEvento>`
|
|
144
|
+
: ''
|
|
145
|
+
}
|
|
146
|
+
</retEnvEvento>`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* @param {string} innerXml
|
|
151
|
+
* @returns {string}
|
|
152
|
+
*/
|
|
153
|
+
export function wrapSoapEventoResponse(innerXml) {
|
|
154
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
155
|
+
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
|
156
|
+
<soap:Body>
|
|
157
|
+
<nfeResultMsg xmlns="http://www.portalfiscal.inf.br/nfe/wsdl/NFeRecepcaoEvento4">
|
|
158
|
+
${innerXml}
|
|
159
|
+
</nfeResultMsg>
|
|
160
|
+
</soap:Body>
|
|
161
|
+
</soap:Envelope>`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* XML de retorno de status do serviço (retConsStatServ).
|
|
166
|
+
* @param {{ cStat: string, xMotivo: string, tMed?: string }} resultado
|
|
167
|
+
* @param {string} [cUF]
|
|
168
|
+
*/
|
|
169
|
+
export function buildMockRetConsStatServXml(resultado, cUF = '35') {
|
|
170
|
+
const dh = new Date().toISOString().replace('Z', '-03:00');
|
|
171
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
172
|
+
<retConsStatServ xmlns="http://www.portalfiscal.inf.br/nfe" versao="4.00">
|
|
173
|
+
<tpAmb>2</tpAmb>
|
|
174
|
+
<verAplic>MOCK_GHINFE_1.0</verAplic>
|
|
175
|
+
<cStat>${resultado.cStat}</cStat>
|
|
176
|
+
<xMotivo>${resultado.xMotivo}</xMotivo>
|
|
177
|
+
<cUF>${cUF}</cUF>
|
|
178
|
+
<dhRecbto>${dh}</dhRecbto>
|
|
179
|
+
<tMed>${resultado.tMed ?? '1'}</tMed>
|
|
180
|
+
</retConsStatServ>`;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* @param {string} innerXml
|
|
185
|
+
*/
|
|
186
|
+
export function wrapSoapStatusResponse(innerXml) {
|
|
187
|
+
return `<?xml version="1.0" encoding="utf-8"?>
|
|
188
|
+
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
|
|
189
|
+
<soap:Body>
|
|
190
|
+
<nfeResultMsg xmlns="http://www.portalfiscal.inf.br/nfe/wsdl/NFeStatusServico4">
|
|
191
|
+
${innerXml}
|
|
192
|
+
</nfeResultMsg>
|
|
193
|
+
</soap:Body>
|
|
194
|
+
</soap:Envelope>`;
|
|
195
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { NFeEventoXmlBuilder } from '../builders/NFeEventoXmlBuilder.js';
|
|
2
|
+
import { NFeSignService } from './NFeSignService.js';
|
|
3
|
+
import { NFeSoapService } from './NFeSoapService.js';
|
|
4
|
+
import { NFE_EVENTOS } from '../types/NFeEvento.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Serviço de eventos NF-e: cancelamento, carta de correção, etc.
|
|
8
|
+
*/
|
|
9
|
+
export class NFeEventoService {
|
|
10
|
+
/**
|
|
11
|
+
* @param {ConstructorParameters<typeof NFeSoapService>[0]} [soapOptions]
|
|
12
|
+
*/
|
|
13
|
+
constructor(soapOptions = {}) {
|
|
14
|
+
this.builder = new NFeEventoXmlBuilder();
|
|
15
|
+
this.signService = new NFeSignService();
|
|
16
|
+
this.soapService = new NFeSoapService(soapOptions);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {import('../types/NFeEvento.js').NFeCancelamento} dados
|
|
21
|
+
*/
|
|
22
|
+
gerarXmlCancelamento(dados) {
|
|
23
|
+
return this.builder.buildCancelamento(dados);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {import('../types/NFeEvento.js').NFeCartaCorrecao} dados
|
|
28
|
+
*/
|
|
29
|
+
gerarXmlCartaCorrecao(dados) {
|
|
30
|
+
return this.builder.buildCartaCorrecao(dados);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Cancela NF-e autorizada (evento 110111).
|
|
35
|
+
* @param {import('../types/NFeEvento.js').NFeCancelamento} dados
|
|
36
|
+
* @param {{ pfx: Buffer|string, senha: string }} certificado
|
|
37
|
+
*/
|
|
38
|
+
async cancelar(dados, certificado) {
|
|
39
|
+
const xml = this.gerarXmlCancelamento(dados);
|
|
40
|
+
const xmlAssinado = this.signService.assinarEvento(xml, certificado.pfx, certificado.senha);
|
|
41
|
+
const retorno = await this.soapService.enviarEvento(
|
|
42
|
+
xmlAssinado,
|
|
43
|
+
dados.chaveAcesso,
|
|
44
|
+
NFE_EVENTOS.CANCELAMENTO,
|
|
45
|
+
dados.idLote
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
return { tipo: 'cancelamento', xmlGerado: xml, xmlAssinado, retorno };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Registra Carta de Correção Eletrônica (evento 110110).
|
|
53
|
+
* @param {import('../types/NFeEvento.js').NFeCartaCorrecao} dados
|
|
54
|
+
* @param {{ pfx: Buffer|string, senha: string }} certificado
|
|
55
|
+
*/
|
|
56
|
+
async cartaCorrecao(dados, certificado) {
|
|
57
|
+
const xml = this.gerarXmlCartaCorrecao(dados);
|
|
58
|
+
const xmlAssinado = this.signService.assinarEvento(xml, certificado.pfx, certificado.senha);
|
|
59
|
+
const retorno = await this.soapService.enviarEvento(
|
|
60
|
+
xmlAssinado,
|
|
61
|
+
dados.chaveAcesso,
|
|
62
|
+
NFE_EVENTOS.CARTA_CORRECAO,
|
|
63
|
+
dados.idLote
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
return { tipo: 'carta_correcao', xmlGerado: xml, xmlAssinado, retorno };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Envia evento já assinado (mock ou produção).
|
|
71
|
+
* @param {string} xmlAssinado
|
|
72
|
+
* @param {string} chaveAcesso
|
|
73
|
+
* @param {string} tpEvento
|
|
74
|
+
*/
|
|
75
|
+
async enviar(xmlAssinado, chaveAcesso, tpEvento) {
|
|
76
|
+
return this.soapService.enviarEvento(xmlAssinado, chaveAcesso, tpEvento);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Payload para persistência de evento no banco.
|
|
81
|
+
* @param {Awaited<ReturnType<NFeEventoService['cancelar']>>} resultado
|
|
82
|
+
*/
|
|
83
|
+
toPersistencePayload(resultado) {
|
|
84
|
+
return {
|
|
85
|
+
chave_acesso: resultado.retorno.chaveAcesso ?? null,
|
|
86
|
+
tipo_evento: resultado.tipo,
|
|
87
|
+
xml_evento: resultado.xmlAssinado,
|
|
88
|
+
xml_retorno: resultado.retorno.xmlRetorno,
|
|
89
|
+
protocolo: resultado.retorno.protocolo,
|
|
90
|
+
status: resultado.retorno.cStat,
|
|
91
|
+
motivo: resultado.retorno.xMotivo,
|
|
92
|
+
registrado_em: new Date(),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { NFeXmlService } from './NFeXmlService.js';
|
|
2
|
+
import { NFeSignService } from './NFeSignService.js';
|
|
3
|
+
import { NFeSoapService } from './NFeSoapService.js';
|
|
4
|
+
import { NFeEventoService } from './NFeEventoService.js';
|
|
5
|
+
import { GhinfeError } from '../../errors/GhinfeError.js';
|
|
6
|
+
import { gerarChaveAcesso, validarChaveAcesso } from '../../utils/chave.utils.js';
|
|
7
|
+
import { SefazEndpointResolver } from '../../sefaz/SefazEndpointResolver.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @param {{
|
|
11
|
+
* mock?: boolean,
|
|
12
|
+
* uf?: string,
|
|
13
|
+
* ambiente?: number,
|
|
14
|
+
* sefazEndpoint?: string,
|
|
15
|
+
* sefazEventoEndpoint?: string,
|
|
16
|
+
* mockScenario?: string,
|
|
17
|
+
* mockEventoScenario?: string,
|
|
18
|
+
* }} options
|
|
19
|
+
*/
|
|
20
|
+
function buildSoapOptions(options) {
|
|
21
|
+
if (options.mock ?? true) {
|
|
22
|
+
return {
|
|
23
|
+
mock: true,
|
|
24
|
+
endpoint: options.sefazEndpoint,
|
|
25
|
+
eventoEndpoint: options.sefazEventoEndpoint,
|
|
26
|
+
mockScenario: options.mockScenario,
|
|
27
|
+
mockEventoScenario: options.mockEventoScenario,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (options.sefazEndpoint) {
|
|
32
|
+
return {
|
|
33
|
+
mock: false,
|
|
34
|
+
endpoint: options.sefazEndpoint,
|
|
35
|
+
eventoEndpoint: options.sefazEventoEndpoint,
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (options.uf) {
|
|
40
|
+
const urls = SefazEndpointResolver.paraNFe(options.uf, options.ambiente ?? 2);
|
|
41
|
+
return { mock: false, endpoint: urls.endpoint, eventoEndpoint: urls.eventoEndpoint };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { mock: true };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Orquestrador principal da NF-e — une geração, assinatura e envio.
|
|
49
|
+
*/
|
|
50
|
+
export class NFeService {
|
|
51
|
+
/**
|
|
52
|
+
* @param {{
|
|
53
|
+
* mock?: boolean,
|
|
54
|
+
* uf?: string,
|
|
55
|
+
* ambiente?: number,
|
|
56
|
+
* sefazEndpoint?: string,
|
|
57
|
+
* sefazEventoEndpoint?: string,
|
|
58
|
+
* mockScenario?: string,
|
|
59
|
+
* mockEventoScenario?: string,
|
|
60
|
+
* validarXsd?: boolean,
|
|
61
|
+
* }} [options]
|
|
62
|
+
*/
|
|
63
|
+
constructor(options = {}) {
|
|
64
|
+
this.options = options;
|
|
65
|
+
const soapOptions = buildSoapOptions(options);
|
|
66
|
+
|
|
67
|
+
this.xmlService = new NFeXmlService({ validarXsd: options.validarXsd });
|
|
68
|
+
this.signService = new NFeSignService();
|
|
69
|
+
this.soapService = new NFeSoapService(soapOptions);
|
|
70
|
+
this.eventoService = new NFeEventoService(soapOptions);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** Resolve endpoints SEFAZ para a UF do documento. */
|
|
74
|
+
configurarUf(uf, ambiente = 2) {
|
|
75
|
+
const urls = SefazEndpointResolver.paraNFe(uf, ambiente);
|
|
76
|
+
this.soapService.endpoint = urls.endpoint;
|
|
77
|
+
this.soapService.eventoEndpoint = urls.eventoEndpoint;
|
|
78
|
+
this.soapService.mock = false;
|
|
79
|
+
this.eventoService.soapService.endpoint = urls.endpoint;
|
|
80
|
+
this.eventoService.soapService.eventoEndpoint = urls.eventoEndpoint;
|
|
81
|
+
this.eventoService.soapService.mock = false;
|
|
82
|
+
return urls;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** @param {Parameters<typeof gerarChaveAcesso>[0]} params */
|
|
86
|
+
gerarChaveAcesso(params) {
|
|
87
|
+
return gerarChaveAcesso(params);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** @param {import('../types/NFeDocument.js').NFeDocument} documento */
|
|
91
|
+
resolverDocumento(documento) {
|
|
92
|
+
if (documento.chaveAcesso) {
|
|
93
|
+
if (!validarChaveAcesso(documento.chaveAcesso)) {
|
|
94
|
+
throw new GhinfeError('chaveAcesso com dígito verificador inválido', 'CHAVE_DV_INVALIDO');
|
|
95
|
+
}
|
|
96
|
+
return documento;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const chaveAcesso = gerarChaveAcesso({
|
|
100
|
+
uf: documento.ide.uf,
|
|
101
|
+
cnpj: documento.emitente.cnpj,
|
|
102
|
+
modelo: '55',
|
|
103
|
+
serie: documento.ide.serie ?? 1,
|
|
104
|
+
numero: documento.ide.numero,
|
|
105
|
+
tipoEmissao: documento.ide.tipoEmissao ?? 1,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return { ...documento, chaveAcesso };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async emitir(documento, certificado, idLote) {
|
|
112
|
+
const doc = this.resolverDocumento(documento);
|
|
113
|
+
|
|
114
|
+
if (!this.soapService.mock && this.options.uf === undefined && !this.options.sefazEndpoint) {
|
|
115
|
+
this.configurarUf(doc.ide.uf, doc.ambiente ?? 2);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const xml = this.xmlService.gerarXml(doc);
|
|
119
|
+
const xmlAssinado = this.signService.assinar(xml, certificado.pfx, certificado.senha);
|
|
120
|
+
const retorno = await this.soapService.autorizar(xmlAssinado, doc.chaveAcesso, idLote);
|
|
121
|
+
|
|
122
|
+
return { chaveAcesso: doc.chaveAcesso, xmlGerado: xml, xmlAssinado, retorno };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
gerarXml(documento, options = {}) {
|
|
126
|
+
const doc = this.resolverDocumento(documento);
|
|
127
|
+
return this.xmlService.gerarXml(doc, options);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
gerarXmlAssinado(documento, certificado) {
|
|
131
|
+
const doc = this.resolverDocumento(documento);
|
|
132
|
+
const xml = this.xmlService.gerarXml(doc);
|
|
133
|
+
return this.signService.assinar(xml, certificado.pfx, certificado.senha);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async enviar(xmlAssinado, chaveAcesso) {
|
|
137
|
+
return this.soapService.autorizar(xmlAssinado, chaveAcesso);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
validarXsd(xml, options = {}) {
|
|
141
|
+
return this.xmlService.validarXml(xml, options);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async cancelar(dados, certificado) {
|
|
145
|
+
return this.eventoService.cancelar(dados, certificado);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async cartaCorrecao(dados, certificado) {
|
|
149
|
+
return this.eventoService.cartaCorrecao(dados, certificado);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
gerarXmlCancelamento(dados) {
|
|
153
|
+
return this.eventoService.gerarXmlCancelamento(dados);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
gerarXmlCartaCorrecao(dados) {
|
|
157
|
+
return this.eventoService.gerarXmlCartaCorrecao(dados);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async enviarEventoMock(xmlEvento, chaveAcesso, tpEvento) {
|
|
161
|
+
return this.eventoService.enviar(xmlEvento, chaveAcesso, tpEvento);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Consulta status do webservice SEFAZ (NfeStatusServico4).
|
|
166
|
+
* @param {string} [uf]
|
|
167
|
+
* @param {number} [ambiente]
|
|
168
|
+
*/
|
|
169
|
+
async consultarStatus(uf, ambiente) {
|
|
170
|
+
const ufConsulta = uf ?? this.options.uf;
|
|
171
|
+
if (!ufConsulta) {
|
|
172
|
+
throw new GhinfeError('UF é obrigatória para consultar status SEFAZ', 'STATUS_UF_REQUIRED');
|
|
173
|
+
}
|
|
174
|
+
return this.soapService.consultarStatus(ufConsulta, ambiente ?? this.options.ambiente ?? 2);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
toPersistencePayload(resultado) {
|
|
178
|
+
if (!resultado.retorno.autorizado) {
|
|
179
|
+
throw new GhinfeError('Não é possível persistir NF-e não autorizada', 'NFE_NOT_AUTHORIZED', resultado.retorno);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
chave_acesso: resultado.chaveAcesso,
|
|
184
|
+
xml_nfe: resultado.xmlAssinado,
|
|
185
|
+
xml_protocolo: resultado.retorno.xmlProtocolo,
|
|
186
|
+
protocolo: resultado.retorno.protocolo,
|
|
187
|
+
status: resultado.retorno.cStat,
|
|
188
|
+
motivo: resultado.retorno.xMotivo,
|
|
189
|
+
autorizado_em: new Date(),
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
toEventoPersistencePayload(resultado) {
|
|
194
|
+
return this.eventoService.toPersistencePayload(resultado);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { SignedXml } from 'xml-crypto';
|
|
2
|
+
import { DOMParser } from '@xmldom/xmldom';
|
|
3
|
+
import { loadPfxCertificate } from '../../utils/certificate.utils.js';
|
|
4
|
+
import { GhinfeError } from '../../errors/GhinfeError.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Serviço de assinatura digital XML-DSig para NF-e (certificado A1 .pfx).
|
|
8
|
+
* Assina o elemento infNFe com algoritmo exigido pela SEFAZ.
|
|
9
|
+
*/
|
|
10
|
+
export class NFeSignService {
|
|
11
|
+
/**
|
|
12
|
+
* Assina o XML da NF-e usando certificado A1.
|
|
13
|
+
* @param {string} xmlNFe XML da NF-e sem assinatura
|
|
14
|
+
* @param {Buffer|string} pfxBuffer Conteúdo do arquivo .pfx
|
|
15
|
+
* @param {string} pfxPassword Senha do certificado
|
|
16
|
+
* @returns {string} XML assinado (NFe + Signature)
|
|
17
|
+
*/
|
|
18
|
+
assinar(xmlNFe, pfxBuffer, pfxPassword) {
|
|
19
|
+
return this.#assinarElemento(xmlNFe, 'infNFe', pfxBuffer, pfxPassword);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Assina XML de evento NF-e (cancelamento, CC-e, etc.).
|
|
24
|
+
* @param {string} xmlEvento
|
|
25
|
+
* @param {Buffer|string} pfxBuffer
|
|
26
|
+
* @param {string} pfxPassword
|
|
27
|
+
* @returns {string}
|
|
28
|
+
*/
|
|
29
|
+
assinarEvento(xmlEvento, pfxBuffer, pfxPassword) {
|
|
30
|
+
return this.#assinarElemento(xmlEvento, 'infEvento', pfxBuffer, pfxPassword);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Assina XML de RPS/NFS-e (elemento InfRps — ABRASF e derivados).
|
|
35
|
+
* @param {string} xmlRps
|
|
36
|
+
* @param {Buffer|string} pfxBuffer
|
|
37
|
+
* @param {string} pfxPassword
|
|
38
|
+
* @returns {string}
|
|
39
|
+
*/
|
|
40
|
+
assinarRps(xmlRps, pfxBuffer, pfxPassword) {
|
|
41
|
+
let tag = 'InfRps';
|
|
42
|
+
if (xmlRps.includes('InfDeclaracaoPrestacaoServico')) tag = 'InfDeclaracaoPrestacaoServico';
|
|
43
|
+
else if (xmlRps.includes('<infRps') || xmlRps.includes('infRps ')) tag = 'infRps';
|
|
44
|
+
else if (xmlRps.includes('<InfRps') || xmlRps.includes('InfRps ')) tag = 'InfRps';
|
|
45
|
+
return this.#assinarElemento(xmlRps, tag, pfxBuffer, pfxPassword);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {string} xml
|
|
50
|
+
* @param {string} tagName
|
|
51
|
+
* @param {Buffer|string} pfxBuffer
|
|
52
|
+
* @param {string} pfxPassword
|
|
53
|
+
*/
|
|
54
|
+
#assinarElemento(xml, tagName, pfxBuffer, pfxPassword) {
|
|
55
|
+
const { privateKeyPem, certificatePem } = loadPfxCertificate(pfxBuffer, pfxPassword);
|
|
56
|
+
|
|
57
|
+
const doc = new DOMParser().parseFromString(xml, 'text/xml');
|
|
58
|
+
const elemento = doc.getElementsByTagName(tagName)[0];
|
|
59
|
+
|
|
60
|
+
if (!elemento) {
|
|
61
|
+
throw new GhinfeError(`Elemento ${tagName} não encontrado no XML`, `SIGN_${tagName}_NOT_FOUND`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const id = elemento.getAttribute('Id');
|
|
65
|
+
if (!id) {
|
|
66
|
+
throw new GhinfeError(`Atributo Id do ${tagName} é obrigatório para assinatura`, 'SIGN_MISSING_ID');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const signedXml = new SignedXml({
|
|
70
|
+
privateKey: privateKeyPem,
|
|
71
|
+
publicCert: certificatePem,
|
|
72
|
+
signatureAlgorithm: 'http://www.w3.org/2000/09/xmldsig#rsa-sha1',
|
|
73
|
+
canonicalizationAlgorithm: 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315',
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
signedXml.addReference({
|
|
77
|
+
xpath: `//*[local-name()='${tagName}']`,
|
|
78
|
+
transforms: [
|
|
79
|
+
'http://www.w3.org/2000/09/xmldsig#enveloped-signature',
|
|
80
|
+
'http://www.w3.org/TR/2001/REC-xml-c14n-20010315',
|
|
81
|
+
],
|
|
82
|
+
digestAlgorithm: 'http://www.w3.org/2000/09/xmldsig#sha1',
|
|
83
|
+
uri: `#${id}`,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
signedXml.computeSignature(xml, {
|
|
87
|
+
location: { reference: `//*[local-name()='${tagName}']`, action: 'after' },
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
return signedXml.getSignedXml();
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Extrai informações do certificado para validação pré-envio.
|
|
95
|
+
* @param {Buffer|string} pfxBuffer
|
|
96
|
+
* @param {string} pfxPassword
|
|
97
|
+
*/
|
|
98
|
+
obterInfoCertificado(pfxBuffer, pfxPassword) {
|
|
99
|
+
const { certificate } = loadPfxCertificate(pfxBuffer, pfxPassword);
|
|
100
|
+
const subject = certificate.subject.attributes
|
|
101
|
+
.map((attr) => `${attr.shortName}=${attr.value}`)
|
|
102
|
+
.join(', ');
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
subject,
|
|
106
|
+
validoDe: certificate.validity.notBefore,
|
|
107
|
+
validoAte: certificate.validity.notAfter,
|
|
108
|
+
serialNumber: certificate.serialNumber,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|