/*
* This file is part of Treeunfe DFe.
*
* Treeunfe DFe is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Treeunfe DFe is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Treeunfe DFe. If not, see .
*/
import { gunzipSync } from 'zlib';
import { Buffer } from 'buffer';
import { XmlParser } from '@treeunfe/shared';
import { LayoutNFSe } from '@treeunfe/types';
/**
* Descompacta o `nfseXmlGZipB64` retornado pela `Autorizacao` da NFS-e Nacional
* (gzip + base64) para a string XML autorizada.
*
* @example
* ```typescript
* const xml = decodeNFSeB64Gzip(response.nfseXmlGZipB64);
* ```
*/
export function decodeNFSeB64Gzip(b64: string): string {
if (!b64 || typeof b64 !== 'string') {
throw new Error('decodeNFSeB64Gzip: entrada vazia ou não-string.');
}
try {
const buffer = Buffer.from(b64, 'base64');
return gunzipSync(buffer).toString('utf-8');
} catch (error: any) {
throw new Error(`decodeNFSeB64Gzip: falha ao descompactar base64+gzip — ${error.message}`);
}
}
/**
* Converte o XML autorizado da NFS-e Nacional em `LayoutNFSe` tipado, pronto
* para alimentar `NFSeGerarDanfse` / `nfseHandler.GerarDanfse(...)`.
*
* Aceita tanto o XML "puro" `...` quanto o envelopado
* `...` (forma persistida no disco quando
* `armazenarXMLAutorizacao = true`). Aplica coerção numérica nos campos
* monetários e percentuais conhecidos da NT-008.
*
* @example
* ```typescript
* const xml = decodeNFSeB64Gzip(response.nfseXmlGZipB64);
* const NFSe = parseNFSeXml(xml);
* ```
*/
export function parseNFSeXml(xml: string | Buffer): LayoutNFSe {
if (!xml) {
throw new Error('parseNFSeXml: XML vazio.');
}
const xmlStr = typeof xml === 'string' ? xml : xml.toString('utf-8');
const parser = new XmlParser();
const json = parser.parseXmlToCompactJson(xmlStr) as any;
const nfseRoot = extractNFSeRoot(json);
if (!nfseRoot || typeof nfseRoot !== 'object') {
throw new Error('parseNFSeXml: estrutura não encontrada no XML.');
}
if (!nfseRoot.infNFSe) {
throw new Error('parseNFSeXml: sem .');
}
// O XmlParser compartilhado usa `ignoreAttributes: true`, descartando os
// atributos no parse. Mas o `Id` da (chave de 50 dígitos
// prefixada por "NFS") e o `Id` da são essenciais para a DANFSe.
// Recuperamos esses atributos via regex direto na string XML e injetamos
// no objeto convertido, somente se ainda estiverem ausentes.
const infNFSeId = extractAttributeFromXml(xmlStr, 'infNFSe', 'Id');
if (infNFSeId && !nfseRoot.infNFSe.id) {
nfseRoot.infNFSe.id = infNFSeId;
}
const infDPSContainer = nfseRoot.infNFSe.DPS?.infDPS;
if (infDPSContainer && !infDPSContainer.id) {
const infDPSId = extractAttributeFromXml(xmlStr, 'infDPS', 'Id');
if (infDPSId) infDPSContainer.id = infDPSId;
}
coerceNumericFields(nfseRoot.infNFSe);
return nfseRoot as LayoutNFSe;
}
/**
* Extrai um atributo de uma tag XML usando regex resiliente a múltiplas
* formatações (aspas simples ou duplas, espaços extras, case-insensitive no
* nome do atributo). Pensado para recuperar o `Id` da /
* que o parser principal descarta.
*/
function extractAttributeFromXml(xml: string, tagName: string, attrName: string): string {
const re = new RegExp(`<${tagName}\\b[^>]*\\b${attrName}\\s*=\\s*["']([^"']*)["']`, 'i');
const match = xml.match(re);
return match ? match[1].trim() : '';
}
/**
* Localiza o nó `` no JSON convertido pelo `xml-js` (modo compacto),
* lidando com os envelopes possíveis: `` ou `` direto na raiz.
*/
function extractNFSeRoot(json: any): any {
if (!json || typeof json !== 'object') return null;
if (json.NFSe) return json.NFSe;
if (json.nfseProc?.NFSe) return json.nfseProc.NFSe;
for (const key of Object.keys(json)) {
const node = json[key];
if (node && typeof node === 'object') {
const found = extractNFSeRoot(node);
if (found) return found;
}
}
return null;
}
/**
* Conjunto de chaves cujo valor é monetário/percentual (campos numéricos da
* NT-008). A coerção é seletiva para não corromper códigos/strings (ex.: `nNFSe`,
* `cStat`, `id`, CEPs, IBGE) que devem permanecer textuais.
*/
const NUMERIC_KEYS = new Set([
'vBC',
'pAliqAplic',
'vISSQN',
'vTotalRet',
'vLiq',
'vCalcBM',
'vRedBCBM',
'vCalcDR',
'vCalcReeRepRes',
'vServ',
'vReceb',
'vDescIncond',
'vDescCond',
'vRetCP',
'vRetIRRF',
'vRetCSLL',
'vPis',
'vCofins',
'vBCPisCofins',
'pAliqPis',
'pAliqCofins',
'pAliq',
'vTotTribFed',
'vTotTribEst',
'vTotTribMun',
'pTotTribFed',
'pTotTribEst',
'pTotTribMun',
'pTotTribSN',
'pRedAliqUF',
'pIBSUF',
'pAliqEfetUF',
'pRedAliqMun',
'pIBSMun',
'pAliqEfetMun',
'pRedAliqCBS',
'pCBS',
'pAliqEfetCBS',
'vIBSMun',
'vIBSUF',
'vIBSTot',
'vCBS',
'vTotNF',
]);
/**
* Percorre recursivamente o objeto e converte os valores das chaves listadas em
* {@link NUMERIC_KEYS} para `number`. Strings vazias viram `0` (compatível com
* a renderização do DANFSe — sem valor = R$ 0,00).
*/
function coerceNumericFields(node: any): void {
if (!node || typeof node !== 'object') return;
if (Array.isArray(node)) {
node.forEach((item) => coerceNumericFields(item));
return;
}
for (const key of Object.keys(node)) {
const value = node[key];
if (value && typeof value === 'object') {
coerceNumericFields(value);
continue;
}
if (NUMERIC_KEYS.has(key) && (typeof value === 'string' || typeof value === 'number')) {
const str = String(value).trim();
if (str === '') {
node[key] = 0;
} else {
const num = Number(str);
node[key] = Number.isFinite(num) ? num : value;
}
}
}
}