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,205 @@
|
|
|
1
|
+
import QRCode from 'qrcode';
|
|
2
|
+
import { extrairDadosDanfe, formatarChaveAcesso } from './xml-extract.js';
|
|
3
|
+
import { GhinfeError } from '../errors/GhinfeError.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Gera DANFE (NF-e) e DANFCE (NFC-e) em HTML imprimível.
|
|
7
|
+
* PDF: imprima o HTML ou use um conversor no sistema consumidor (puppeteer, etc.).
|
|
8
|
+
*/
|
|
9
|
+
export class DanfeService {
|
|
10
|
+
/**
|
|
11
|
+
* @param {string} xmlNFe XML autorizado (nfeProc) ou NFe assinada
|
|
12
|
+
* @param {{ protocolo?: string, titulo?: string }} [opcoes]
|
|
13
|
+
* @returns {Promise<{ html: string, dados: object, tipo: 'DANFE' }>}
|
|
14
|
+
*/
|
|
15
|
+
async gerarDanfe(xmlNFe, opcoes = {}) {
|
|
16
|
+
const dados = extrairDadosDanfe(xmlNFe);
|
|
17
|
+
if (!dados.chaveAcesso) {
|
|
18
|
+
throw new GhinfeError('Chave de acesso não encontrada no XML', 'DANFE_CHAVE_MISSING');
|
|
19
|
+
}
|
|
20
|
+
if (opcoes.protocolo) dados.protocolo = opcoes.protocolo;
|
|
21
|
+
|
|
22
|
+
const html = this.#renderDanfe(dados, opcoes.titulo ?? 'DANFE — Documento Auxiliar da NF-e');
|
|
23
|
+
return { html, dados, tipo: 'DANFE' };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} xmlNFCe
|
|
28
|
+
* @param {{ protocolo?: string, qrCodeUrl?: string, titulo?: string }} [opcoes]
|
|
29
|
+
* @returns {Promise<{ html: string, dados: object, tipo: 'DANFCE', qrCodeDataUrl?: string }>}
|
|
30
|
+
*/
|
|
31
|
+
async gerarDanfce(xmlNFCe, opcoes = {}) {
|
|
32
|
+
const dados = extrairDadosDanfe(xmlNFCe);
|
|
33
|
+
if (!dados.chaveAcesso) {
|
|
34
|
+
throw new GhinfeError('Chave de acesso não encontrada no XML', 'DANFCE_CHAVE_MISSING');
|
|
35
|
+
}
|
|
36
|
+
if (opcoes.protocolo) dados.protocolo = opcoes.protocolo;
|
|
37
|
+
if (opcoes.qrCodeUrl) dados.qrCode = opcoes.qrCodeUrl;
|
|
38
|
+
|
|
39
|
+
let qrCodeDataUrl = '';
|
|
40
|
+
if (dados.qrCode) {
|
|
41
|
+
qrCodeDataUrl = await QRCode.toDataURL(dados.qrCode, { width: 180, margin: 1 });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const html = this.#renderDanfce(dados, qrCodeDataUrl, opcoes.titulo ?? 'NFC-e — Cupom Fiscal Eletrônico');
|
|
45
|
+
return { html, dados, tipo: 'DANFCE', qrCodeDataUrl };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Detecta modelo e gera o documento adequado.
|
|
50
|
+
* @param {string} xml
|
|
51
|
+
* @param {object} [opcoes]
|
|
52
|
+
*/
|
|
53
|
+
async gerar(xml, opcoes = {}) {
|
|
54
|
+
const dados = extrairDadosDanfe(xml);
|
|
55
|
+
if (dados.modelo === '65') return this.gerarDanfce(xml, opcoes);
|
|
56
|
+
return this.gerarDanfe(xml, opcoes);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#esc(s) {
|
|
60
|
+
return String(s ?? '')
|
|
61
|
+
.replace(/&/g, '&')
|
|
62
|
+
.replace(/</g, '<')
|
|
63
|
+
.replace(/>/g, '>')
|
|
64
|
+
.replace(/"/g, '"');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
#renderDanfe(d, titulo) {
|
|
68
|
+
const chaveFmt = formatarChaveAcesso(d.chaveAcesso);
|
|
69
|
+
const linhas = d.itens.map((i, idx) => `
|
|
70
|
+
<tr>
|
|
71
|
+
<td>${idx + 1}</td>
|
|
72
|
+
<td>${this.#esc(i.codigo)}</td>
|
|
73
|
+
<td>${this.#esc(i.descricao)}</td>
|
|
74
|
+
<td>${this.#esc(i.ncm)}</td>
|
|
75
|
+
<td>${this.#esc(i.cfop)}</td>
|
|
76
|
+
<td>${this.#esc(i.unidade)}</td>
|
|
77
|
+
<td class="r">${this.#esc(i.quantidade)}</td>
|
|
78
|
+
<td class="r">${this.#esc(i.valorUnitario)}</td>
|
|
79
|
+
<td class="r">${this.#esc(i.valorTotal)}</td>
|
|
80
|
+
</tr>`).join('');
|
|
81
|
+
|
|
82
|
+
return `<!DOCTYPE html>
|
|
83
|
+
<html lang="pt-BR">
|
|
84
|
+
<head>
|
|
85
|
+
<meta charset="UTF-8"/>
|
|
86
|
+
<title>${this.#esc(titulo)}</title>
|
|
87
|
+
<style>
|
|
88
|
+
* { box-sizing: border-box; }
|
|
89
|
+
body { font-family: Arial, Helvetica, sans-serif; font-size: 11px; color: #111; margin: 16px; }
|
|
90
|
+
h1 { font-size: 14px; margin: 0 0 8px; text-align: center; }
|
|
91
|
+
.box { border: 1px solid #333; padding: 8px; margin-bottom: 8px; }
|
|
92
|
+
.row { display: flex; gap: 8px; }
|
|
93
|
+
.col { flex: 1; }
|
|
94
|
+
.label { color: #555; font-size: 9px; text-transform: uppercase; }
|
|
95
|
+
.val { font-weight: 600; }
|
|
96
|
+
table { width: 100%; border-collapse: collapse; margin-top: 6px; }
|
|
97
|
+
th, td { border: 1px solid #999; padding: 4px; }
|
|
98
|
+
th { background: #eee; font-size: 9px; }
|
|
99
|
+
.r { text-align: right; }
|
|
100
|
+
.chave { letter-spacing: 1px; font-size: 12px; font-weight: bold; word-break: break-all; }
|
|
101
|
+
.homolog { color: #b00; font-weight: bold; text-align: center; }
|
|
102
|
+
@media print { body { margin: 0; } }
|
|
103
|
+
</style>
|
|
104
|
+
</head>
|
|
105
|
+
<body>
|
|
106
|
+
<h1>${this.#esc(titulo)}</h1>
|
|
107
|
+
${d.ambiente === '2' ? '<p class="homolog">NF-e EMITIDA EM AMBIENTE DE HOMOLOGAÇÃO — SEM VALOR FISCAL</p>' : ''}
|
|
108
|
+
<div class="box row">
|
|
109
|
+
<div class="col">
|
|
110
|
+
<div class="label">Emitente</div>
|
|
111
|
+
<div class="val">${this.#esc(d.emitente.razaoSocial)}</div>
|
|
112
|
+
<div>CNPJ ${this.#esc(d.emitente.cnpj)} — IE ${this.#esc(d.emitente.ie)}</div>
|
|
113
|
+
<div>${this.#esc(d.emitente.municipio)} / ${this.#esc(d.emitente.uf)}</div>
|
|
114
|
+
</div>
|
|
115
|
+
<div class="col">
|
|
116
|
+
<div class="label">Documento</div>
|
|
117
|
+
<div>NF-e nº <strong>${this.#esc(d.numero)}</strong> — Série ${this.#esc(d.serie)}</div>
|
|
118
|
+
<div>Emissão: ${this.#esc(d.dhEmi)}</div>
|
|
119
|
+
<div>Nat. Op.: ${this.#esc(d.naturezaOperacao)}</div>
|
|
120
|
+
<div>Protocolo: ${this.#esc(d.protocolo || '—')}</div>
|
|
121
|
+
</div>
|
|
122
|
+
</div>
|
|
123
|
+
<div class="box">
|
|
124
|
+
<div class="label">Destinatário</div>
|
|
125
|
+
<div class="val">${this.#esc(d.destinatario.razaoSocial)}</div>
|
|
126
|
+
<div>CPF/CNPJ ${this.#esc(d.destinatario.doc)}</div>
|
|
127
|
+
</div>
|
|
128
|
+
<div class="box">
|
|
129
|
+
<div class="label">Chave de acesso</div>
|
|
130
|
+
<div class="chave">${this.#esc(chaveFmt)}</div>
|
|
131
|
+
</div>
|
|
132
|
+
<table>
|
|
133
|
+
<thead>
|
|
134
|
+
<tr>
|
|
135
|
+
<th>#</th><th>Cód.</th><th>Descrição</th><th>NCM</th><th>CFOP</th>
|
|
136
|
+
<th>Un</th><th>Qtd</th><th>V. Unit</th><th>V. Total</th>
|
|
137
|
+
</tr>
|
|
138
|
+
</thead>
|
|
139
|
+
<tbody>${linhas}</tbody>
|
|
140
|
+
</table>
|
|
141
|
+
<div class="box row" style="margin-top:8px">
|
|
142
|
+
<div class="col"><div class="label">Produtos</div><div class="val">R$ ${this.#esc(d.totais.vProd)}</div></div>
|
|
143
|
+
<div class="col"><div class="label">ICMS</div><div class="val">R$ ${this.#esc(d.totais.vICMS)}</div></div>
|
|
144
|
+
<div class="col"><div class="label">Frete</div><div class="val">R$ ${this.#esc(d.totais.vFrete)}</div></div>
|
|
145
|
+
<div class="col"><div class="label">Total NF</div><div class="val">R$ ${this.#esc(d.totais.vNF)}</div></div>
|
|
146
|
+
</div>
|
|
147
|
+
${d.informacoesComplementares ? `<div class="box"><div class="label">Informações complementares</div>${this.#esc(d.informacoesComplementares)}</div>` : ''}
|
|
148
|
+
<p style="text-align:center;color:#666;margin-top:12px">Documento gerado por GHINFE — representação auxiliar</p>
|
|
149
|
+
</body>
|
|
150
|
+
</html>`;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
#renderDanfce(d, qrDataUrl, titulo) {
|
|
154
|
+
const chaveFmt = formatarChaveAcesso(d.chaveAcesso);
|
|
155
|
+
const linhas = d.itens.map((i) => `
|
|
156
|
+
<tr>
|
|
157
|
+
<td>${this.#esc(i.descricao)}</td>
|
|
158
|
+
<td class="r">${this.#esc(i.quantidade)}</td>
|
|
159
|
+
<td class="r">${this.#esc(i.valorUnitario)}</td>
|
|
160
|
+
<td class="r">${this.#esc(i.valorTotal)}</td>
|
|
161
|
+
</tr>`).join('');
|
|
162
|
+
|
|
163
|
+
return `<!DOCTYPE html>
|
|
164
|
+
<html lang="pt-BR">
|
|
165
|
+
<head>
|
|
166
|
+
<meta charset="UTF-8"/>
|
|
167
|
+
<title>${this.#esc(titulo)}</title>
|
|
168
|
+
<style>
|
|
169
|
+
body { font-family: 'Courier New', monospace; font-size: 12px; width: 302px; margin: 8px auto; color: #000; }
|
|
170
|
+
h1 { font-size: 13px; text-align: center; margin: 0 0 6px; }
|
|
171
|
+
.c { text-align: center; }
|
|
172
|
+
.r { text-align: right; }
|
|
173
|
+
hr { border: none; border-top: 1px dashed #000; margin: 8px 0; }
|
|
174
|
+
table { width: 100%; border-collapse: collapse; }
|
|
175
|
+
td { padding: 2px 0; vertical-align: top; }
|
|
176
|
+
img.qr { display: block; margin: 8px auto; }
|
|
177
|
+
.homolog { color: #b00; font-weight: bold; text-align: center; font-size: 11px; }
|
|
178
|
+
@media print { body { margin: 0; width: 80mm; } }
|
|
179
|
+
</style>
|
|
180
|
+
</head>
|
|
181
|
+
<body>
|
|
182
|
+
<h1>${this.#esc(d.emitente.razaoSocial)}</h1>
|
|
183
|
+
<div class="c">CNPJ ${this.#esc(d.emitente.cnpj)} IE ${this.#esc(d.emitente.ie)}</div>
|
|
184
|
+
<div class="c">${this.#esc(d.emitente.municipio)}/${this.#esc(d.emitente.uf)}</div>
|
|
185
|
+
<hr/>
|
|
186
|
+
<div class="c"><strong>${this.#esc(titulo)}</strong></div>
|
|
187
|
+
${d.ambiente === '2' ? '<p class="homolog">EMITIDA EM HOMOLOGAÇÃO — SEM VALOR FISCAL</p>' : ''}
|
|
188
|
+
<div class="c">NFC-e nº ${this.#esc(d.numero)} Série ${this.#esc(d.serie)}</div>
|
|
189
|
+
<div class="c">${this.#esc(d.dhEmi)}</div>
|
|
190
|
+
<hr/>
|
|
191
|
+
<table>
|
|
192
|
+
<thead><tr><td>Item</td><td class="r">Qtd</td><td class="r">Vl Un</td><td class="r">Total</td></tr></thead>
|
|
193
|
+
<tbody>${linhas}</tbody>
|
|
194
|
+
</table>
|
|
195
|
+
<hr/>
|
|
196
|
+
<div class="c"><strong>TOTAL R$ ${this.#esc(d.totais.vNF)}</strong></div>
|
|
197
|
+
<div class="c">Protocolo: ${this.#esc(d.protocolo || '—')}</div>
|
|
198
|
+
<hr/>
|
|
199
|
+
<div class="c" style="font-size:10px;word-break:break-all">${this.#esc(chaveFmt)}</div>
|
|
200
|
+
${qrDataUrl ? `<img class="qr" src="${qrDataUrl}" alt="QR Code NFC-e" width="160" height="160"/>` : '<p class="c">(QR Code indisponível — assine e adicione infNFeSupl)</p>'}
|
|
201
|
+
<p class="c" style="font-size:10px">Consulte pela chave de acesso no portal da SEFAZ</p>
|
|
202
|
+
</body>
|
|
203
|
+
</html>`;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extrai campos úteis de XML NF-e/NFC-e para montagem do DANFE/DANFCE.
|
|
3
|
+
* @param {string} xml
|
|
4
|
+
*/
|
|
5
|
+
export function extrairDadosDanfe(xml) {
|
|
6
|
+
const get = (tag) => xml.match(new RegExp(`<${tag}[^>]*>([^<]*)</${tag}>`))?.[1]?.trim() ?? '';
|
|
7
|
+
const getAll = (tag) => [...xml.matchAll(new RegExp(`<${tag}[^>]*>([^<]*)</${tag}>`, 'g'))].map((m) => m[1]);
|
|
8
|
+
|
|
9
|
+
const chave = get('chNFe') || (xml.match(/Id="NFe(\d{44})"/)?.[1] ?? '');
|
|
10
|
+
const protocolo = get('nProt');
|
|
11
|
+
const qrCode = xml.match(/<!\[CDATA\[(https?:\/\/[^\]]+)\]\]>/)?.[1]
|
|
12
|
+
|| xml.match(/<qrCode[^>]*>([^<]+)<\/qrCode>/)?.[1]
|
|
13
|
+
|| '';
|
|
14
|
+
|
|
15
|
+
const itens = [];
|
|
16
|
+
const detBlocks = xml.match(/<det[\s\S]*?<\/det>/g) ?? [];
|
|
17
|
+
for (const block of detBlocks) {
|
|
18
|
+
const g = (t) => block.match(new RegExp(`<${t}[^>]*>([^<]*)</${t}>`))?.[1] ?? '';
|
|
19
|
+
itens.push({
|
|
20
|
+
codigo: g('cProd'),
|
|
21
|
+
descricao: g('xProd'),
|
|
22
|
+
ncm: g('NCM'),
|
|
23
|
+
cfop: g('CFOP'),
|
|
24
|
+
unidade: g('uCom'),
|
|
25
|
+
quantidade: g('qCom'),
|
|
26
|
+
valorUnitario: g('vUnCom'),
|
|
27
|
+
valorTotal: g('vProd'),
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
chaveAcesso: chave,
|
|
33
|
+
protocolo,
|
|
34
|
+
modelo: get('mod') || (chave.substring(20, 22) || '55'),
|
|
35
|
+
serie: get('serie'),
|
|
36
|
+
numero: get('nNF'),
|
|
37
|
+
dhEmi: get('dhEmi'),
|
|
38
|
+
naturezaOperacao: get('natOp'),
|
|
39
|
+
ambiente: get('tpAmb'),
|
|
40
|
+
emitente: {
|
|
41
|
+
cnpj: get('CNPJ'),
|
|
42
|
+
razaoSocial: get('xNome'),
|
|
43
|
+
ie: get('IE'),
|
|
44
|
+
municipio: (xml.match(/<enderEmit>[\s\S]*?<xMun>([^<]+)/)?.[1]) ?? '',
|
|
45
|
+
uf: (xml.match(/<enderEmit>[\s\S]*?<UF>([^<]+)/)?.[1]) ?? '',
|
|
46
|
+
},
|
|
47
|
+
destinatario: {
|
|
48
|
+
doc: xml.match(/<dest>[\s\S]*?<(?:CNPJ|CPF)>([^<]+)/)?.[1] ?? '',
|
|
49
|
+
razaoSocial: xml.match(/<dest>[\s\S]*?<xNome>([^<]+)/)?.[1] ?? '',
|
|
50
|
+
},
|
|
51
|
+
totais: {
|
|
52
|
+
vProd: get('vProd'),
|
|
53
|
+
vNF: get('vNF'),
|
|
54
|
+
vICMS: get('vICMS'),
|
|
55
|
+
vFrete: get('vFrete'),
|
|
56
|
+
},
|
|
57
|
+
itens,
|
|
58
|
+
qrCode,
|
|
59
|
+
informacoesComplementares: get('infCpl'),
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Formata chave de acesso com espaços a cada 4 dígitos.
|
|
65
|
+
* @param {string} chave
|
|
66
|
+
*/
|
|
67
|
+
export function formatarChaveAcesso(chave) {
|
|
68
|
+
return (chave || '').replace(/\D/g, '').replace(/(\d{4})(?=\d)/g, '$1 ');
|
|
69
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export class GhinfeError extends Error {
|
|
2
|
+
/**
|
|
3
|
+
* @param {string} message
|
|
4
|
+
* @param {string} [code]
|
|
5
|
+
* @param {unknown} [details]
|
|
6
|
+
*/
|
|
7
|
+
constructor(message, code = 'GHINFE_ERROR', details = null) {
|
|
8
|
+
super(message);
|
|
9
|
+
this.name = 'GhinfeError';
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.details = details;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { GhinfeError } from './GhinfeError.js';
|
|
2
|
+
|
|
3
|
+
export class SefazError extends GhinfeError {
|
|
4
|
+
/**
|
|
5
|
+
* @param {string} cStat
|
|
6
|
+
* @param {string} xMotivo
|
|
7
|
+
* @param {Record<string, unknown>} [raw]
|
|
8
|
+
*/
|
|
9
|
+
constructor(cStat, xMotivo, raw = {}) {
|
|
10
|
+
super(`SEFAZ [${cStat}]: ${xMotivo}`, `SEFAZ_${cStat}`, { cStat, xMotivo, ...raw });
|
|
11
|
+
this.name = 'SefazError';
|
|
12
|
+
this.cStat = cStat;
|
|
13
|
+
this.xMotivo = xMotivo;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** @returns {boolean} */
|
|
17
|
+
isAutorizado() {
|
|
18
|
+
return this.cStat === '100';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** @returns {boolean} */
|
|
22
|
+
isRejeicao() {
|
|
23
|
+
return this.cStat.length === 3 && this.cStat.startsWith('2');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { GhinfeError } from './GhinfeError.js';
|
|
2
|
+
|
|
3
|
+
export class XsdValidationError extends GhinfeError {
|
|
4
|
+
/**
|
|
5
|
+
* @param {string[]} erros
|
|
6
|
+
* @param {string} [schema]
|
|
7
|
+
*/
|
|
8
|
+
constructor(erros, schema = 'unknown') {
|
|
9
|
+
const resumo = erros[0] ?? 'XML não conforme ao schema XSD';
|
|
10
|
+
super(`Validação XSD falhou: ${resumo}`, 'XSD_VALIDATION_FAILED', { erros, schema });
|
|
11
|
+
this.name = 'XsdValidationError';
|
|
12
|
+
this.erros = erros;
|
|
13
|
+
this.schema = schema;
|
|
14
|
+
}
|
|
15
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export { NFeService } from './nfe/services/NFeService.js';
|
|
2
|
+
export { NFeXmlService } from './nfe/services/NFeXmlService.js';
|
|
3
|
+
export { NFeSignService } from './nfe/services/NFeSignService.js';
|
|
4
|
+
export { NFeSoapService } from './nfe/services/NFeSoapService.js';
|
|
5
|
+
export { NFeEventoService } from './nfe/services/NFeEventoService.js';
|
|
6
|
+
export { NFeXsdService } from './nfe/services/NFeXsdService.js';
|
|
7
|
+
export { NFeXmlBuilder } from './nfe/builders/NFeXmlBuilder.js';
|
|
8
|
+
export { NFeEventoXmlBuilder } from './nfe/builders/NFeEventoXmlBuilder.js';
|
|
9
|
+
export { NFeQueue } from './queue/NFeQueue.js';
|
|
10
|
+
|
|
11
|
+
export { NFCeService } from './nfce/services/NFCeService.js';
|
|
12
|
+
export { NFCeXmlService } from './nfce/services/NFCeXmlService.js';
|
|
13
|
+
export { NFCeXmlBuilder } from './nfce/builders/NFCeXmlBuilder.js';
|
|
14
|
+
export { NFCeQueue } from './queue/NFCeQueue.js';
|
|
15
|
+
export { gerarUrlQrCodeV3, gerarHashQrCodeV3, inserirInfNFeSupl } from './nfce/utils/qrcode.utils.js';
|
|
16
|
+
|
|
17
|
+
export { NFSeService } from './nfse/services/NFSeService.js';
|
|
18
|
+
export { NFSeXmlService } from './nfse/services/NFSeXmlService.js';
|
|
19
|
+
export { NFSeXmlBuilder } from './nfse/builders/NFSeXmlBuilder.js';
|
|
20
|
+
export { NFSeEventoXmlBuilder } from './nfse/builders/NFSeEventoXmlBuilder.js';
|
|
21
|
+
export { NFSeSoapService, NFSeSoapError } from './nfse/services/NFSeSoapService.js';
|
|
22
|
+
export { NFSeMunicipioResolver } from './nfse/NFSeMunicipioResolver.js';
|
|
23
|
+
export { NFSeQueue } from './queue/NFSeQueue.js';
|
|
24
|
+
export { NFSE_LAYOUTS, NFSE_MUNICIPIOS } from './nfse/constants/municipios.js';
|
|
25
|
+
export { obterAdapter, NFSeLayoutAdapters } from './nfse/layouts/adapters.js';
|
|
26
|
+
|
|
27
|
+
export { DanfeService } from './danfe/DanfeService.js';
|
|
28
|
+
export { extrairDadosDanfe, formatarChaveAcesso } from './danfe/xml-extract.js';
|
|
29
|
+
|
|
30
|
+
export { SefazEndpointResolver, SEFAZ_SERVICO } from './sefaz/SefazEndpointResolver.js';
|
|
31
|
+
export { UF_AUTORIZADOR, AUTORIZADOR_ENDPOINTS } from './sefaz/endpoints.registry.js';
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
calcularIBSCBS,
|
|
35
|
+
buildFragmentoIBSCBS,
|
|
36
|
+
somarTotaisReforma,
|
|
37
|
+
reformaHabilitada,
|
|
38
|
+
appendIBSCBS,
|
|
39
|
+
appendTotaisIBSCBS,
|
|
40
|
+
CST_IBSCBS,
|
|
41
|
+
REFORMA_LAYOUT,
|
|
42
|
+
} from './reforma/ibscbs.js';
|
|
43
|
+
|
|
44
|
+
export { XsdValidator } from './validation/XsdValidator.js';
|
|
45
|
+
export { XSD_SCHEMAS } from './validation/schemas.js';
|
|
46
|
+
|
|
47
|
+
export { GhinfeError } from './errors/GhinfeError.js';
|
|
48
|
+
export { SefazError } from './errors/SefazError.js';
|
|
49
|
+
export { XsdValidationError } from './errors/XsdValidationError.js';
|
|
50
|
+
|
|
51
|
+
export { SEFAZ_STATUS, SEFAZ_URLS, AMBIENTE, NFE_VERSAO, UF_IBGE } from './constants/sefaz.js';
|
|
52
|
+
export { NFE_EVENTOS } from './nfe/types/NFeEvento.js';
|
|
53
|
+
export { loadPfxCertificate } from './utils/certificate.utils.js';
|
|
54
|
+
export {
|
|
55
|
+
gerarChaveAcesso,
|
|
56
|
+
calcularDigitoVerificador,
|
|
57
|
+
validarChaveAcesso,
|
|
58
|
+
parseChaveAcesso,
|
|
59
|
+
gerarCodigoNumerico,
|
|
60
|
+
} from './utils/chave.utils.js';
|
|
@@ -0,0 +1,178 @@
|
|
|
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 { codigoUf, formatDecimal, appendEndereco, appendImpostosItem } from '../../shared/fiscal.helpers.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Monta XML da NFC-e (modelo 65, layout 4.00).
|
|
9
|
+
*/
|
|
10
|
+
export class NFCeXmlBuilder {
|
|
11
|
+
/**
|
|
12
|
+
* @param {import('../types/NFCeDocument.js').NFCeDocumento} documento
|
|
13
|
+
* @returns {string}
|
|
14
|
+
*/
|
|
15
|
+
build(documento) {
|
|
16
|
+
this.#validar(documento);
|
|
17
|
+
|
|
18
|
+
const dhEmi = formatSefazDateTime();
|
|
19
|
+
const idInfNFe = `NFe${documento.chaveAcesso}`;
|
|
20
|
+
const ambiente = documento.ambiente ?? 2;
|
|
21
|
+
|
|
22
|
+
const totalProdutos = documento.itens.reduce((s, i) => s + i.valorTotal, 0);
|
|
23
|
+
const valorIcms = documento.itens.reduce((s, i) => s + (i.valorIcms ?? 0), 0);
|
|
24
|
+
const valorDesc = documento.itens.reduce((s, i) => s + (i.valorDesconto ?? 0), 0);
|
|
25
|
+
const valorNf = totalProdutos - valorDesc;
|
|
26
|
+
|
|
27
|
+
const root = create({ version: '1.0', encoding: 'UTF-8' })
|
|
28
|
+
.ele('NFe', { xmlns: 'http://www.portalfiscal.inf.br/nfe' })
|
|
29
|
+
.ele('infNFe', { versao: NFE_VERSAO, Id: idInfNFe });
|
|
30
|
+
|
|
31
|
+
const inf = root;
|
|
32
|
+
|
|
33
|
+
// ide — NFC-e modelo 65
|
|
34
|
+
inf
|
|
35
|
+
.ele('ide')
|
|
36
|
+
.ele('cUF').txt(codigoUf(documento.ide.uf)).up()
|
|
37
|
+
.ele('cNF').txt(documento.chaveAcesso.substring(35, 43)).up()
|
|
38
|
+
.ele('natOp').txt(documento.ide.naturezaOperacao ?? 'VENDA').up()
|
|
39
|
+
.ele('mod').txt('65').up()
|
|
40
|
+
.ele('serie').txt(String(documento.ide.serie ?? 1)).up()
|
|
41
|
+
.ele('nNF').txt(String(documento.ide.numero)).up()
|
|
42
|
+
.ele('dhEmi').txt(dhEmi).up()
|
|
43
|
+
.ele('tpNF').txt('1').up()
|
|
44
|
+
.ele('idDest').txt('1').up()
|
|
45
|
+
.ele('cMunFG').txt(documento.ide.codigoMunicipio).up()
|
|
46
|
+
.ele('tpImp').txt('4').up()
|
|
47
|
+
.ele('tpEmis').txt('1').up()
|
|
48
|
+
.ele('cDV').txt(documento.chaveAcesso.slice(-1)).up()
|
|
49
|
+
.ele('tpAmb').txt(String(ambiente)).up()
|
|
50
|
+
.ele('finNFe').txt('1').up()
|
|
51
|
+
.ele('indFinal').txt('1').up()
|
|
52
|
+
.ele('indPres').txt(String(documento.ide.presenca ?? 1)).up()
|
|
53
|
+
.ele('procEmi').txt('0').up()
|
|
54
|
+
.ele('verProc').txt('ghinfe-0.3.0').up()
|
|
55
|
+
.up();
|
|
56
|
+
|
|
57
|
+
// emit
|
|
58
|
+
const emit = inf.ele('emit');
|
|
59
|
+
emit.ele('CNPJ').txt(documento.emitente.cnpj.replace(/\D/g, '')).up();
|
|
60
|
+
emit.ele('xNome').txt(documento.emitente.razaoSocial).up();
|
|
61
|
+
if (documento.emitente.nomeFantasia) {
|
|
62
|
+
emit.ele('xFant').txt(documento.emitente.nomeFantasia).up();
|
|
63
|
+
}
|
|
64
|
+
appendEndereco(emit, 'enderEmit', documento.emitente.endereco);
|
|
65
|
+
emit.ele('IE').txt(documento.emitente.ie).up();
|
|
66
|
+
emit.ele('CRT').txt(documento.emitente.crt).up();
|
|
67
|
+
emit.up();
|
|
68
|
+
|
|
69
|
+
// dest — opcional na NFC-e (se informado CPF/CNPJ, exige indIEDest ou enderDest)
|
|
70
|
+
if (documento.destinatario?.cpf || documento.destinatario?.cnpj) {
|
|
71
|
+
const dest = inf.ele('dest');
|
|
72
|
+
const docDest = (documento.destinatario.cnpj ?? documento.destinatario.cpf ?? '').replace(/\D/g, '');
|
|
73
|
+
if (docDest.length === 14) dest.ele('CNPJ').txt(docDest).up();
|
|
74
|
+
else if (docDest.length === 11) dest.ele('CPF').txt(docDest).up();
|
|
75
|
+
if (documento.destinatario.razaoSocial) {
|
|
76
|
+
dest.ele('xNome').txt(documento.destinatario.razaoSocial).up();
|
|
77
|
+
}
|
|
78
|
+
if (documento.destinatario.endereco) {
|
|
79
|
+
appendEndereco(dest, 'enderDest', documento.destinatario.endereco);
|
|
80
|
+
} else {
|
|
81
|
+
dest.ele('indIEDest').txt('9').up();
|
|
82
|
+
}
|
|
83
|
+
dest.up();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// det — itens
|
|
87
|
+
documento.itens.forEach((item, index) => {
|
|
88
|
+
const det = inf.ele('det', { nItem: String(index + 1) });
|
|
89
|
+
const prod = det.ele('prod');
|
|
90
|
+
prod.ele('cProd').txt(item.codigo).up();
|
|
91
|
+
prod.ele('cEAN').txt(item.ean ?? 'SEM GTIN').up();
|
|
92
|
+
prod.ele('xProd').txt(item.descricao).up();
|
|
93
|
+
prod.ele('NCM').txt(item.ncm).up();
|
|
94
|
+
prod.ele('CFOP').txt(item.cfop).up();
|
|
95
|
+
prod.ele('uCom').txt(item.unidade).up();
|
|
96
|
+
prod.ele('qCom').txt(formatDecimal(item.quantidade, 4)).up();
|
|
97
|
+
prod.ele('vUnCom').txt(formatDecimal(item.valorUnitario, 10)).up();
|
|
98
|
+
prod.ele('vProd').txt(formatDecimal(item.valorTotal, 2)).up();
|
|
99
|
+
prod.ele('cEANTrib').txt(item.ean ?? 'SEM GTIN').up();
|
|
100
|
+
prod.ele('uTrib').txt(item.unidade).up();
|
|
101
|
+
prod.ele('qTrib').txt(formatDecimal(item.quantidade, 4)).up();
|
|
102
|
+
prod.ele('vUnTrib').txt(formatDecimal(item.valorUnitario, 10)).up();
|
|
103
|
+
prod.ele('indTot').txt('1').up();
|
|
104
|
+
prod.up();
|
|
105
|
+
|
|
106
|
+
appendImpostosItem(det.ele('imposto'), item);
|
|
107
|
+
det.up();
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// total
|
|
111
|
+
const total = inf.ele('total').ele('ICMSTot');
|
|
112
|
+
total
|
|
113
|
+
.ele('vBC').txt(formatDecimal(totalProdutos, 2)).up()
|
|
114
|
+
.ele('vICMS').txt(formatDecimal(valorIcms, 2)).up()
|
|
115
|
+
.ele('vICMSDeson').txt('0.00').up()
|
|
116
|
+
.ele('vFCP').txt('0.00').up()
|
|
117
|
+
.ele('vBCST').txt('0.00').up()
|
|
118
|
+
.ele('vST').txt('0.00').up()
|
|
119
|
+
.ele('vFCPST').txt('0.00').up()
|
|
120
|
+
.ele('vFCPSTRet').txt('0.00').up()
|
|
121
|
+
.ele('vProd').txt(formatDecimal(totalProdutos, 2)).up()
|
|
122
|
+
.ele('vFrete').txt('0.00').up()
|
|
123
|
+
.ele('vSeg').txt('0.00').up()
|
|
124
|
+
.ele('vDesc').txt(formatDecimal(valorDesc, 2)).up()
|
|
125
|
+
.ele('vII').txt('0.00').up()
|
|
126
|
+
.ele('vIPI').txt('0.00').up()
|
|
127
|
+
.ele('vIPIDevol').txt('0.00').up()
|
|
128
|
+
.ele('vPIS').txt('0.00').up()
|
|
129
|
+
.ele('vCOFINS').txt('0.00').up()
|
|
130
|
+
.ele('vOutro').txt('0.00').up()
|
|
131
|
+
.ele('vNF').txt(formatDecimal(valorNf, 2)).up();
|
|
132
|
+
total.up().up();
|
|
133
|
+
|
|
134
|
+
// transp — NFC-e sem frete
|
|
135
|
+
inf.ele('transp').ele('modFrete').txt('9').up().up();
|
|
136
|
+
|
|
137
|
+
// pag
|
|
138
|
+
const pag = inf.ele('pag');
|
|
139
|
+
const pagamentos = documento.pagamentos ?? [{ forma: '01', valor: valorNf }];
|
|
140
|
+
pagamentos.forEach((p) => {
|
|
141
|
+
const detPag = pag.ele('detPag');
|
|
142
|
+
detPag.ele('tPag').txt(p.forma).up();
|
|
143
|
+
detPag.ele('vPag').txt(formatDecimal(p.valor, 2)).up();
|
|
144
|
+
detPag.up();
|
|
145
|
+
});
|
|
146
|
+
pag.up();
|
|
147
|
+
|
|
148
|
+
// infAdic
|
|
149
|
+
if (documento.informacoesComplementares || documento.informacoesFisco) {
|
|
150
|
+
const infAdic = inf.ele('infAdic');
|
|
151
|
+
if (documento.informacoesFisco) {
|
|
152
|
+
infAdic.ele('infAdFisco').txt(documento.informacoesFisco).up();
|
|
153
|
+
}
|
|
154
|
+
if (documento.informacoesComplementares) {
|
|
155
|
+
infAdic.ele('infCpl').txt(documento.informacoesComplementares).up();
|
|
156
|
+
}
|
|
157
|
+
infAdic.up();
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return root.end({ prettyPrint: false, headless: false });
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** @param {import('../types/NFCeDocument.js').NFCeDocumento} documento */
|
|
164
|
+
#validar(documento) {
|
|
165
|
+
if (!documento.chaveAcesso || documento.chaveAcesso.length !== 44) {
|
|
166
|
+
throw new GhinfeError('chaveAcesso deve conter 44 dígitos', 'INVALID_CHAVE');
|
|
167
|
+
}
|
|
168
|
+
if (!documento.emitente?.cnpj) {
|
|
169
|
+
throw new GhinfeError('emitente.cnpj é obrigatório', 'INVALID_EMITENTE');
|
|
170
|
+
}
|
|
171
|
+
if (!documento.itens?.length) {
|
|
172
|
+
throw new GhinfeError('A NFC-e deve conter ao menos um item', 'INVALID_ITENS');
|
|
173
|
+
}
|
|
174
|
+
if (!documento.csc?.idCSC || !documento.csc?.codigo) {
|
|
175
|
+
throw new GhinfeError('csc.idCSC e csc.codigo são obrigatórios para QR Code', 'INVALID_CSC');
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/** URLs de consulta QR Code NFC-e por UF e ambiente. */
|
|
2
|
+
export const NFCE_QRCODE_URLS = {
|
|
3
|
+
SP: {
|
|
4
|
+
1: 'https://www.nfce.fazenda.sp.gov.br/NFCeConsultaPublica/Paginas/ConsultaQRCode.aspx',
|
|
5
|
+
2: 'https://www.homologacao.nfce.fazenda.sp.gov.br/NFCeConsultaPublica/Paginas/ConsultaQRCode.aspx',
|
|
6
|
+
},
|
|
7
|
+
PR: {
|
|
8
|
+
1: 'http://www.fazenda.pr.gov.br/nfce/qrcode',
|
|
9
|
+
2: 'http://www.fazenda.pr.gov.br/nfce/qrcode',
|
|
10
|
+
},
|
|
11
|
+
RS: {
|
|
12
|
+
1: 'https://www.sefaz.rs.gov.br/NFCE/NFCE-COM-QRCode',
|
|
13
|
+
2: 'https://www.sefaz.rs.gov.br/NFCE/NFCE-COM-QRCode',
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/** URLs de consulta por chave (portal SEFAZ). */
|
|
18
|
+
export const NFCE_URL_CHAVE = {
|
|
19
|
+
SP: {
|
|
20
|
+
1: 'https://www.nfce.fazenda.sp.gov.br/consulta',
|
|
21
|
+
2: 'https://www.homologacao.nfce.fazenda.sp.gov.br/consulta',
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Versão do QR Code NFC-e (2 = atual). */
|
|
26
|
+
export const NFCE_QRCODE_VERSAO = '2';
|