ghinfe 0.6.0 → 0.6.2

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 (110) hide show
  1. package/.env.example +24 -15
  2. package/LICENSE +21 -21
  3. package/README.md +491 -413
  4. package/docs/developer-guide.html +421 -421
  5. package/docs/openapi.yaml +428 -428
  6. package/examples/dce-homologacao.js +141 -0
  7. package/examples/emitir-nfce.js +51 -51
  8. package/examples/emitir-nfe.js +83 -83
  9. package/examples/emitir-nfse.js +46 -46
  10. package/examples/endpoints-sefaz.js +14 -14
  11. package/examples/eventos-nfe.js +49 -49
  12. package/examples/gerar-chave.js +25 -25
  13. package/examples/gerar-dace.js +59 -0
  14. package/examples/gerar-danfe.js +75 -75
  15. package/examples/gerar-dce.js +58 -0
  16. package/examples/layouts-nfse.js +51 -51
  17. package/examples/mock-sefaz.js +52 -52
  18. package/examples/nfse-eventos.js +57 -57
  19. package/examples/reforma-ibscbs.js +27 -27
  20. package/examples/reforma-nfe-xml.js +52 -52
  21. package/examples/status-sefaz.js +12 -12
  22. package/examples/validar-xsd.js +48 -48
  23. package/package.json +59 -68
  24. package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/consSitDCeTiposBasico_v1.00.xsd +123 -0
  25. package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/consSitDCe_v1.00.xsd +10 -0
  26. package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/consStatServDCeTiposBasico_v1.00.xsd +110 -0
  27. package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/consStatServDCe_v1.00.xsd +10 -0
  28. package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/dceTiposBasico_v1.00.xsd +1388 -0
  29. package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/retConsSitDCe_v1.00.xsd +10 -0
  30. package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/retConsStatServDCe_v1.00.xsd +10 -0
  31. package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/tiposGeralDCe_v1.00.xsd +712 -0
  32. package/schemas/dce/PL_DCe_100a_NT2024.001/consulta/xmldsig-core-schema_v1.01.xsd +98 -0
  33. package/schemas/dce/PL_DCe_100a_NT2024.001/dceTiposBasico_v1.00.xsd +1410 -0
  34. package/schemas/dce/PL_DCe_100a_NT2024.001/dce_v1.00.xsd +10 -0
  35. package/schemas/dce/PL_DCe_100a_NT2024.001/evento/evCancDCe_v1.00.xsd +36 -0
  36. package/schemas/dce/PL_DCe_100a_NT2024.001/evento/eventoDCeTiposBasico_v1.00.xsd +283 -0
  37. package/schemas/dce/PL_DCe_100a_NT2024.001/evento/eventoDCe_v1.00.xsd +9 -0
  38. package/schemas/dce/PL_DCe_100a_NT2024.001/evento/procEventoDCe_v1.00.xsd +10 -0
  39. package/schemas/dce/PL_DCe_100a_NT2024.001/evento/retEventoDCe_v1.00.xsd +9 -0
  40. package/schemas/dce/PL_DCe_100a_NT2024.001/evento/tiposGeralDCe_v1.00.xsd +712 -0
  41. package/schemas/dce/PL_DCe_100a_NT2024.001/evento/xmldsig-core-schema_v1.01.xsd +98 -0
  42. package/schemas/dce/PL_DCe_100a_NT2024.001/procDCe_v1.00.xsd +37 -0
  43. package/schemas/dce/PL_DCe_100a_NT2024.001/retDCe_v1.00.xsd +9 -0
  44. package/schemas/dce/PL_DCe_100a_NT2024.001/tiposGeralDCe_v1.00.xsd +712 -0
  45. package/schemas/dce/PL_DCe_100a_NT2024.001/xmldsig-core-schema_v1.01.xsd +98 -0
  46. package/schemas/nfe/PL_009_V4/enviNFe_v3.10.xsd +8 -8
  47. package/schemas/nfe/PL_009_V4/xmldsig-core-schema_v1.01.xsd +97 -97
  48. package/scripts/download-schemas.js +39 -39
  49. package/scripts/test-xsd.js +34 -34
  50. package/src/api/server.js +316 -316
  51. package/src/constants/sefaz.js +69 -69
  52. package/src/danfe/DanfeService.js +205 -205
  53. package/src/danfe/xml-extract.js +69 -69
  54. package/src/dce/builders/DCeEventoXmlBuilder.js +86 -0
  55. package/src/dce/builders/DCeXmlBuilder.js +345 -0
  56. package/src/dce/constants/dce.js +87 -0
  57. package/src/dce/dace/DaceService.js +356 -0
  58. package/src/dce/dace/dace-extract.js +114 -0
  59. package/src/dce/services/DCeService.js +263 -0
  60. package/src/dce/services/DCeSoapService.js +235 -0
  61. package/src/dce/services/DCeXmlService.js +67 -0
  62. package/src/dce/types/DCeDocument.js +106 -0
  63. package/src/dce/utils/qrcode.utils.js +59 -0
  64. package/src/errors/GhinfeError.js +13 -13
  65. package/src/errors/SefazError.js +25 -25
  66. package/src/errors/XsdValidationError.js +15 -15
  67. package/src/index.js +84 -60
  68. package/src/nfce/builders/NFCeXmlBuilder.js +178 -178
  69. package/src/nfce/constants/qrcode.js +26 -26
  70. package/src/nfce/services/NFCeService.js +188 -188
  71. package/src/nfce/services/NFCeXmlService.js +38 -38
  72. package/src/nfce/types/NFCeDocument.js +42 -42
  73. package/src/nfce/utils/qrcode.utils.js +92 -92
  74. package/src/nfe/builders/NFeEventoXmlBuilder.js +110 -110
  75. package/src/nfe/builders/NFeXmlBuilder.js +215 -215
  76. package/src/nfe/mocks/sefaz-responses.js +195 -195
  77. package/src/nfe/services/NFeEventoService.js +95 -95
  78. package/src/nfe/services/NFeService.js +196 -196
  79. package/src/nfe/services/NFeSignService.js +127 -111
  80. package/src/nfe/services/NFeSoapService.js +437 -437
  81. package/src/nfe/services/NFeXmlService.js +41 -41
  82. package/src/nfe/services/NFeXsdService.js +56 -56
  83. package/src/nfe/types/NFeDocument.js +92 -92
  84. package/src/nfe/types/NFeEvento.js +44 -44
  85. package/src/nfse/NFSeMunicipioResolver.js +42 -42
  86. package/src/nfse/builders/NFSeEventoXmlBuilder.js +92 -92
  87. package/src/nfse/builders/NFSeXmlBuilder.js +16 -16
  88. package/src/nfse/constants/municipios.js +82 -82
  89. package/src/nfse/layouts/adapters.js +202 -202
  90. package/src/nfse/mocks/nfse-responses.js +142 -142
  91. package/src/nfse/services/NFSeService.js +191 -191
  92. package/src/nfse/services/NFSeSoapService.js +232 -232
  93. package/src/nfse/services/NFSeXmlService.js +12 -12
  94. package/src/nfse/types/NFSeDocument.js +54 -54
  95. package/src/queue/NFCeQueue.js +63 -63
  96. package/src/queue/NFSeQueue.js +59 -59
  97. package/src/queue/NFeQueue.js +76 -76
  98. package/src/reforma/ibscbs.js +175 -175
  99. package/src/sefaz/SefazEndpointResolver.js +95 -95
  100. package/src/sefaz/ca/icp-brasil-raiz-v10.pem +38 -0
  101. package/src/sefaz/endpoints.registry.js +205 -205
  102. package/src/sefaz/httpsAgent.js +33 -0
  103. package/src/shared/fiscal.helpers.js +53 -53
  104. package/src/utils/certificate.utils.js +69 -47
  105. package/src/utils/chave.utils.js +205 -117
  106. package/src/utils/code128.utils.js +67 -0
  107. package/src/utils/date.utils.js +26 -26
  108. package/src/utils/xml.utils.js +44 -44
  109. package/src/validation/XsdValidator.js +133 -114
  110. package/src/validation/schemas.js +18 -14
@@ -1,69 +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
- }
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,86 @@
1
+ import { create } from 'xmlbuilder2';
2
+ import { DCE_NAMESPACE, DCE_VERSAO, DCE_EVENTOS } from '../constants/dce.js';
3
+ import { parseChaveAcessoDCe } from '../../utils/chave.utils.js';
4
+ import { formatSefazDateTime } from '../../utils/date.utils.js';
5
+ import { GhinfeError } from '../../errors/GhinfeError.js';
6
+
7
+ const onlyDigits = (v) => String(v ?? '').replace(/\D/g, '');
8
+
9
+ /**
10
+ * Monta XML de eventos da DC-e (eventoDCe, layout 1.00).
11
+ */
12
+ export class DCeEventoXmlBuilder {
13
+ /**
14
+ * Evento 110111 — Cancelamento (prazo: 24h após a autorização).
15
+ * @param {{
16
+ * chaveAcesso: string,
17
+ * protocolo: string,
18
+ * justificativa: string,
19
+ * emitente: { cnpj?: string, cpf?: string, idOutros?: string },
20
+ * ambiente?: number,
21
+ * sequencia?: number,
22
+ * dataEvento?: Date,
23
+ * }} params emitente: usuário emitente da DC-e (grupo emit)
24
+ * @returns {string}
25
+ */
26
+ buildCancelamento(params) {
27
+ const justificativa = params.justificativa?.trim() ?? '';
28
+ if (justificativa.length < 15 || justificativa.length > 255) {
29
+ throw new GhinfeError('Justificativa deve ter entre 15 e 255 caracteres', 'DCE_JUSTIFICATIVA_INVALIDA');
30
+ }
31
+ if (!/^\d{15,16}$/.test(onlyDigits(params.protocolo))) {
32
+ throw new GhinfeError('Protocolo de autorização inválido', 'DCE_PROTOCOLO_INVALIDO');
33
+ }
34
+
35
+ const { codigo, descricao } = DCE_EVENTOS.CANCELAMENTO;
36
+ return this.#build(params, codigo, (det) => {
37
+ const canc = det.ele('evCancDCe');
38
+ canc.ele('descEvento').txt(descricao);
39
+ canc.ele('nProt').txt(onlyDigits(params.protocolo));
40
+ canc.ele('xJust').txt(justificativa);
41
+ });
42
+ }
43
+
44
+ /**
45
+ * @param {{ chaveAcesso: string, emitente: { cnpj?: string, cpf?: string, idOutros?: string }, ambiente?: number, sequencia?: number, dataEvento?: Date }} params
46
+ * @param {string} tpEvento
47
+ * @param {(det: import('xmlbuilder2').XMLBuilder) => void} buildDetalhe
48
+ */
49
+ #build(params, tpEvento, buildDetalhe) {
50
+ const chave = parseChaveAcessoDCe(params.chaveAcesso ?? '');
51
+ if (!chave.valida || chave.modelo !== '99') {
52
+ throw new GhinfeError('chaveAcesso de DC-e inválida', 'DCE_CHAVE_INVALIDA');
53
+ }
54
+
55
+ const emitente = params.emitente ?? {};
56
+ if (!emitente.cnpj && !emitente.cpf && !emitente.idOutros) {
57
+ throw new GhinfeError(
58
+ 'emitente.cnpj, emitente.cpf ou emitente.idOutros (usuário emitente da DC-e) é obrigatório',
59
+ 'DCE_EVENTO_EMITENTE_AUSENTE'
60
+ );
61
+ }
62
+
63
+ const sequencia = params.sequencia ?? 1;
64
+ const id = `ID${tpEvento}${params.chaveAcesso}${String(sequencia).padStart(3, '0')}`;
65
+
66
+ const root = create({ version: '1.0', encoding: 'UTF-8' }).ele('eventoDCe', {
67
+ xmlns: DCE_NAMESPACE,
68
+ versao: DCE_VERSAO,
69
+ });
70
+ const inf = root.ele('infEvento', { Id: id });
71
+ inf.ele('cOrgao').txt(chave.cUF);
72
+ inf.ele('tpAmb').txt(String(params.ambiente ?? 2));
73
+ inf.ele('tpEmit').txt(String(chave.tipoEmitente));
74
+ inf.ele('CNPJAutor').txt(chave.cnpj);
75
+ if (emitente.cnpj) inf.ele('CNPJUsEmit').txt(onlyDigits(emitente.cnpj));
76
+ else if (emitente.cpf) inf.ele('CPFUsEmit').txt(onlyDigits(emitente.cpf));
77
+ else inf.ele('IdOutrosUsEmit').txt(emitente.idOutros.trim());
78
+ inf.ele('chDCe').txt(params.chaveAcesso);
79
+ inf.ele('dhEvento').txt(formatSefazDateTime(params.dataEvento ?? new Date()));
80
+ inf.ele('tpEvento').txt(tpEvento);
81
+ inf.ele('nSeqEvento').txt(String(sequencia));
82
+ buildDetalhe(inf.ele('detEvento', { versaoEvento: DCE_VERSAO }));
83
+
84
+ return root.end({ prettyPrint: false, headless: false });
85
+ }
86
+ }
@@ -0,0 +1,345 @@
1
+ import { create } from 'xmlbuilder2';
2
+ import {
3
+ DCE_VERSAO,
4
+ DCE_MODELO,
5
+ DCE_NAMESPACE,
6
+ DCE_TIPO_EMITENTE,
7
+ DCE_MODALIDADE_TRANSPORTE,
8
+ DCE_DECLARACAO,
9
+ DCE_NOME_DEST_HOMOLOGACAO,
10
+ DCE_QRCODE_URL,
11
+ } from '../constants/dce.js';
12
+ import { gerarUrlQrCodeDCe } from '../utils/qrcode.utils.js';
13
+ import { parseChaveAcessoDCe } from '../../utils/chave.utils.js';
14
+ import { formatSefazDateTime } from '../../utils/date.utils.js';
15
+ import { formatDecimal } from '../../shared/fiscal.helpers.js';
16
+ import { GhinfeError } from '../../errors/GhinfeError.js';
17
+
18
+ const onlyDigits = (v) => String(v ?? '').replace(/\D/g, '');
19
+
20
+ /** Formata vUnCom (TDec_1302_1308): mínimo 2 e máximo 8 casas decimais. */
21
+ function formatValorUnitario(value) {
22
+ return Number(value).toFixed(8).replace(/(\.\d{2}\d*?)0+$/, '$1');
23
+ }
24
+
25
+ /** @param {import('../types/DCeDocument.js').DCeItem} item */
26
+ function valorTotalItem(item) {
27
+ return item.valorTotal ?? Math.round(item.quantidade * item.valorUnitario * 100) / 100;
28
+ }
29
+
30
+ /**
31
+ * Monta XML da DC-e (modelo 99, layout 1.00 — PL_DCe_v1.00a + NT 2024.001).
32
+ */
33
+ export class DCeXmlBuilder {
34
+ /**
35
+ * Gera o XML completo, incluindo infDCeSupl (QR Code), pronto para assinatura.
36
+ * @param {import('../types/DCeDocument.js').DCeDocumento} documento
37
+ * @param {{ qrCode?: string, urlChave?: string, sign?: string, qrCodeBaseUrl?: string }} [options]
38
+ * sign: assinatura da chave para QR Code em contingência off-line
39
+ * @returns {string}
40
+ */
41
+ build(documento, options = {}) {
42
+ const chave = parseChaveAcessoDCe(documento.chaveAcesso ?? '');
43
+ this.#validar(documento, chave);
44
+
45
+ const ambiente = documento.ambiente ?? 2;
46
+ const homologacao = ambiente === 2;
47
+
48
+ const root = create({ version: '1.0', encoding: 'UTF-8' }).ele('DCe', { xmlns: DCE_NAMESPACE });
49
+ const inf = root.ele('infDCe', { versao: DCE_VERSAO, Id: `DCe${documento.chaveAcesso}` });
50
+
51
+ // ide — campos da chave vêm dela para garantir a concatenação (rejeição 502)
52
+ const ide = inf.ele('ide');
53
+ ide.ele('cUF').txt(chave.cUF);
54
+ ide.ele('cDC').txt(chave.codigoNumerico);
55
+ ide.ele('mod').txt(DCE_MODELO);
56
+ ide.ele('serie').txt(String(chave.serie));
57
+ ide.ele('nDC').txt(String(chave.numero));
58
+ ide.ele('dhEmi').txt(formatSefazDateTime(documento.ide.dataEmissao ?? new Date()));
59
+ ide.ele('tpEmis').txt(String(chave.tipoEmissao));
60
+ ide.ele('tpEmit').txt(String(chave.tipoEmitente));
61
+ ide.ele('nSiteAutoriz').txt(String(chave.siteAutorizador));
62
+ ide.ele('cDV').txt(chave.digitoVerificador);
63
+ ide.ele('tpAmb').txt(String(ambiente));
64
+ ide.ele('verProc').txt('ghinfe-0.6.0');
65
+
66
+ // emit
67
+ const emit = inf.ele('emit');
68
+ this.#appendIdentificacao(emit, documento.emitente);
69
+ emit.ele('xNome').txt(documento.emitente.razaoSocial.trim());
70
+ this.#appendEndereco(emit, 'enderEmit', documento.emitente.endereco, { emitente: true });
71
+
72
+ // Fisco / Marketplace / Transportadora / ECT
73
+ this.#appendEmpresaEmissora(inf, chave.tipoEmitente, documento);
74
+
75
+ // dest
76
+ const dest = inf.ele('dest');
77
+ this.#appendIdentificacao(dest, documento.destinatario);
78
+ dest.ele('xNome').txt(homologacao ? DCE_NOME_DEST_HOMOLOGACAO : documento.destinatario.razaoSocial.trim());
79
+ this.#appendEndereco(dest, 'enderDest', documento.destinatario.endereco, { emitente: false });
80
+
81
+ // autXML
82
+ for (const aut of documento.autorizadosXml ?? []) {
83
+ const node = inf.ele('autXML');
84
+ if (aut.cnpj) node.ele('CNPJ').txt(onlyDigits(aut.cnpj));
85
+ else node.ele('CPF').txt(onlyDigits(aut.cpf));
86
+ }
87
+
88
+ // det
89
+ let valorTotal = 0;
90
+ documento.itens.forEach((item, index) => {
91
+ const vProd = valorTotalItem(item);
92
+ valorTotal += vProd;
93
+
94
+ const det = inf.ele('det', { nItem: String(index + 1) });
95
+ const prod = det.ele('prod');
96
+ prod.ele('xProd').txt(item.descricao.trim());
97
+ prod.ele('NCM').txt(onlyDigits(item.ncm));
98
+ prod.ele('qCom').txt(formatDecimal(item.quantidade, 4));
99
+ prod.ele('vUnCom').txt(formatValorUnitario(item.valorUnitario));
100
+ prod.ele('vProd').txt(formatDecimal(vProd, 2));
101
+ if (item.informacoesAdicionais) {
102
+ det.ele('infAdProd').txt(item.informacoesAdicionais.trim());
103
+ }
104
+ });
105
+
106
+ // total
107
+ inf.ele('total').ele('vDC').txt(formatDecimal(valorTotal, 2));
108
+
109
+ // transp — CNPJTransp é obrigatório no XSD, mas aceita vazio
110
+ const transp = inf.ele('transp');
111
+ transp.ele('modTrans').txt(String(documento.transporte.modalidade));
112
+ transp.ele('CNPJTransp').txt(onlyDigits(documento.transporte.cnpjTransportadora));
113
+
114
+ this.#appendInfAdic(inf, documento.informacoesAdicionais);
115
+
116
+ const infDec = inf.ele('infDec');
117
+ infDec.ele('xObs1').txt(DCE_DECLARACAO.xObs1);
118
+ infDec.ele('xObs2').txt(DCE_DECLARACAO.xObs2);
119
+
120
+ if (documento.solicitacaoFisco) {
121
+ root.ele('infSolicDCe').ele('xSolic').txt(documento.solicitacaoFisco);
122
+ }
123
+
124
+ const qrCode =
125
+ options.qrCode ??
126
+ gerarUrlQrCodeDCe({
127
+ chaveAcesso: documento.chaveAcesso,
128
+ ambiente,
129
+ tipoEmissao: chave.tipoEmissao,
130
+ emitente: documento.emitente,
131
+ sign: options.sign,
132
+ baseUrl: options.qrCodeBaseUrl,
133
+ });
134
+
135
+ const supl = root.ele('infDCeSupl');
136
+ supl.ele('qrCodDCe').dat(qrCode);
137
+ supl.ele('urlChave').txt(options.urlChave ?? options.qrCodeBaseUrl ?? DCE_QRCODE_URL);
138
+
139
+ return root.end({ prettyPrint: false, headless: false });
140
+ }
141
+
142
+ /**
143
+ * @param {import('xmlbuilder2').XMLBuilder} parent
144
+ * @param {{ cnpj?: string, cpf?: string, idOutros?: string }} parte
145
+ */
146
+ #appendIdentificacao(parent, parte) {
147
+ if (parte.cnpj) parent.ele('CNPJ').txt(onlyDigits(parte.cnpj));
148
+ else if (parte.cpf) parent.ele('CPF').txt(onlyDigits(parte.cpf));
149
+ else parent.ele('idOutros').txt(parte.idOutros.trim());
150
+ }
151
+
152
+ /**
153
+ * TEndeEmi aceita somente Brasil; TEndeDest permite país estrangeiro.
154
+ * @param {import('xmlbuilder2').XMLBuilder} parent
155
+ * @param {string} tag
156
+ * @param {import('../types/DCeDocument.js').DCeEndereco} endereco
157
+ * @param {{ emitente: boolean }} opts
158
+ */
159
+ #appendEndereco(parent, tag, endereco, { emitente }) {
160
+ const node = parent.ele(tag);
161
+ node.ele('xLgr').txt(endereco.logradouro.trim());
162
+ node.ele('nro').txt(String(endereco.numero).trim());
163
+ if (endereco.complemento) node.ele('xCpl').txt(endereco.complemento.trim());
164
+ node.ele('xBairro').txt(endereco.bairro.trim());
165
+ node.ele('cMun').txt(onlyDigits(endereco.codigoMunicipio));
166
+ node.ele('xMun').txt(endereco.municipio.trim());
167
+ node.ele('UF').txt(endereco.uf.toUpperCase());
168
+ node.ele('CEP').txt(onlyDigits(endereco.cep).padStart(8, '0'));
169
+ node.ele('cPais').txt(emitente ? '1058' : onlyDigits(endereco.codigoPais ?? '1058'));
170
+ node.ele('xPais').txt(emitente ? 'BRASIL' : (endereco.pais ?? 'BRASIL').trim());
171
+ if (endereco.telefone) node.ele('fone').txt(onlyDigits(endereco.telefone));
172
+ if (!emitente && endereco.email) node.ele('email').txt(endereco.email.trim());
173
+ }
174
+
175
+ /**
176
+ * @param {import('xmlbuilder2').XMLBuilder} inf
177
+ * @param {number} tipoEmitente
178
+ * @param {import('../types/DCeDocument.js').DCeDocumento} documento
179
+ */
180
+ #appendEmpresaEmissora(inf, tipoEmitente, documento) {
181
+ switch (tipoEmitente) {
182
+ case DCE_TIPO_EMITENTE.FISCO: {
183
+ const node = inf.ele('Fisco');
184
+ node.ele('CNPJ').txt(onlyDigits(documento.fisco.cnpj));
185
+ node.ele('xOrgao').txt(documento.fisco.orgao.trim());
186
+ node.ele('UF').txt(documento.fisco.uf.toUpperCase());
187
+ break;
188
+ }
189
+ case DCE_TIPO_EMITENTE.MARKETPLACE: {
190
+ const node = inf.ele('Marketplace');
191
+ node.ele('CNPJ').txt(onlyDigits(documento.marketplace.cnpj));
192
+ node.ele('xNome').txt(documento.marketplace.razaoSocial.trim());
193
+ node.ele('Site').txt(documento.marketplace.site.trim());
194
+ break;
195
+ }
196
+ case DCE_TIPO_EMITENTE.TRANSPORTADORA: {
197
+ const node = inf.ele('Transportadora');
198
+ node.ele('CNPJ').txt(onlyDigits(documento.transportadora.cnpj));
199
+ node.ele('xNome').txt(documento.transportadora.razaoSocial.trim());
200
+ break;
201
+ }
202
+ case DCE_TIPO_EMITENTE.ECT: {
203
+ const node = inf.ele('ECT');
204
+ node.ele('CNPJ').txt(onlyDigits(documento.ect.cnpj));
205
+ node.ele('xNome').txt(documento.ect.razaoSocial.trim());
206
+ break;
207
+ }
208
+ default:
209
+ break;
210
+ }
211
+ }
212
+
213
+ /**
214
+ * @param {import('xmlbuilder2').XMLBuilder} inf
215
+ * @param {import('../types/DCeDocument.js').DCeInformacoesAdicionais} [info]
216
+ */
217
+ #appendInfAdic(inf, info) {
218
+ if (!info) return;
219
+
220
+ const textos = [
221
+ ['infAdFisco', info.fisco],
222
+ ['infCpl', info.complementares],
223
+ ['infAdMarketplace', info.marketplace],
224
+ ['infAdECT', info.ect],
225
+ ].filter(([, valor]) => valor);
226
+
227
+ const observacoes = [
228
+ ['obsEmit', info.obsEmitente],
229
+ ['obsFisco', info.obsFisco],
230
+ ['obsMarketplace', info.obsMarketplace],
231
+ ['obsECT', info.obsECT],
232
+ ].filter(([, lista]) => lista?.length);
233
+
234
+ if (!textos.length && !observacoes.length) return;
235
+
236
+ const infAdic = inf.ele('infAdic');
237
+ for (const [tag, valor] of textos) {
238
+ infAdic.ele(tag).txt(valor.trim());
239
+ }
240
+ for (const [tag, lista] of observacoes) {
241
+ for (const obs of lista) {
242
+ infAdic.ele(tag, { xCampo: obs.campo.trim() }).ele('xTexto').txt(obs.texto.trim());
243
+ }
244
+ }
245
+ }
246
+
247
+ /**
248
+ * @param {import('../types/DCeDocument.js').DCeDocumento} documento
249
+ * @param {ReturnType<typeof parseChaveAcessoDCe>} chave
250
+ */
251
+ #validar(documento, chave) {
252
+ if (!chave.valida) {
253
+ throw new GhinfeError('chaveAcesso com dígito verificador inválido', 'CHAVE_DV_INVALIDO');
254
+ }
255
+ if (chave.modelo !== DCE_MODELO) {
256
+ throw new GhinfeError(`Modelo da chave deve ser 99 (recebido ${chave.modelo})`, 'DCE_MODELO_INVALIDO');
257
+ }
258
+
259
+ const tpEmit = chave.tipoEmitente;
260
+ if (!Object.values(DCE_TIPO_EMITENTE).includes(tpEmit)) {
261
+ throw new GhinfeError(`tpEmit inválido: ${tpEmit}`, 'DCE_TP_EMIT_INVALIDO');
262
+ }
263
+
264
+ const grupoObrigatorio = {
265
+ [DCE_TIPO_EMITENTE.FISCO]: ['fisco', 'Fisco'],
266
+ [DCE_TIPO_EMITENTE.MARKETPLACE]: ['marketplace', 'Marketplace'],
267
+ [DCE_TIPO_EMITENTE.TRANSPORTADORA]: ['transportadora', 'Transportadora'],
268
+ [DCE_TIPO_EMITENTE.ECT]: ['ect', 'ECT'],
269
+ }[tpEmit];
270
+
271
+ if (grupoObrigatorio && !documento[grupoObrigatorio[0]]?.cnpj) {
272
+ throw new GhinfeError(
273
+ `${grupoObrigatorio[0]}.cnpj é obrigatório para tpEmit=${tpEmit} (grupo ${grupoObrigatorio[1]})`,
274
+ 'DCE_EMPRESA_EMISSORA_AUSENTE'
275
+ );
276
+ }
277
+
278
+ const emitente = documento.emitente;
279
+ if (!emitente?.cnpj && !emitente?.cpf && !emitente?.idOutros) {
280
+ throw new GhinfeError('emitente.cnpj, emitente.cpf ou emitente.idOutros é obrigatório', 'INVALID_EMITENTE');
281
+ }
282
+ if (tpEmit === DCE_TIPO_EMITENTE.EMISSOR_PROPRIO && !emitente.cnpj) {
283
+ throw new GhinfeError(
284
+ 'Emissão própria (tpEmit=2) exige emitente com CNPJ; pessoa física emite via Fisco, Marketplace, Transportadora ou ECT',
285
+ 'DCE_EMISSAO_PROPRIA_SEM_CNPJ'
286
+ );
287
+ }
288
+ if (!emitente.razaoSocial || !emitente.endereco) {
289
+ throw new GhinfeError('emitente.razaoSocial e emitente.endereco são obrigatórios', 'INVALID_EMITENTE');
290
+ }
291
+
292
+ const dest = documento.destinatario;
293
+ if (!dest?.cnpj && !dest?.cpf && !dest?.idOutros) {
294
+ throw new GhinfeError(
295
+ 'destinatario.cnpj, destinatario.cpf ou destinatario.idOutros é obrigatório',
296
+ 'INVALID_DESTINATARIO'
297
+ );
298
+ }
299
+ if (!dest.razaoSocial || !dest.endereco) {
300
+ throw new GhinfeError('destinatario.razaoSocial e destinatario.endereco são obrigatórios', 'INVALID_DESTINATARIO');
301
+ }
302
+
303
+ if ((documento.autorizadosXml?.length ?? 0) > 10) {
304
+ throw new GhinfeError('Máximo de 10 autorizados (autXML)', 'DCE_AUTXML_EXCEDIDO');
305
+ }
306
+
307
+ if (!documento.itens?.length || documento.itens.length > 999) {
308
+ throw new GhinfeError('A DC-e deve conter de 1 a 999 itens', 'INVALID_ITENS');
309
+ }
310
+ documento.itens.forEach((item, index) => {
311
+ const nItem = index + 1;
312
+ if (!item.descricao) {
313
+ throw new GhinfeError(`Item ${nItem}: descricao é obrigatória`, 'INVALID_ITENS');
314
+ }
315
+ if (!/^(\d{2}|\d{8})$/.test(onlyDigits(item.ncm))) {
316
+ throw new GhinfeError(`Item ${nItem}: NCM deve ter 2 ou 8 dígitos`, 'DCE_NCM_INVALIDO');
317
+ }
318
+ if (!(item.quantidade > 0)) {
319
+ throw new GhinfeError(`Item ${nItem}: quantidade deve ser maior que zero`, 'INVALID_ITENS');
320
+ }
321
+ const calculado = Math.round(item.quantidade * item.valorUnitario * 100) / 100;
322
+ if (item.valorTotal !== undefined && Math.abs(item.valorTotal - calculado) > 0.01) {
323
+ throw new GhinfeError(
324
+ `Item ${nItem}: valorTotal (${item.valorTotal}) difere de quantidade × valorUnitario (${calculado})`,
325
+ 'DCE_VPROD_DIVERGENTE'
326
+ );
327
+ }
328
+ });
329
+
330
+ const modalidade = documento.transporte?.modalidade;
331
+ if (!Object.values(DCE_MODALIDADE_TRANSPORTE).includes(modalidade)) {
332
+ throw new GhinfeError('transporte.modalidade deve ser 0, 1 ou 2', 'DCE_MOD_TRANS_INVALIDO');
333
+ }
334
+ if (modalidade === DCE_MODALIDADE_TRANSPORTE.TRANSPORTADORA && !documento.transporte.cnpjTransportadora) {
335
+ throw new GhinfeError(
336
+ 'transporte.cnpjTransportadora é obrigatório quando modalidade=2',
337
+ 'DCE_CNPJ_TRANSP_AUSENTE'
338
+ );
339
+ }
340
+
341
+ if (documento.solicitacaoFisco && tpEmit !== DCE_TIPO_EMITENTE.FISCO) {
342
+ throw new GhinfeError('solicitacaoFisco só pode ser informada com tpEmit=0 (Fisco)', 'DCE_SOLIC_INDEVIDA');
343
+ }
344
+ }
345
+ }
@@ -0,0 +1,87 @@
1
+ /** Layout DC-e versão 1.00 (PL_DCe_v1.00a + NT 2024.001). */
2
+ export const DCE_VERSAO = '1.00';
3
+
4
+ /** Modelo da Declaração de Conteúdo eletrônica. */
5
+ export const DCE_MODELO = '99';
6
+
7
+ export const DCE_NAMESPACE = 'http://www.portalfiscal.inf.br/dce';
8
+
9
+ /** Tipo do emitente da DC-e (tpEmit). */
10
+ export const DCE_TIPO_EMITENTE = {
11
+ FISCO: 0,
12
+ MARKETPLACE: 1,
13
+ EMISSOR_PROPRIO: 2,
14
+ TRANSPORTADORA: 3,
15
+ ECT: 4,
16
+ };
17
+
18
+ /** Forma de emissão (tpEmis). */
19
+ export const DCE_TIPO_EMISSAO = {
20
+ NORMAL: 1,
21
+ CONTINGENCIA_OFFLINE: 2,
22
+ };
23
+
24
+ /** Modalidade do transporte (modTrans). */
25
+ export const DCE_MODALIDADE_TRANSPORTE = {
26
+ CORREIOS: 0,
27
+ PROPRIO: 1,
28
+ TRANSPORTADORA: 2,
29
+ };
30
+
31
+ /** Textos fixos obrigatórios do grupo infDec (Manual DC-e — Anexo I, ZA02/ZA03). */
32
+ export const DCE_DECLARACAO = {
33
+ xObs1:
34
+ 'É contribuinte de ICMS qualquer pessoa física ou jurídica, que realize, com habitualidade ' +
35
+ 'ou em volume que caracterize intuito comercial, operações de circulação de mercadoria ou ' +
36
+ 'prestações de serviços de transportes interestadual e intermunicipal e de comunicação, ' +
37
+ 'ainda que as operações e prestações de iniciem no exterior (Lei Complementar nº 87/96, Art. 4º)',
38
+ xObs2:
39
+ 'Constitui crime contra a ordem tributária suprimir ou reduzir tributo, ou contribuição ' +
40
+ 'social e qualquer acessório: quando negar ou deixar de fornecer, quando obrigatório, nota ' +
41
+ 'fiscal ou documento equivalente, relativa a venda de mercadoria ou prestação de serviço, ' +
42
+ 'efetivamente realizada ou fornece-la em desacordo com a legislação. Sob pena de reclusão ' +
43
+ 'de 2 (dois) e 5 (cinco) anos, e multa (Lei 8.137/90, Art 1ª, V)',
44
+ };
45
+
46
+ /** Literal exigido em xNome do destinatário em homologação (rejeição 598). */
47
+ export const DCE_NOME_DEST_HOMOLOGACAO = 'DCE EMITIDA EM AMBIENTE DE HOMOLOGACAO';
48
+
49
+ /**
50
+ * Web services do autorizador da DC-e (SEFA-PR), que atende todas as UFs.
51
+ * SOAP 1.2; namespace do corpo: http://www.portalfiscal.inf.br/dce/wsdl/{servico}.
52
+ */
53
+ export const DCE_WEBSERVICES = {
54
+ 1: {
55
+ autorizacao: 'https://dce.fazenda.pr.gov.br/dce/DCeAutorizacao',
56
+ consulta: 'https://dce.fazenda.pr.gov.br/dce/DCeConsulta',
57
+ status: 'https://dce.fazenda.pr.gov.br/dce/DCeStatusServico',
58
+ evento: 'https://dce.fazenda.pr.gov.br/dce/DCeRecepcaoEvento',
59
+ },
60
+ 2: {
61
+ autorizacao: 'https://homologacao.dce.fazenda.pr.gov.br/dce/DCeAutorizacao',
62
+ consulta: 'https://homologacao.dce.fazenda.pr.gov.br/dce/DCeConsulta',
63
+ status: 'https://homologacao.dce.fazenda.pr.gov.br/dce/DCeStatusServico',
64
+ evento: 'https://homologacao.dce.fazenda.pr.gov.br/dce/DCeRecepcaoEvento',
65
+ },
66
+ };
67
+
68
+ /** Tipos de evento da DC-e. */
69
+ export const DCE_EVENTOS = {
70
+ CANCELAMENTO: { codigo: '110111', descricao: 'Cancelamento' },
71
+ };
72
+
73
+ /** cStat de sucesso da DC-e. */
74
+ export const DCE_STATUS = {
75
+ AUTORIZADO: '100',
76
+ CANCELADO: '101',
77
+ SERVICO_EM_OPERACAO: '107',
78
+ EVENTO_REGISTRADO: '135',
79
+ EVENTO_REGISTRADO_SEM_VINCULO: '136',
80
+ CANCELAMENTO_FORA_PRAZO: '155',
81
+ };
82
+
83
+ /**
84
+ * URL de consulta via QR Code. O autorizador da DC-e (SEFA-PR) usa o mesmo
85
+ * endereço para produção e homologação — o ambiente vai no parâmetro tpAmb.
86
+ */
87
+ export const DCE_QRCODE_URL = 'https://www.fazenda.pr.gov.br/dce/qrcode';