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,188 @@
1
+ import { NFCeXmlService } from './NFCeXmlService.js';
2
+ import { NFeSignService } from '../../nfe/services/NFeSignService.js';
3
+ import { NFeSoapService } from '../../nfe/services/NFeSoapService.js';
4
+ import { GhinfeError } from '../../errors/GhinfeError.js';
5
+ import { gerarChaveAcesso, validarChaveAcesso } from '../../utils/chave.utils.js';
6
+ import { SefazEndpointResolver } from '../../sefaz/SefazEndpointResolver.js';
7
+ import {
8
+ extrairDigestValor,
9
+ gerarUrlQrCodeV3,
10
+ obterUrlChave,
11
+ inserirInfNFeSupl,
12
+ } from '../utils/qrcode.utils.js';
13
+
14
+ /**
15
+ * Orquestrador da NFC-e (modelo 65).
16
+ */
17
+ export class NFCeService {
18
+ /**
19
+ * @param {{
20
+ * mock?: boolean,
21
+ * uf?: string,
22
+ * ambiente?: number,
23
+ * sefazEndpoint?: string,
24
+ * mockScenario?: string,
25
+ * validarXsd?: boolean,
26
+ * }} [options]
27
+ */
28
+ constructor(options = {}) {
29
+ this.options = options;
30
+ this.xmlService = new NFCeXmlService({ validarXsd: options.validarXsd });
31
+ this.signService = new NFeSignService();
32
+
33
+ let endpoint = options.sefazEndpoint;
34
+ if (!endpoint && options.uf && !options.mock) {
35
+ endpoint = SefazEndpointResolver.paraNFCe(options.uf, options.ambiente ?? 2).endpoint;
36
+ }
37
+
38
+ this.soapService = new NFeSoapService({
39
+ mock: options.mock ?? true,
40
+ endpoint,
41
+ mockScenario: options.mockScenario,
42
+ });
43
+ }
44
+
45
+ /** @param {string} uf @param {number} [ambiente] */
46
+ configurarUf(uf, ambiente = 2) {
47
+ const urls = SefazEndpointResolver.paraNFCe(uf, ambiente);
48
+ this.soapService.endpoint = urls.endpoint;
49
+ this.soapService.mock = false;
50
+ return urls;
51
+ }
52
+
53
+ /** @param {import('../types/NFCeDocument.js').NFCeDocumento} documento */
54
+ resolverDocumento(documento) {
55
+ if (documento.chaveAcesso) {
56
+ if (!validarChaveAcesso(documento.chaveAcesso)) {
57
+ throw new GhinfeError('chaveAcesso com dígito verificador inválido', 'CHAVE_DV_INVALIDO');
58
+ }
59
+ return documento;
60
+ }
61
+
62
+ const chaveAcesso = gerarChaveAcesso({
63
+ uf: documento.ide.uf,
64
+ cnpj: documento.emitente.cnpj,
65
+ modelo: '65',
66
+ serie: documento.ide.serie ?? 1,
67
+ numero: documento.ide.numero,
68
+ });
69
+
70
+ return { ...documento, chaveAcesso };
71
+ }
72
+
73
+ /** @param {import('../types/NFCeDocument.js').NFCeDocumento} documento */
74
+ #calcularTotais(documento) {
75
+ const valorProdutos = documento.itens.reduce((s, i) => s + i.valorTotal, 0);
76
+ const valorIcms = documento.itens.reduce((s, i) => s + (i.valorIcms ?? 0), 0);
77
+ const valorDesc = documento.itens.reduce((s, i) => s + (i.valorDesconto ?? 0), 0);
78
+ return { valorNF: valorProdutos - valorDesc, valorIcms };
79
+ }
80
+
81
+ /**
82
+ * Gera XML base (infNFe) sem QR Code — QR é adicionado após assinatura.
83
+ * @param {import('../types/NFCeDocument.js').NFCeDocumento} documento
84
+ * @param {{ validarXsd?: boolean }} [options]
85
+ */
86
+ gerarXml(documento, options = {}) {
87
+ const doc = this.resolverDocumento(documento);
88
+ return this.xmlService.gerarXml(doc, { ...options, validarXsd: false });
89
+ }
90
+
91
+ /**
92
+ * Adiciona infNFeSupl com QR Code v3 ao XML já assinado.
93
+ * @param {string} xmlAssinado
94
+ * @param {import('../types/NFCeDocument.js').NFCeDocumento} documento
95
+ */
96
+ adicionarQrCode(xmlAssinado, documento) {
97
+ const doc = this.resolverDocumento(documento);
98
+ const { valorNF, valorIcms } = this.#calcularTotais(doc);
99
+ const digVal = extrairDigestValor(xmlAssinado);
100
+ const cDest = (doc.destinatario?.cnpj ?? doc.destinatario?.cpf ?? '').replace(/\D/g, '');
101
+
102
+ const qrCode = gerarUrlQrCodeV3({
103
+ chaveAcesso: doc.chaveAcesso,
104
+ uf: doc.ide.uf,
105
+ ambiente: doc.ambiente ?? 2,
106
+ cDest,
107
+ valorNF,
108
+ valorICMS: valorIcms,
109
+ digVal,
110
+ idCSC: doc.csc.idCSC,
111
+ csc: doc.csc.codigo,
112
+ });
113
+
114
+ const urlChave = obterUrlChave(doc.ide.uf, doc.ambiente ?? 2);
115
+ const xmlCompleto = inserirInfNFeSupl(xmlAssinado, qrCode, urlChave);
116
+
117
+ return { xmlCompleto, qrCode, urlChave };
118
+ }
119
+
120
+ /**
121
+ * @param {import('../types/NFCeDocument.js').NFCeDocumento} documento
122
+ * @param {{ pfx: Buffer|string, senha: string }} certificado
123
+ * @param {{ validarXsd?: boolean }} [options]
124
+ */
125
+ gerarXmlAssinado(documento, certificado, options = {}) {
126
+ const doc = this.resolverDocumento(documento);
127
+ const xml = this.xmlService.gerarXml(doc, { validarXsd: false });
128
+ const xmlAssinado = this.signService.assinar(xml, certificado.pfx, certificado.senha);
129
+ const { xmlCompleto, qrCode } = this.adicionarQrCode(xmlAssinado, doc);
130
+
131
+ if (options.validarXsd) {
132
+ this.xmlService.validarXml(xmlCompleto);
133
+ }
134
+
135
+ return { xml: xmlCompleto, qrCode };
136
+ }
137
+
138
+ /**
139
+ * @param {import('../types/NFCeDocument.js').NFCeDocumento} documento
140
+ * @param {{ pfx: Buffer|string, senha: string }} certificado
141
+ */
142
+ async emitir(documento, certificado) {
143
+ const doc = this.resolverDocumento(documento);
144
+
145
+ if (!this.soapService.mock && this.options.uf === undefined && !this.options.sefazEndpoint) {
146
+ this.configurarUf(doc.ide.uf, doc.ambiente ?? 2);
147
+ }
148
+
149
+ const { xml, qrCode } = this.gerarXmlAssinado(doc, certificado);
150
+ const retorno = await this.soapService.autorizar(xml, doc.chaveAcesso);
151
+
152
+ return {
153
+ chaveAcesso: doc.chaveAcesso,
154
+ qrCode,
155
+ xmlGerado: this.gerarXml(doc),
156
+ xmlAssinado: xml,
157
+ retorno,
158
+ };
159
+ }
160
+
161
+ /** @param {string} xml */
162
+ validarXsd(xml) {
163
+ return this.xmlService.validarXml(xml);
164
+ }
165
+
166
+ async enviar(xmlAssinado, chaveAcesso) {
167
+ return this.soapService.autorizar(xmlAssinado, chaveAcesso);
168
+ }
169
+
170
+ /** @param {Awaited<ReturnType<NFCeService['emitir']>>} resultado */
171
+ toPersistencePayload(resultado) {
172
+ if (!resultado.retorno.autorizado) {
173
+ throw new GhinfeError('NFC-e não autorizada', 'NFCE_NOT_AUTHORIZED', resultado.retorno);
174
+ }
175
+
176
+ return {
177
+ chave_acesso: resultado.chaveAcesso,
178
+ modelo: '65',
179
+ xml_nfce: resultado.xmlAssinado,
180
+ xml_protocolo: resultado.retorno.xmlProtocolo,
181
+ protocolo: resultado.retorno.protocolo,
182
+ qrcode_url: resultado.qrCode,
183
+ status: resultado.retorno.cStat,
184
+ motivo: resultado.retorno.xMotivo,
185
+ autorizado_em: new Date(),
186
+ };
187
+ }
188
+ }
@@ -0,0 +1,38 @@
1
+ import { NFCeXmlBuilder } from '../builders/NFCeXmlBuilder.js';
2
+ import { NFeXsdService } from '../../nfe/services/NFeXsdService.js';
3
+
4
+ /**
5
+ * Serviço de geração e validação XSD da NFC-e.
6
+ */
7
+ export class NFCeXmlService {
8
+ /**
9
+ * @param {{ validarXsd?: boolean }} [options]
10
+ */
11
+ constructor(options = {}) {
12
+ this.builder = new NFCeXmlBuilder();
13
+ this.xsdService = new NFeXsdService();
14
+ this.validarXsd = options.validarXsd ?? false;
15
+ }
16
+
17
+ /**
18
+ * @param {import('../types/NFCeDocument.js').NFCeDocumento} documento
19
+ * @param {{ validarXsd?: boolean }} [options]
20
+ */
21
+ gerarXml(documento, options = {}) {
22
+ const xml = this.builder.build(documento);
23
+ const deveValidar = options.validarXsd ?? this.validarXsd;
24
+
25
+ if (deveValidar) {
26
+ this.xsdService.validarNFCe(xml, { preAssinatura: true });
27
+ }
28
+
29
+ return xml;
30
+ }
31
+
32
+ /**
33
+ * @param {string} xml
34
+ */
35
+ validarXml(xml) {
36
+ return this.xsdService.validarNFCe(xml);
37
+ }
38
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @typedef {import('../../nfe/types/NFeDocument.js').NFeEndereco} NFCeEndereco
3
+ * @typedef {import('../../nfe/types/NFeDocument.js').NFeEmitente} NFCeEmitente
4
+ * @typedef {import('../../nfe/types/NFeDocument.js').NFeItem} NFCeItem
5
+ * @typedef {import('../../nfe/types/NFeDocument.js').NFePagamento} NFCePagamento
6
+ */
7
+
8
+ /**
9
+ * @typedef {Object} NFCeDestinatario
10
+ * @property {string} [cpf]
11
+ * @property {string} [cnpj]
12
+ * @property {string} [razaoSocial]
13
+ * @property {NFCeEndereco} [endereco]
14
+ */
15
+
16
+ /**
17
+ * @typedef {Object} NFCeCsc
18
+ * @property {string} idCSC Identificador do CSC (ex "000001")
19
+ * @property {string} codigo Código CSC fornecido pela SEFAZ
20
+ */
21
+
22
+ /**
23
+ * @typedef {Object} NFCeDocumento
24
+ * @property {string} [chaveAcesso]
25
+ * @property {number} [ambiente] 1=Produção, 2=Homologação
26
+ * @property {Object} ide
27
+ * @property {string} ide.uf
28
+ * @property {string} ide.codigoMunicipio
29
+ * @property {number} [ide.serie]
30
+ * @property {number} ide.numero
31
+ * @property {string} [ide.naturezaOperacao]
32
+ * @property {number} [ide.presenca] 1=Presencial, 4=Entrega domicílio
33
+ * @property {NFCeEmitente} emitente
34
+ * @property {NFCeDestinatario} [destinatario]
35
+ * @property {NFCeItem[]} itens
36
+ * @property {NFCePagamento[]} [pagamentos]
37
+ * @property {NFCeCsc} csc Dados do CSC para QR Code
38
+ * @property {string} [informacoesComplementares]
39
+ * @property {string} [informacoesFisco]
40
+ */
41
+
42
+ export {};
@@ -0,0 +1,92 @@
1
+ import crypto from 'crypto';
2
+ import { NFCE_QRCODE_URLS, NFCE_URL_CHAVE } from '../constants/qrcode.js';
3
+ import { GhinfeError } from '../../errors/GhinfeError.js';
4
+
5
+ /**
6
+ * Extrai DigestValue da assinatura XML para compor QR Code v3.
7
+ * @param {string} xmlAssinado
8
+ */
9
+ export function extrairDigestValor(xmlAssinado) {
10
+ const match = xmlAssinado.match(/<DigestValue>([^<]+)<\/DigestValue>/);
11
+ return match?.[1] ?? '';
12
+ }
13
+
14
+ /**
15
+ * Gera hash QR Code NFC-e versão 3.
16
+ * SHA1(chave + versao + tpAmb + cDest + vNF + vICMS + digVal + idCSC + CSC)
17
+ */
18
+ export function gerarHashQrCodeV3(chaveAcesso, ambiente, cDest, vNF, vICMS, digVal, idCSC, csc) {
19
+ const vNFStr = Number(vNF).toFixed(2);
20
+ const vICMSStr = Number(vICMS).toFixed(2);
21
+ const id = idCSC.padStart(6, '0');
22
+ const input = `${chaveAcesso}3${ambiente}${cDest}${vNFStr}${vICMSStr}${digVal}${id}${csc}`;
23
+ return crypto.createHash('sha1').update(input).digest('hex').toUpperCase();
24
+ }
25
+
26
+ /**
27
+ * Monta URL do QR Code NFC-e versão 3 (layout exigido pelo XSD PL_009_V4).
28
+ * @param {{
29
+ * chaveAcesso: string,
30
+ * uf: string,
31
+ * ambiente?: number,
32
+ * cDest?: string,
33
+ * valorNF: number,
34
+ * valorICMS?: number,
35
+ * digVal: string,
36
+ * idCSC: string,
37
+ * csc: string,
38
+ * }} params
39
+ */
40
+ export function gerarUrlQrCodeV3(params) {
41
+ const ambiente = params.ambiente ?? 2;
42
+ const uf = params.uf.toUpperCase();
43
+ const baseUrl = NFCE_QRCODE_URLS[uf]?.[ambiente];
44
+
45
+ if (!baseUrl) {
46
+ throw new GhinfeError(
47
+ `URL de QR Code NFC-e não configurada para UF ${uf} ambiente ${ambiente}`,
48
+ 'NFCE_QRCODE_URL_MISSING'
49
+ );
50
+ }
51
+
52
+ const cDest = (params.cDest ?? '').replace(/\D/g, '');
53
+ const vNFStr = Number(params.valorNF).toFixed(2);
54
+ const vICMSStr = Number(params.valorICMS ?? 0).toFixed(2);
55
+ const idCSC = params.idCSC.padStart(6, '0');
56
+ const hash = gerarHashQrCodeV3(
57
+ params.chaveAcesso,
58
+ ambiente,
59
+ cDest,
60
+ params.valorNF,
61
+ params.valorICMS ?? 0,
62
+ params.digVal,
63
+ idCSC,
64
+ params.csc
65
+ );
66
+
67
+ const p = `${params.chaveAcesso}|3|${ambiente}|${cDest}|${vNFStr}|${vICMSStr}|${params.digVal}|${idCSC}|${hash}`;
68
+ return `${baseUrl}?p=${p}`;
69
+ }
70
+
71
+ /**
72
+ * @param {string} uf
73
+ * @param {number} [ambiente]
74
+ */
75
+ export function obterUrlChave(uf, ambiente = 2) {
76
+ const url = NFCE_URL_CHAVE[uf.toUpperCase()]?.[ambiente];
77
+ if (!url) {
78
+ return NFCE_QRCODE_URLS[uf.toUpperCase()]?.[ambiente] ?? 'https://www.nfce.fazenda.sp.gov.br/consulta';
79
+ }
80
+ return url;
81
+ }
82
+
83
+ /**
84
+ * Insere infNFeSupl no XML assinado da NFC-e.
85
+ * @param {string} xmlAssinado
86
+ * @param {string} qrCodeUrl
87
+ * @param {string} urlChave
88
+ */
89
+ export function inserirInfNFeSupl(xmlAssinado, qrCodeUrl, urlChave) {
90
+ const supl = `<infNFeSupl><qrCode><![CDATA[${qrCodeUrl}]]></qrCode><urlChave>${urlChave}</urlChave></infNFeSupl>`;
91
+ return xmlAssinado.replace('</NFe>', `${supl}</NFe>`);
92
+ }
@@ -0,0 +1,110 @@
1
+ import { create } from 'xmlbuilder2';
2
+ import { UF_IBGE } from '../../constants/sefaz.js';
3
+ import { NFE_EVENTOS, CCE_COND_USO } from '../types/NFeEvento.js';
4
+ import { formatSefazDateTime } from '../../utils/date.utils.js';
5
+ import { GhinfeError } from '../../errors/GhinfeError.js';
6
+
7
+ const VERSAO_EVENTO = '1.00';
8
+
9
+ /**
10
+ * Monta XML de eventos NF-e (cancelamento, CC-e, etc.).
11
+ */
12
+ export class NFeEventoXmlBuilder {
13
+ /**
14
+ * @param {import('../types/NFeEvento.js').NFeCancelamento} dados
15
+ * @returns {string}
16
+ */
17
+ buildCancelamento(dados) {
18
+ this.#validarBase(dados);
19
+ if (!dados.protocolo) {
20
+ throw new GhinfeError('protocolo é obrigatório para cancelamento', 'CANCEL_PROTOCOLO_REQUIRED');
21
+ }
22
+ if (!dados.justificativa || dados.justificativa.trim().length < 15) {
23
+ throw new GhinfeError(
24
+ 'justificativa deve ter no mínimo 15 caracteres',
25
+ 'CANCEL_JUSTIFICATIVA_MIN'
26
+ );
27
+ }
28
+
29
+ return this.#buildEvento(dados, NFE_EVENTOS.CANCELAMENTO, (det) => {
30
+ det.ele('descEvento').txt('Cancelamento').up();
31
+ det.ele('nProt').txt(dados.protocolo).up();
32
+ det.ele('xJust').txt(dados.justificativa.trim()).up();
33
+ });
34
+ }
35
+
36
+ /**
37
+ * @param {import('../types/NFeEvento.js').NFeCartaCorrecao} dados
38
+ * @returns {string}
39
+ */
40
+ buildCartaCorrecao(dados) {
41
+ this.#validarBase(dados);
42
+ if (!dados.correcao || dados.correcao.trim().length < 15) {
43
+ throw new GhinfeError(
44
+ 'correcao deve ter no mínimo 15 caracteres',
45
+ 'CCE_TEXTO_MIN'
46
+ );
47
+ }
48
+ if (dados.correcao.length > 1000) {
49
+ throw new GhinfeError('correcao não pode exceder 1000 caracteres', 'CCE_TEXTO_MAX');
50
+ }
51
+
52
+ return this.#buildEvento(dados, NFE_EVENTOS.CARTA_CORRECAO, (det) => {
53
+ det.ele('descEvento').txt('Carta de Correcao').up();
54
+ det.ele('xCorrecao').txt(dados.correcao.trim()).up();
55
+ det.ele('xCondUso').txt(CCE_COND_USO).up();
56
+ });
57
+ }
58
+
59
+ /**
60
+ * @param {import('../types/NFeEvento.js').NFeEventoBase} dados
61
+ * @param {string} tpEvento
62
+ * @param {(det: import('xmlbuilder2').XMLBuilder) => void} buildDetEvento
63
+ */
64
+ #buildEvento(dados, tpEvento, buildDetEvento) {
65
+ const sequencia = dados.sequencia ?? 1;
66
+ const idLote = dados.idLote ?? '1';
67
+ const cOrgao = UF_IBGE[dados.uf.toUpperCase()] ?? '35';
68
+ const dhEvento = formatSefazDateTime();
69
+ const idInfEvento = `ID${tpEvento}${dados.chaveAcesso}${String(sequencia).padStart(2, '0')}`;
70
+
71
+ const root = create({ version: '1.0', encoding: 'UTF-8' })
72
+ .ele('envEvento', { xmlns: 'http://www.portalfiscal.inf.br/nfe', versao: VERSAO_EVENTO })
73
+ .ele('idLote').txt(idLote).up();
74
+
75
+ const evento = root.ele('evento', { versao: VERSAO_EVENTO });
76
+ const infEvento = evento.ele('infEvento', { Id: idInfEvento });
77
+
78
+ infEvento
79
+ .ele('cOrgao').txt(cOrgao).up()
80
+ .ele('tpAmb').txt(String(dados.ambiente ?? 2)).up()
81
+ .ele('CNPJ').txt(dados.cnpj.replace(/\D/g, '')).up()
82
+ .ele('chNFe').txt(dados.chaveAcesso).up()
83
+ .ele('dhEvento').txt(dhEvento).up()
84
+ .ele('tpEvento').txt(tpEvento).up()
85
+ .ele('nSeqEvento').txt(String(sequencia)).up()
86
+ .ele('verEvento').txt(VERSAO_EVENTO).up();
87
+
88
+ const detEvento = infEvento.ele('detEvento', { versao: VERSAO_EVENTO });
89
+ buildDetEvento(detEvento);
90
+ detEvento.up();
91
+ infEvento.up();
92
+ evento.up();
93
+
94
+ return root.end({ prettyPrint: false, headless: false });
95
+ }
96
+
97
+ /** @param {import('../types/NFeEvento.js').NFeEventoBase} dados */
98
+ #validarBase(dados) {
99
+ const chave = dados.chaveAcesso?.replace(/\D/g, '');
100
+ if (!chave || chave.length !== 44) {
101
+ throw new GhinfeError('chaveAcesso deve conter 44 dígitos', 'INVALID_CHAVE');
102
+ }
103
+ if (!dados.cnpj) {
104
+ throw new GhinfeError('cnpj do autor do evento é obrigatório', 'INVALID_CNPJ');
105
+ }
106
+ if (!dados.uf) {
107
+ throw new GhinfeError('uf é obrigatória', 'INVALID_UF');
108
+ }
109
+ }
110
+ }
@@ -0,0 +1,215 @@
1
+ import { create } from 'xmlbuilder2';
2
+ import { NFE_VERSAO } from '../../constants/sefaz.js';
3
+ import { formatSefazDateTime } from '../../utils/date.utils.js';
4
+ import { GhinfeError } from '../../errors/GhinfeError.js';
5
+ import {
6
+ reformaHabilitada,
7
+ normalizarItensReforma,
8
+ somarTotaisReforma,
9
+ appendTotaisIBSCBS,
10
+ appendIBSCBS,
11
+ } from '../../reforma/ibscbs.js';
12
+
13
+ /**
14
+ * Monta o XML da NF-e (layout 4.00).
15
+ * Com item.reforma ou reformaTributaria, injeta grupos IBS/CBS (PL_010 experimental).
16
+ * Atenção: validar XSD PL_009 com reforma ativa pode falhar — use validarXsd só sem reforma,
17
+ * ou aguarde schemas PL_010.
18
+ */
19
+ export class NFeXmlBuilder {
20
+ /**
21
+ * @param {import('../types/NFeDocument.js').NFeDocument} documento
22
+ * @returns {string}
23
+ */
24
+ build(documento) {
25
+ this.#validar(documento);
26
+
27
+ const comReforma = reformaHabilitada(documento);
28
+ const itens = comReforma ? normalizarItensReforma(documento.itens, true) : documento.itens;
29
+
30
+ const dhEmi = formatSefazDateTime();
31
+ const dhSaiEnt = formatSefazDateTime();
32
+ const idInfNFe = `NFe${documento.chaveAcesso}`;
33
+
34
+ const totalProdutos = itens.reduce((s, i) => s + i.valorTotal, 0);
35
+ const valorFrete = documento.frete?.valor ?? 0;
36
+ const valorIcms = itens.reduce((s, i) => s + (i.valorIcms ?? 0), 0);
37
+ const valorNf = totalProdutos + valorFrete;
38
+ const totaisReforma = comReforma ? somarTotaisReforma(itens) : null;
39
+
40
+ const root = create({ version: '1.0', encoding: 'UTF-8' })
41
+ .ele('NFe', { xmlns: 'http://www.portalfiscal.inf.br/nfe' })
42
+ .ele('infNFe', { versao: NFE_VERSAO, Id: idInfNFe });
43
+
44
+ const inf = root;
45
+
46
+ inf
47
+ .ele('ide')
48
+ .ele('cUF').txt(this.#codigoUf(documento.ide.uf)).up()
49
+ .ele('cNF').txt(documento.chaveAcesso.substring(35, 43)).up()
50
+ .ele('natOp').txt(documento.ide.naturezaOperacao ?? 'VENDA').up()
51
+ .ele('mod').txt('55').up()
52
+ .ele('serie').txt(String(documento.ide.serie ?? 1)).up()
53
+ .ele('nNF').txt(String(documento.ide.numero)).up()
54
+ .ele('dhEmi').txt(dhEmi).up()
55
+ .ele('dhSaiEnt').txt(dhSaiEnt).up()
56
+ .ele('tpNF').txt('1').up()
57
+ .ele('idDest').txt('1').up()
58
+ .ele('cMunFG').txt(documento.ide.codigoMunicipio).up()
59
+ .ele('tpImp').txt('1').up()
60
+ .ele('tpEmis').txt(String(documento.ide.tipoEmissao ?? 1)).up()
61
+ .ele('cDV').txt(documento.chaveAcesso.slice(-1)).up()
62
+ .ele('tpAmb').txt(String(documento.ambiente ?? 2)).up()
63
+ .ele('finNFe').txt(String(documento.ide.finalidade ?? 1)).up()
64
+ .ele('indFinal').txt(String(documento.ide.consumidorFinal ?? 1)).up()
65
+ .ele('indPres').txt(String(documento.ide.presenca ?? 1)).up()
66
+ .ele('procEmi').txt('0').up()
67
+ .ele('verProc').txt('ghinfe-0.6.0').up()
68
+ .up();
69
+
70
+ const emit = inf.ele('emit');
71
+ emit.ele('CNPJ').txt(documento.emitente.cnpj.replace(/\D/g, '')).up();
72
+ emit.ele('xNome').txt(documento.emitente.razaoSocial).up();
73
+ if (documento.emitente.nomeFantasia) emit.ele('xFant').txt(documento.emitente.nomeFantasia).up();
74
+ this.#appendEndereco(emit, 'enderEmit', documento.emitente.endereco);
75
+ emit.ele('IE').txt(documento.emitente.ie).up();
76
+ emit.ele('CRT').txt(documento.emitente.crt).up();
77
+ emit.up();
78
+
79
+ const dest = inf.ele('dest');
80
+ const docDest = (documento.destinatario.cnpj ?? documento.destinatario.cpf ?? '').replace(/\D/g, '');
81
+ if (docDest.length === 14) dest.ele('CNPJ').txt(docDest).up();
82
+ else if (docDest.length === 11) dest.ele('CPF').txt(docDest).up();
83
+ dest.ele('xNome').txt(documento.destinatario.razaoSocial).up();
84
+ this.#appendEndereco(dest, 'enderDest', documento.destinatario.endereco);
85
+ if (documento.destinatario.indIEDest !== undefined) {
86
+ dest.ele('indIEDest').txt(String(documento.destinatario.indIEDest)).up();
87
+ }
88
+ if (documento.destinatario.ie) dest.ele('IE').txt(documento.destinatario.ie).up();
89
+ dest.up();
90
+
91
+ itens.forEach((item, index) => {
92
+ const det = inf.ele('det', { nItem: String(index + 1) });
93
+ const prod = det.ele('prod');
94
+ prod.ele('cProd').txt(item.codigo).up();
95
+ prod.ele('cEAN').txt(item.ean ?? 'SEM GTIN').up();
96
+ prod.ele('xProd').txt(item.descricao).up();
97
+ prod.ele('NCM').txt(item.ncm).up();
98
+ prod.ele('CFOP').txt(item.cfop).up();
99
+ prod.ele('uCom').txt(item.unidade).up();
100
+ prod.ele('qCom').txt(this.#decimal(item.quantidade, 4)).up();
101
+ prod.ele('vUnCom').txt(this.#decimal(item.valorUnitario, 10)).up();
102
+ prod.ele('vProd').txt(this.#decimal(item.valorTotal, 2)).up();
103
+ prod.ele('cEANTrib').txt(item.ean ?? 'SEM GTIN').up();
104
+ prod.ele('uTrib').txt(item.unidade).up();
105
+ prod.ele('qTrib').txt(this.#decimal(item.quantidade, 4)).up();
106
+ prod.ele('vUnTrib').txt(this.#decimal(item.valorUnitario, 10)).up();
107
+ prod.ele('indTot').txt('1').up();
108
+ prod.up();
109
+
110
+ const imposto = det.ele('imposto');
111
+ const icms = imposto.ele('ICMS').ele('ICMS00');
112
+ icms.ele('orig').txt('0').up();
113
+ icms.ele('CST').txt(item.cst ?? '00').up();
114
+ icms.ele('modBC').txt('0').up();
115
+ icms.ele('vBC').txt(this.#decimal(item.valorTotal, 2)).up();
116
+ icms.ele('pICMS').txt(this.#decimal(item.aliquotaIcms ?? 0, 4)).up();
117
+ icms.ele('vICMS').txt(this.#decimal(item.valorIcms ?? 0, 2)).up();
118
+ icms.up().up();
119
+ imposto.ele('PIS').ele('PISNT').ele('CST').txt('07').up().up().up();
120
+ imposto.ele('COFINS').ele('COFINSNT').ele('CST').txt('07').up().up().up();
121
+ if (item.reforma) appendIBSCBS(imposto, item.reforma);
122
+ imposto.up();
123
+ det.up();
124
+ });
125
+
126
+ const totalParent = inf.ele('total');
127
+ const total = totalParent.ele('ICMSTot');
128
+ total
129
+ .ele('vBC').txt(this.#decimal(totalProdutos, 2)).up()
130
+ .ele('vICMS').txt(this.#decimal(valorIcms, 2)).up()
131
+ .ele('vICMSDeson').txt('0.00').up()
132
+ .ele('vFCP').txt('0.00').up()
133
+ .ele('vBCST').txt('0.00').up()
134
+ .ele('vST').txt('0.00').up()
135
+ .ele('vFCPST').txt('0.00').up()
136
+ .ele('vFCPSTRet').txt('0.00').up()
137
+ .ele('vProd').txt(this.#decimal(totalProdutos, 2)).up()
138
+ .ele('vFrete').txt(this.#decimal(valorFrete, 2)).up()
139
+ .ele('vSeg').txt('0.00').up()
140
+ .ele('vDesc').txt('0.00').up()
141
+ .ele('vII').txt('0.00').up()
142
+ .ele('vIPI').txt('0.00').up()
143
+ .ele('vIPIDevol').txt('0.00').up()
144
+ .ele('vPIS').txt('0.00').up()
145
+ .ele('vCOFINS').txt('0.00').up()
146
+ .ele('vOutro').txt('0.00').up()
147
+ .ele('vNF').txt(this.#decimal(valorNf, 2)).up();
148
+ total.up();
149
+ if (totaisReforma) appendTotaisIBSCBS(totalParent, totaisReforma);
150
+ totalParent.up();
151
+
152
+ const transp = inf.ele('transp');
153
+ transp.ele('modFrete').txt(String(documento.frete?.modalidade ?? 9)).up();
154
+ if (documento.frete?.transportadora) {
155
+ const transporta = transp.ele('transporta');
156
+ if (documento.frete.cnpjTransportadora) {
157
+ transporta.ele('CNPJ').txt(documento.frete.cnpjTransportadora.replace(/\D/g, '')).up();
158
+ }
159
+ transporta.ele('xNome').txt(documento.frete.transportadora).up();
160
+ transporta.up();
161
+ }
162
+ transp.up();
163
+
164
+ const pag = inf.ele('pag');
165
+ (documento.pagamentos ?? [{ forma: '01', valor: valorNf }]).forEach((p) => {
166
+ const detPag = pag.ele('detPag');
167
+ detPag.ele('tPag').txt(p.forma).up();
168
+ detPag.ele('vPag').txt(this.#decimal(p.valor, 2)).up();
169
+ detPag.up();
170
+ });
171
+ pag.up();
172
+
173
+ if (documento.informacoesComplementares) {
174
+ inf.ele('infAdic').ele('infCpl').txt(documento.informacoesComplementares).up().up();
175
+ }
176
+
177
+ return root.end({ prettyPrint: false, headless: false });
178
+ }
179
+
180
+ #validar(documento) {
181
+ if (!documento.chaveAcesso || documento.chaveAcesso.length !== 44) {
182
+ throw new GhinfeError('chaveAcesso deve conter 44 dígitos', 'INVALID_CHAVE');
183
+ }
184
+ if (!documento.emitente?.cnpj) throw new GhinfeError('emitente.cnpj é obrigatório', 'INVALID_EMITENTE');
185
+ if (!documento.itens?.length) throw new GhinfeError('A NF-e deve conter ao menos um item', 'INVALID_ITENS');
186
+ }
187
+
188
+ #appendEndereco(parent, tag, endereco) {
189
+ const node = parent.ele(tag);
190
+ node.ele('xLgr').txt(endereco.logradouro).up();
191
+ node.ele('nro').txt(endereco.numero).up();
192
+ if (endereco.complemento) node.ele('xCpl').txt(endereco.complemento).up();
193
+ node.ele('xBairro').txt(endereco.bairro).up();
194
+ node.ele('cMun').txt(endereco.codigoMunicipio).up();
195
+ node.ele('xMun').txt(endereco.municipio).up();
196
+ node.ele('UF').txt(endereco.uf).up();
197
+ node.ele('CEP').txt(endereco.cep.replace(/\D/g, '')).up();
198
+ if (endereco.telefone) node.ele('fone').txt(endereco.telefone.replace(/\D/g, '')).up();
199
+ node.up();
200
+ }
201
+
202
+ #codigoUf(uf) {
203
+ const map = {
204
+ AC: '12', AL: '27', AM: '13', AP: '16', BA: '29', CE: '23', DF: '53',
205
+ ES: '32', GO: '52', MA: '21', MG: '31', MS: '50', MT: '51', PA: '15',
206
+ PB: '25', PE: '26', PI: '22', PR: '41', RJ: '33', RN: '24', RO: '11',
207
+ RR: '14', RS: '43', SC: '42', SE: '28', SP: '35', TO: '17',
208
+ };
209
+ return map[uf.toUpperCase()] ?? '35';
210
+ }
211
+
212
+ #decimal(value, decimals) {
213
+ return Number(value).toFixed(decimals);
214
+ }
215
+ }