/* * 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 { BaseNFe, Environment, Utility, ValidaCPFCNPJ, XmlBuilder, XmlParser, logger, mountCOFINS, mountICMS, mountPIS } from '@treeunfe/shared'; import { GenericObject, LayoutNFe, NFe, ProtNFe } from '@treeunfe/types'; import { AxiosInstance, AxiosResponse } from 'axios'; import { format } from 'date-fns'; import { Agent } from 'http'; import { GerarConsultaImpl, NFEAutorizacaoServiceImpl, SaveFilesImpl } from '@treeunfe/types/interfaces'; import NFERetornoAutorizacao from '../../operations/NFERetornoAutorizacao/NFERetornoAutorizacao.js'; import NFERetornoAutorizacaoService from '../NFERetornoAutorizacao/NFERetornoAutorizacaoService.js'; const T_MED = 5; // Tempo médio de resposta padrão em segundos class NFEAutorizacaoService extends BaseNFe implements NFEAutorizacaoServiceImpl { xmlNFe: string[]; constructor(environment: Environment, utility: Utility, xmlBuilder: XmlBuilder, axios: AxiosInstance, saveFiles: SaveFilesImpl, gerarConsulta: GerarConsultaImpl) { super(environment, utility, xmlBuilder, 'NFEAutorizacao', axios, saveFiles, gerarConsulta); this.xmlNFe = []; } protected gerarXml(data: NFe): string { return this.gerarXmlNFeAutorizacao(data); } protected salvaArquivos(_xmlConsulta: string, responseInJson: GenericObject, _xmlRetorno: AxiosResponse, _options?: Record): GenericObject { // Recupera configuração do ambiente para verificar se os arquivos gerados serão gravados em disco const config = this.environment.getConfig(); let dateAndTimeInFileName = config.incluirTimestampNoNomeDosArquivos; const createFileName = (prefix: string | undefined, includeMethodName?: boolean) => { const dtaTime = dateAndTimeInFileName ? `-${format(new Date(), 'dd-MM-yyyy-HHmm')}` : ''; const baseFileName = includeMethodName ? `${this.metodo}` : ''; const prefixPart = prefix ? includeMethodName ? `-${prefix}` : `${prefix}` : ''; const nfePart = responseInJson.chNFe ? `-${responseInJson.chNFe}` : ''; const dateTimePart = dtaTime; return `${baseFileName}${prefixPart}${nfePart}${dateTimePart}`; } const salvarArquivo = (data: any, prefix: string | undefined, path: string | undefined, fileType: 'xml' | 'json', includeMethodName?: boolean) => { const fileName = createFileName(prefix, includeMethodName); const method = fileType === 'xml' ? 'salvaXML' : 'salvaJSON'; this.utility[method]({ data: data, fileName, metodo: this.metodo, path, }); }; let xmlAutorizacaoInJson: GenericObject = {} as GenericObject; let xMotivoPorXml: GenericObject[] = []; let xmlsInJson: GenericObject[] = []; if (_options) { const { xmlAutorizacao } = _options; const json = new XmlParser(); for (let i = 0; i < xmlAutorizacao.length; i++) { xmlAutorizacaoInJson = json.convertXmlToJson(xmlAutorizacao[i], 'NFEAutorizacaoFinal'); xmlsInJson.push(xmlAutorizacaoInJson); const chNFe = xmlAutorizacaoInJson.protNFe.infProt.chNFe; const xMotivo = xmlAutorizacaoInJson.protNFe.infProt.xMotivo; const cStat = xmlAutorizacaoInJson.protNFe.infProt.cStat; xMotivoPorXml.push({ chNFe, xMotivo, cStat, }) if (config.armazenarXMLAutorizacao) { salvarArquivo(xmlAutorizacao[i], chNFe, config.pathXMLAutorizacao, 'xml', false); salvarArquivo(xmlAutorizacaoInJson, chNFe, config.pathXMLAutorizacao, 'json', false); } } return { success: true, xMotivo: xMotivoPorXml, response: xmlsInJson, } } return { success: true, xMotivo: xMotivoPorXml, response: xmlsInJson, } } private async trataRetorno(_xmlRetorno: string, indSinc: number, responseInJson: GenericObject) { /** * nRec / protNFe: mesmo JSON já produzido por verificaRejeicao (xml-js + retEnviNFe), * evitando segundo parse com xml2js e alinhando ao formato esperado no merge com a NFe. */ const { nRec, protNFe } = this.utility.extractNRecProtFromLoteBody(responseInJson); const hasProtNFe = Array.isArray(protNFe) ? protNFe.length > 0 : Boolean(protNFe); if (!hasProtNFe && !nRec) { throw new Error(this.utility.describeRetEnviNFeSemProtocolo(responseInJson)); } /** * 0 - assíncrona * 1 - síncrona */ let tipoEmissao = 0; if (indSinc === 1 && hasProtNFe) { tipoEmissao = 1; } const nfeRetornoAutService = new NFERetornoAutorizacaoService(this.environment, this.utility, this.xmlBuilder, this.axios, this.saveFiles, this.gerarConsulta); const nfeRetornoAut = new NFERetornoAutorizacao(nfeRetornoAutService); /** * Aguarda o Tempo médio de resposta do serviço (em segundos) dos últimos 5 minutos * A informação do tMed só é recebida caso o processamento for assíncrono (indSinc = 0) */ if (tipoEmissao !== 1) await new Promise(resolve => setTimeout(resolve, Number(responseInJson.infRec?.tMed || T_MED) * 1000)); const retorno = await nfeRetornoAut.getXmlRetorno({ tipoEmissao, nRec, protNFe, xmlNFe: this.xmlNFe }); return retorno; } /** * Método utilitário para criação do XML a partir de um Objeto */ private anoMesEmissao(dhEmi: string) { // Lógica para obter o ano e mês de emissão (AAMM) const dataAtual = new Date(dhEmi); const ano = dataAtual.getFullYear().toString().slice(-2); const mes = (dataAtual.getMonth() + 1).toString().padStart(2, '0'); return ano + mes; } // Método não utilizado atualmente, mantido para possível uso futuro // private gerarCodigoNumerico() { // // Lógica para gerar um código numérico aleatório de 8 dígitos // return Math.floor(Math.random() * 100000000).toString().padStart(8, '0'); // } private calcularModulo11(sequencia: string) { const pesos = [4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]; let somatoria = 0; for (let i = 0; i < sequencia.length; i++) { somatoria += parseInt(sequencia.charAt(i)) * pesos[i]; } const restoDivisao = somatoria % 11; const digitoVerificador = restoDivisao === 0 || restoDivisao === 1 ? 0 : 11 - restoDivisao; return digitoVerificador; } private calcularDigitoVerificador(data: LayoutNFe) { const { infNFe: { Id, ide: { cUF, mod, serie, nNF, tpEmis, cNF, dhEmi }, emit: { CNPJCPF }, }, } = data if (Id) { this.chaveNfe = Id return { chaveAcesso: `NFe${Id}`, dv: parseInt(Id.charAt(Id.length - 1), 10), } } const anoMes = this.anoMesEmissao(dhEmi); // Montando a sequência para o cálculo do dígito verificador const sequencia = `${cUF}${anoMes}${CNPJCPF}${mod}${String(serie).padStart(3, '0')}${String(nNF).padStart(9, '0')}${tpEmis}${cNF}`; // Calculando o dígito verificador const dv = this.calcularModulo11(sequencia); // Montando a chave de acesso const chaveAcesso = `NFe${sequencia}` + dv; this.chaveNfe = `${sequencia}${dv}`; return { chaveAcesso, dv }; } private validaDocumento(doc: string, campo: string) { // Valida se CPF ou CNPJ const nfeAutorizacaoHandler = new ValidaCPFCNPJ(); const { documentoValido, tipoDoDocumento } = nfeAutorizacaoHandler.validarCpfCnpj(doc); if (!documentoValido || tipoDoDocumento === 'Desconhecido') { const message = tipoDoDocumento === 'Desconhecido' ? `Documento do ${campo} ausente ou inválido` : `${tipoDoDocumento} do ${campo} é inválido` throw new Error(message); } return tipoDoDocumento; } private gerarXmlNFeAutorizacao(data: NFe) { logger.info('Montando estrutuda do XML em JSON', { context: 'NFEAutorizacaoService', }); const createXML = (NFe: LayoutNFe) => { const rawDet = NFe?.infNFe?.det; if (rawDet) { const dets = rawDet instanceof Array ? rawDet : [rawDet]; const formatedItens = dets.map((det, index) => { if (det.imposto?.ICMS?.dadosICMS) { const icms = mountICMS(det.imposto.ICMS.dadosICMS); det.imposto.ICMS = icms; } if (det.imposto?.PIS?.dadosPIS) { const pis = mountPIS(det.imposto.PIS.dadosPIS); det.imposto.PIS = pis; } if (det.imposto?.COFINS?.dadosCOFINS) { const cofins = mountCOFINS(det.imposto.COFINS.dadosCOFINS); det.imposto.COFINS = cofins; } return { $: { nItem: index + 1, }, ...det, }; }); NFe.infNFe.det = formatedItens; } // Cria chave da nota e grava digito verificador const { chaveAcesso, dv } = this.calcularDigitoVerificador(NFe); NFe.infNFe.ide.cDV = dv; NFe.infNFe.ide.verProc = NFe.infNFe.ide.verProc || '1.0.0.0'; delete NFe.infNFe.Id // Valida Documento do emitente NFe.infNFe.emit = Object.assign({ [this.validaDocumento(String(NFe.infNFe.emit.CNPJCPF), 'emitente')]: NFe.infNFe.emit.CNPJCPF }, NFe.infNFe.emit) delete NFe.infNFe.emit.CNPJCPF; // Valida Documento do destinatário if (NFe.infNFe.dest) { NFe.infNFe.dest = Object.assign({ [this.validaDocumento(String(NFe.infNFe.dest?.CNPJCPF || ''), 'destinatário')]: NFe.infNFe.dest?.CNPJCPF || '' }, NFe.infNFe.dest) delete NFe.infNFe.dest.CNPJCPF; } // Valida Documento do transportador if (NFe.infNFe.transp.transporta) { NFe.infNFe.transp.transporta = Object.assign({ [this.validaDocumento(String(NFe.infNFe.transp.transporta?.CNPJCPF), 'transportador')]: NFe.infNFe.transp.transporta?.CNPJCPF }, NFe.infNFe.transp.transporta) delete NFe.infNFe.transp.transporta?.CNPJCPF; } // Valida Documento do produtor rural if (NFe.infNFe?.NFref instanceof Array) { const NFrefArray = NFe.infNFe.NFref; if (NFrefArray && NFrefArray.length > 0) { NFe.infNFe.NFref = NFrefArray.map(NFref => { if (NFref.refNFP) { NFref.refNFP = Object.assign( { [this.validaDocumento(String(NFref.refNFP.CNPJCPF), 'produtor rural')]: NFref.refNFP.CNPJCPF }, NFref.refNFP ); delete NFref.refNFP.CNPJCPF; } return NFref; }); } } else { if (NFe.infNFe.NFref && NFe.infNFe.NFref.refNFP) { NFe.infNFe.NFref.refNFP = Object.assign( { [this.validaDocumento(String(NFe.infNFe.NFref.refNFP.CNPJCPF), 'produtor rural')]: NFe.infNFe.NFref.refNFP.CNPJCPF }, NFe.infNFe.NFref.refNFP ) } } // Calculo do hash do CSRT e preenchimento automático do idCSRT if ( NFe.infNFe.emit.enderEmit.UF === "PR" && NFe.infNFe.ide.tpAmb === 2 && NFe.infNFe.infRespTec ) { const { tokenCSC, idCSRT } = this.environment.getConfig() as any; if (!tokenCSC) { throw new Error('tokenCSC não configurado. É necessário configurar o tokenCSC nas configurações para calcular o hash do CSRT.'); } if (!idCSRT) { throw new Error('idCSRT não configurado. É necessário configurar o idCSRT nas configurações para preencher o identificador do CSRT.'); } // Preenche automaticamente o idCSRT a partir das configurações NFe.infNFe.infRespTec.idCSRT = idCSRT; const hashCSRT = this.utility.calcularHashCSRT( tokenCSC, // remove o 3 primeiros caracteres da chave de acesso equivalente a sigla NFe chaveAcesso.slice(3) ); NFe.infNFe.infRespTec.hashCSRT = hashCSRT; } // Caso Seja hambiente de homologação if (NFe.infNFe.dest) { if (NFe.infNFe.ide.tpAmb === 2) { NFe.infNFe.dest.xNome = 'NF-E EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCAL'; } } const xmlObject = { $: { xmlns: 'http://www.portalfiscal.inf.br/nfe' }, infNFe: { $: { versao: "4.00", Id: chaveAcesso, }, ...NFe.infNFe } } const eventoXML = this.xmlBuilder.gerarXml(xmlObject, 'NFe', this.metodo) const xmlAssinado = this.xmlBuilder.assinarXML(eventoXML, 'infNFe') this.xmlNFe.push(xmlAssinado); } if (data.NFe instanceof Array) { for (let i = 0; i < data.NFe.length; i++) { const NFe = data.NFe[i]; createXML(NFe); } } else { createXML(data.NFe); } // Base do XML const baseXML = { $: { versao: "4.00", xmlns: 'http://www.portalfiscal.inf.br/nfe' }, idLote: data.idLote, indSinc: data.indSinc, _: '[XML]' } // Gera base do XML const xml = this.xmlBuilder.gerarXml(baseXML, 'enviNFe', this.metodo) return xml.replace('[XML]', this.xmlNFe.join('')); } protected async callWebService(xmlConsulta: string, webServiceUrl: string, ContentType: string, action: string, agent: Agent): Promise> { const startTime = Date.now(); const headers = { 'Content-Type': ContentType, }; logger.http('Iniciando comunicação com o webservice', { context: `NFEAutorizacaoService`, method: this.metodo, url: webServiceUrl, action, headers, }); const response = await this.axios.post(webServiceUrl, xmlConsulta, { headers, httpsAgent: agent }); const duration = Date.now() - startTime; logger.http('Comunicação concluída com sucesso', { context: `NFEAutorizacaoService`, method: this.metodo, duration: `${duration}ms`, responseSize: response.data ? JSON.stringify(response.data).length : 0 }); return response; } public async Exec(data: NFe): Promise<{ success: boolean; xMotivo: GenericObject; xmls: { NFe: LayoutNFe; protNFe: ProtNFe }[]; }> { let xmlConsulta: string = ''; let xmlConsultaSoap: string = ''; // webServiceUrlTmp não é usado, mas mantido para compatibilidade // let webServiceUrlTmp: string = ''; let responseInJson: GenericObject | undefined = undefined; let xmlRetorno: AxiosResponse = {} as AxiosResponse; const ContentType = this.setContentType(); try { // Gerando XML para consulta de Status do Serviço xmlConsulta = this.gerarXmlNFeAutorizacao(data); const { xmlFormated, agent, webServiceUrl, action } = await this.gerarConsulta.gerarConsulta(xmlConsulta, this.metodo); xmlConsultaSoap = xmlFormated; // webServiceUrlTmp não é usado, mas mantido para compatibilidade // webServiceUrlTmp = webServiceUrl; // Efetua requisição para o webservice NFEStatusServico const xmlRetorno = await this.callWebService(xmlFormated, webServiceUrl, ContentType, action, agent); /** * Verifica se houve rejeição no processamento do lote */ responseInJson = this.utility.verificaRejeicao(xmlRetorno.data, this.metodo); const retorno = await this.trataRetorno(xmlRetorno.data, data.indSinc, responseInJson); const xmlFinal = this.salvaArquivos(xmlConsulta, responseInJson, xmlRetorno.data, { xmlAutorizacao: retorno.data, xMotivo: retorno.message }) logger.info('NFe transmitida com sucesso', { context: 'NFEAutorizacaoService', }); return { success: true, xMotivo: xmlFinal.xMotivo, xmls: xmlFinal.response, } } finally { // Salva XML de Consulta this.utility.salvaConsulta(xmlConsulta, xmlConsultaSoap, this.metodo); // Salva XML de Retorno this.utility.salvaRetorno(xmlRetorno.data, responseInJson, this.metodo); } } } export default NFEAutorizacaoService;