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.
Files changed (80) hide show
  1. package/.env.example +15 -0
  2. package/LICENSE +21 -0
  3. package/README.md +413 -0
  4. package/docs/developer-guide.html +421 -0
  5. package/docs/openapi.yaml +428 -0
  6. package/examples/emitir-nfce.js +51 -0
  7. package/examples/emitir-nfe.js +83 -0
  8. package/examples/emitir-nfse.js +46 -0
  9. package/examples/endpoints-sefaz.js +14 -0
  10. package/examples/eventos-nfe.js +49 -0
  11. package/examples/gerar-chave.js +25 -0
  12. package/examples/gerar-danfe.js +75 -0
  13. package/examples/layouts-nfse.js +51 -0
  14. package/examples/mock-sefaz.js +52 -0
  15. package/examples/nfse-eventos.js +57 -0
  16. package/examples/reforma-ibscbs.js +27 -0
  17. package/examples/reforma-nfe-xml.js +52 -0
  18. package/examples/status-sefaz.js +12 -0
  19. package/examples/validar-xsd.js +48 -0
  20. package/package.json +68 -0
  21. package/schemas/nfe/PL_009_V4/consReciNFe_v4.00.xsd +9 -0
  22. package/schemas/nfe/PL_009_V4/consSitNFe_v4.00.xsd +9 -0
  23. package/schemas/nfe/PL_009_V4/consStatServ_v4.00.xsd +9 -0
  24. package/schemas/nfe/PL_009_V4/enviNFe_v3.10.xsd +9 -0
  25. package/schemas/nfe/PL_009_V4/enviNFe_v4.00.xsd +9 -0
  26. package/schemas/nfe/PL_009_V4/inutNFe_v4.00.xsd +9 -0
  27. package/schemas/nfe/PL_009_V4/leiauteNFe_v4.00.xsd +7339 -0
  28. package/schemas/nfe/PL_009_V4/nfe_v4.00.xsd +9 -0
  29. package/schemas/nfe/PL_009_V4/tiposBasico_v4.00.xsd +598 -0
  30. package/schemas/nfe/PL_009_V4/xmldsig-core-schema_v1.01.xsd +98 -0
  31. package/scripts/download-schemas.js +39 -0
  32. package/scripts/test-xsd.js +34 -0
  33. package/src/api/server.js +316 -0
  34. package/src/constants/sefaz.js +69 -0
  35. package/src/danfe/DanfeService.js +205 -0
  36. package/src/danfe/xml-extract.js +69 -0
  37. package/src/errors/GhinfeError.js +13 -0
  38. package/src/errors/SefazError.js +25 -0
  39. package/src/errors/XsdValidationError.js +15 -0
  40. package/src/index.js +60 -0
  41. package/src/nfce/builders/NFCeXmlBuilder.js +178 -0
  42. package/src/nfce/constants/qrcode.js +26 -0
  43. package/src/nfce/services/NFCeService.js +188 -0
  44. package/src/nfce/services/NFCeXmlService.js +38 -0
  45. package/src/nfce/types/NFCeDocument.js +42 -0
  46. package/src/nfce/utils/qrcode.utils.js +92 -0
  47. package/src/nfe/builders/NFeEventoXmlBuilder.js +110 -0
  48. package/src/nfe/builders/NFeXmlBuilder.js +215 -0
  49. package/src/nfe/mocks/sefaz-responses.js +195 -0
  50. package/src/nfe/services/NFeEventoService.js +95 -0
  51. package/src/nfe/services/NFeService.js +196 -0
  52. package/src/nfe/services/NFeSignService.js +111 -0
  53. package/src/nfe/services/NFeSoapService.js +437 -0
  54. package/src/nfe/services/NFeXmlService.js +41 -0
  55. package/src/nfe/services/NFeXsdService.js +56 -0
  56. package/src/nfe/types/NFeDocument.js +92 -0
  57. package/src/nfe/types/NFeEvento.js +44 -0
  58. package/src/nfse/NFSeMunicipioResolver.js +42 -0
  59. package/src/nfse/builders/NFSeEventoXmlBuilder.js +92 -0
  60. package/src/nfse/builders/NFSeXmlBuilder.js +16 -0
  61. package/src/nfse/constants/municipios.js +82 -0
  62. package/src/nfse/layouts/adapters.js +202 -0
  63. package/src/nfse/mocks/nfse-responses.js +142 -0
  64. package/src/nfse/services/NFSeService.js +191 -0
  65. package/src/nfse/services/NFSeSoapService.js +232 -0
  66. package/src/nfse/services/NFSeXmlService.js +12 -0
  67. package/src/nfse/types/NFSeDocument.js +54 -0
  68. package/src/queue/NFCeQueue.js +63 -0
  69. package/src/queue/NFSeQueue.js +59 -0
  70. package/src/queue/NFeQueue.js +76 -0
  71. package/src/reforma/ibscbs.js +175 -0
  72. package/src/sefaz/SefazEndpointResolver.js +95 -0
  73. package/src/sefaz/endpoints.registry.js +205 -0
  74. package/src/shared/fiscal.helpers.js +53 -0
  75. package/src/utils/certificate.utils.js +47 -0
  76. package/src/utils/chave.utils.js +117 -0
  77. package/src/utils/date.utils.js +26 -0
  78. package/src/utils/xml.utils.js +44 -0
  79. package/src/validation/XsdValidator.js +114 -0
  80. 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
+ }