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,117 @@
1
+ import { UF_IBGE } from '../constants/sefaz.js';
2
+ import { GhinfeError } from '../errors/GhinfeError.js';
3
+
4
+ /**
5
+ * Gera código numérico aleatório de 8 dígitos (cNF).
6
+ * @returns {string}
7
+ */
8
+ export function gerarCodigoNumerico() {
9
+ const valor = Math.floor(Math.random() * 99_999_999) + 1;
10
+ return String(valor).padStart(8, '0');
11
+ }
12
+
13
+ /**
14
+ * Calcula dígito verificador da chave NF-e (módulo 11).
15
+ * @param {string} chave43 Primeiros 43 dígitos da chave (sem DV)
16
+ * @returns {string} Dígito verificador (0-9)
17
+ */
18
+ export function calcularDigitoVerificador(chave43) {
19
+ if (!/^\d{43}$/.test(chave43)) {
20
+ throw new GhinfeError('Chave deve conter exatamente 43 dígitos para cálculo do DV', 'CHAVE_INVALID_LENGTH');
21
+ }
22
+
23
+ const pesos = [2, 3, 4, 5, 6, 7, 8, 9];
24
+ let soma = 0;
25
+ let pesoIndex = 0;
26
+
27
+ for (let i = chave43.length - 1; i >= 0; i--) {
28
+ soma += parseInt(chave43[i], 10) * pesos[pesoIndex];
29
+ pesoIndex = (pesoIndex + 1) % pesos.length;
30
+ }
31
+
32
+ const resto = soma % 11;
33
+ return String(resto < 2 ? 0 : 11 - resto);
34
+ }
35
+
36
+ /**
37
+ * Valida se a chave de acesso possui DV correto.
38
+ * @param {string} chaveAcesso 44 dígitos
39
+ * @returns {boolean}
40
+ */
41
+ export function validarChaveAcesso(chaveAcesso) {
42
+ const chave = chaveAcesso.replace(/\D/g, '');
43
+ if (chave.length !== 44) return false;
44
+ const dvCalculado = calcularDigitoVerificador(chave.substring(0, 43));
45
+ return dvCalculado === chave[43];
46
+ }
47
+
48
+ /**
49
+ * Gera chave de acesso NF-e/NFC-e (44 dígitos) conforme layout SEFAZ.
50
+ *
51
+ * Composição: cUF(2) + AAMM(4) + CNPJ(14) + mod(2) + serie(3) + nNF(9) + tpEmis(1) + cNF(8) + cDV(1)
52
+ *
53
+ * @param {{
54
+ * uf: string,
55
+ * cnpj: string,
56
+ * modelo?: string|number,
57
+ * serie: number,
58
+ * numero: number,
59
+ * tipoEmissao?: number,
60
+ * codigoNumerico?: string,
61
+ * dataEmissao?: Date,
62
+ * }} params
63
+ * @returns {string}
64
+ */
65
+ export function gerarChaveAcesso(params) {
66
+ const uf = params.uf?.toUpperCase();
67
+ const cUF = UF_IBGE[uf];
68
+
69
+ if (!cUF) {
70
+ throw new GhinfeError(`UF inválida: ${params.uf}`, 'UF_INVALIDA');
71
+ }
72
+
73
+ const data = params.dataEmissao ?? new Date();
74
+ const ano = String(data.getFullYear()).slice(-2);
75
+ const mes = String(data.getMonth() + 1).padStart(2, '0');
76
+ const aamm = `${ano}${mes}`;
77
+
78
+ const cnpj = params.cnpj.replace(/\D/g, '');
79
+ if (cnpj.length !== 14) {
80
+ throw new GhinfeError('CNPJ deve conter 14 dígitos', 'CNPJ_INVALIDO');
81
+ }
82
+
83
+ const modelo = String(params.modelo ?? '55').padStart(2, '0');
84
+ const serie = String(params.serie).padStart(3, '0');
85
+ const numero = String(params.numero).padStart(9, '0');
86
+ const tpEmis = String(params.tipoEmissao ?? 1);
87
+ const cNF = (params.codigoNumerico ?? gerarCodigoNumerico()).padStart(8, '0');
88
+
89
+ const chave43 = `${cUF}${aamm}${cnpj}${modelo}${serie}${numero}${tpEmis}${cNF}`;
90
+ const cDV = calcularDigitoVerificador(chave43);
91
+
92
+ return chave43 + cDV;
93
+ }
94
+
95
+ /**
96
+ * Extrai campos da chave de acesso para conferência.
97
+ * @param {string} chaveAcesso
98
+ */
99
+ export function parseChaveAcesso(chaveAcesso) {
100
+ const chave = chaveAcesso.replace(/\D/g, '');
101
+ if (chave.length !== 44) {
102
+ throw new GhinfeError('Chave de acesso deve ter 44 dígitos', 'CHAVE_INVALID_LENGTH');
103
+ }
104
+
105
+ return {
106
+ cUF: chave.substring(0, 2),
107
+ aamm: chave.substring(2, 6),
108
+ cnpj: chave.substring(6, 20),
109
+ modelo: chave.substring(20, 22),
110
+ serie: parseInt(chave.substring(22, 25), 10),
111
+ numero: parseInt(chave.substring(25, 34), 10),
112
+ tipoEmissao: parseInt(chave[34], 10),
113
+ codigoNumerico: chave.substring(35, 43),
114
+ digitoVerificador: chave[43],
115
+ valida: validarChaveAcesso(chave),
116
+ };
117
+ }
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Formata data para padrão SEFAZ: AAAA-MM-DDTHH:mm:ss-03:00
3
+ * @param {Date} [date]
4
+ * @returns {string}
5
+ */
6
+ export function formatSefazDateTime(date = new Date()) {
7
+ const pad = (n) => String(n).padStart(2, '0');
8
+ const offset = -date.getTimezoneOffset();
9
+ const sign = offset >= 0 ? '+' : '-';
10
+ const abs = Math.abs(offset);
11
+ const tz = `${sign}${pad(Math.floor(abs / 60))}:${pad(abs % 60)}`;
12
+
13
+ return (
14
+ `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}` +
15
+ `T${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}${tz}`
16
+ );
17
+ }
18
+
19
+ /**
20
+ * @param {Date} [date]
21
+ * @returns {string} AAAA-MM-DD
22
+ */
23
+ export function formatSefazDate(date = new Date()) {
24
+ const pad = (n) => String(n).padStart(2, '0');
25
+ return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())}`;
26
+ }
@@ -0,0 +1,44 @@
1
+ import { DOMParser, XMLSerializer } from '@xmldom/xmldom';
2
+
3
+ /**
4
+ * @param {string} xml
5
+ * @returns {string}
6
+ */
7
+ export function minifyXml(xml) {
8
+ return xml.replace(/>\s+</g, '><').trim();
9
+ }
10
+
11
+ /**
12
+ * @param {string} xml
13
+ * @returns {Document}
14
+ */
15
+ export function parseXml(xml) {
16
+ const parser = new DOMParser();
17
+ const doc = parser.parseFromString(xml, 'text/xml');
18
+ const parseError = doc.getElementsByTagName('parsererror')[0];
19
+ if (parseError) {
20
+ throw new Error(`XML inválido: ${parseError.textContent}`);
21
+ }
22
+ return doc;
23
+ }
24
+
25
+ /**
26
+ * @param {Document|Element} node
27
+ * @returns {string}
28
+ */
29
+ export function serializeXml(node) {
30
+ return new XMLSerializer().serializeToString(node);
31
+ }
32
+
33
+ /**
34
+ * @param {string} value
35
+ * @returns {string}
36
+ */
37
+ export function escapeXml(value) {
38
+ return String(value)
39
+ .replace(/&/g, '&amp;')
40
+ .replace(/</g, '&lt;')
41
+ .replace(/>/g, '&gt;')
42
+ .replace(/"/g, '&quot;')
43
+ .replace(/'/g, '&apos;');
44
+ }
@@ -0,0 +1,114 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import libxmljs from 'libxmljs2';
4
+ import { pathToFileURL } from 'url';
5
+ import { XsdValidationError } from '../errors/XsdValidationError.js';
6
+ import { GhinfeError } from '../errors/GhinfeError.js';
7
+
8
+ const schemaCache = new Map();
9
+
10
+ /**
11
+ * Validador XSD genérico usando libxmljs2 e schemas oficiais SEFAZ.
12
+ */
13
+ export class XsdValidator {
14
+ /**
15
+ * @param {string} xsdPath Caminho absoluto do arquivo .xsd
16
+ */
17
+ static loadSchema(xsdPath) {
18
+ if (schemaCache.has(xsdPath)) {
19
+ return schemaCache.get(xsdPath);
20
+ }
21
+
22
+ if (!fs.existsSync(xsdPath)) {
23
+ throw new GhinfeError(
24
+ `Schema XSD não encontrado: ${xsdPath}. Execute: node scripts/download-schemas.js`,
25
+ 'XSD_SCHEMA_NOT_FOUND'
26
+ );
27
+ }
28
+
29
+ const dir = path.dirname(xsdPath);
30
+ const baseUrl = pathToFileURL(dir + path.sep).href;
31
+ const content = fs.readFileSync(xsdPath, 'utf8');
32
+ const schema = libxmljs.parseXml(content, { baseUrl });
33
+
34
+ schemaCache.set(xsdPath, schema);
35
+ return schema;
36
+ }
37
+
38
+ /**
39
+ * Valida XML contra schema XSD.
40
+ * @param {string} xml
41
+ * @param {string} xsdPath
42
+ * @param {{ injetarAssinaturaStub?: boolean, modelo?: '55'|'65' }} [options]
43
+ * @returns {{ valido: true, avisos: string[] }}
44
+ */
45
+ static validar(xml, xsdPath, options = {}) {
46
+ let xmlParaValidar = xml;
47
+
48
+ if (options.injetarAssinaturaStub) {
49
+ xmlParaValidar = this.#prepararXmlEstrutural(xml, options.modelo ?? '55');
50
+ }
51
+
52
+ const schema = this.loadSchema(xsdPath);
53
+ const doc = libxmljs.parseXml(xmlParaValidar);
54
+
55
+ const valido = doc.validate(schema);
56
+ if (valido) {
57
+ return { valido: true, avisos: [] };
58
+ }
59
+
60
+ const erros = doc.validationErrors.map((e) => e.message).filter(Boolean);
61
+ throw new XsdValidationError(erros, path.basename(xsdPath));
62
+ }
63
+
64
+ /**
65
+ * Valida e retorna boolean (sem lançar exceção).
66
+ * @param {string} xml
67
+ * @param {string} xsdPath
68
+ * @param {object} [options]
69
+ */
70
+ static tentarValidar(xml, xsdPath, options = {}) {
71
+ try {
72
+ return this.validar(xml, xsdPath, options);
73
+ } catch (error) {
74
+ if (error instanceof XsdValidationError) {
75
+ return { valido: false, erros: error.erros };
76
+ }
77
+ throw error;
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Injeta elementos exigidos pelo XSD para validação estrutural pré-assinatura.
83
+ * O schema exige Signature (NF-e) ou infNFeSupl + Signature (NFC-e).
84
+ * @param {string} xml
85
+ * @param {'55'|'65'} modelo
86
+ */
87
+ static #prepararXmlEstrutural(xml, modelo) {
88
+ if (xml.includes('</Signature>')) return xml;
89
+
90
+ const idMatch = xml.match(/Id="(NFe\d{44})"/);
91
+ const id = idMatch?.[1];
92
+ if (!id) return xml;
93
+
94
+ let complemento = '';
95
+
96
+ if (modelo === '65' && !xml.includes('infNFeSupl')) {
97
+ const qrStub = 'https://nfce.sefaz.example/qr?p=STUB';
98
+ complemento += `<infNFeSupl><qrCode><![CDATA[${qrStub}]]></qrCode><urlChave>${qrStub}</urlChave></infNFeSupl>`;
99
+ }
100
+
101
+ complemento += this.#assinaturaStub(id);
102
+ return xml.replace('</NFe>', `${complemento}</NFe>`);
103
+ }
104
+
105
+ /** @param {string} id */
106
+ static #assinaturaStub(id) {
107
+ return `<Signature xmlns="http://www.w3.org/2000/09/xmldsig#"><SignedInfo><CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/><SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"/><Reference URI="#${id}"><Transforms><Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"/><Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"/></Transforms><DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"/><DigestValue>WCF0TVY=</DigestValue></Reference></SignedInfo><SignatureValue>WCF0TVY=</SignatureValue><KeyInfo><X509Data><X509Certificate>WCF0TVY=</X509Certificate></X509Data></KeyInfo></Signature>`;
108
+ }
109
+
110
+ /** Limpa cache de schemas (útil em testes). */
111
+ static limparCache() {
112
+ schemaCache.clear();
113
+ }
114
+ }
@@ -0,0 +1,14 @@
1
+ import path from 'path';
2
+ import { fileURLToPath } from 'url';
3
+
4
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
5
+ const SCHEMAS_ROOT = path.resolve(__dirname, '../../schemas');
6
+
7
+ /** Caminhos dos schemas XSD oficiais (PL_009_V4 — NF-e/NFC-e 4.00). */
8
+ export const XSD_SCHEMAS = {
9
+ NFE: path.join(SCHEMAS_ROOT, 'nfe/PL_009_V4/nfe_v4.00.xsd'),
10
+ ENVI_NFE: path.join(SCHEMAS_ROOT, 'nfe/PL_009_V4/enviNFe_v4.00.xsd'),
11
+ EVENTO: path.join(SCHEMAS_ROOT, 'nfe/PL_009_V4/leiauteNFe_v4.00.xsd'),
12
+ };
13
+
14
+ export const SCHEMAS_ROOT_PATH = SCHEMAS_ROOT;