/* * 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; } } } }