/* * 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 { XmlBuilder, Environment, Utility, logger, BaseNFSe } from '@treeunfe/shared'; import { GerarConsultaImpl, NFSeDistribuicaoServiceImpl, SaveFilesImpl } from '@treeunfe/types/interfaces'; import { NFSeDistribuicaoPorNSU, NFSeDistribuicaoResponse, NFSeEventosPorChave } from '@treeunfe/types'; import { AxiosInstance } from 'axios'; import { gunzipSync } from 'zlib'; class NFSeDistribuicaoService extends BaseNFSe implements NFSeDistribuicaoServiceImpl { constructor(environment: Environment, utility: Utility, xmlBuilder: XmlBuilder, axios: AxiosInstance, saveFiles: SaveFilesImpl, gerarConsulta: GerarConsultaImpl) { super(environment, utility, xmlBuilder, 'NFSe_Distribuicao', axios, saveFiles, gerarConsulta); } protected prepararDados(_data?: any): any { return null; // GET request } protected getHttpMethod(): 'GET' | 'POST' | 'PUT' | 'DELETE' { return 'GET'; } protected getUrlPath(data?: any): string { if (data && 'chaveAcesso' in data) { return `/${(data as NFSeEventosPorChave).chaveAcesso}/Eventos`; } if (data && 'nsu' in data) { return `/${(data as NFSeDistribuicaoPorNSU).nsu}`; } return ''; } protected getQueryParams(data?: any): Record { if (data && 'nsu' in data) { const nsuData = data as NFSeDistribuicaoPorNSU; const params: Record = {}; if (nsuData.cnpjConsulta) { params.cnpjConsulta = nsuData.cnpjConsulta; } if (nsuData.lote !== undefined) { params.lote = nsuData.lote; } return params; } return {}; } protected getWebServiceUrl(): string { if (this.metodo === 'NFSe_EventosPorChave') { return this.utility.getWebServiceUrlNFSe('NFSe_EventosPorChave'); } return this.utility.getWebServiceUrlNFSe('NFSe_Distribuicao'); } async DistribuicaoPorNSU(data: NFSeDistribuicaoPorNSU): Promise { const response = await super.Exec(data) as NFSeDistribuicaoResponse; // Se a resposta contém documentos, processa e salva cada um if (response.LoteDFe && Array.isArray(response.LoteDFe) && response.LoteDFe.length > 0) { const config = this.environment.getConfig(); // Processa cada documento retornado for (const documento of response.LoteDFe) { if (documento.ArquivoXml && documento.ChaveAcesso) { try { // O ArquivoXml pode estar em dois formatos: // 1. Base64 simples → GZip direto // 2. Base64 duplo → Base64 → GZip let documentoXml: string; try { // Tenta primeiro: Base64 simples → GZip direto const documentoBuffer = Buffer.from(documento.ArquivoXml, 'base64'); documentoXml = gunzipSync(documentoBuffer).toString('utf-8'); } catch (gzipError) { // Se falhar, tenta: Base64 duplo → Base64 → GZip const primeiraDecodificacao = Buffer.from(documento.ArquivoXml, 'base64').toString('utf-8'); const documentoBuffer = Buffer.from(primeiraDecodificacao, 'base64'); documentoXml = gunzipSync(documentoBuffer).toString('utf-8'); } // Salva o XML do documento (apenas se estiver configurado) // Documentos autorizados devem ser salvos em Autorizacao/NFSe/ if (config.armazenarXMLAutorizacao && config.pathXMLAutorizacao) { let fileName: string; let pathCompleto: string; if (documento.TipoDocumento === 'EVENTO' && documento.TipoEvento) { // Evento: salva em subpasta do tipo de evento const tipoEventoMap: Record = { 'CANCELAMENTO': 101101, 'CANCELAMENTO_POR_SUBSTITUICAO': 105102, 'SOLICITACAO_CANCELAMENTO_ANALISE_FISCAL': 101103, 'CANCELAMENTO_DEFERIDO_ANALISE_FISCAL': 105104, 'CANCELAMENTO_INDEFERIDO_ANALISE_FISCAL': 105105, 'CONFIRMACAO_PRESTADOR': 202201, 'REJEICAO_PRESTADOR': 202205, 'CONFIRMACAO_TOMADOR': 203202, 'REJEICAO_TOMADOR': 203206, }; let tipoEvento: number; if (typeof documento.TipoEvento === 'string') { tipoEvento = tipoEventoMap[documento.TipoEvento] || parseInt(documento.TipoEvento, 10) || 101101; } else { tipoEvento = documento.TipoEvento as number; } const nsu = documento.NSU || 0; fileName = `${documento.ChaveAcesso}_evento_${tipoEvento}_${nsu}`; pathCompleto = `${config.pathXMLAutorizacao}/${tipoEvento}`; } else if (documento.TipoDocumento === 'NFSE') { // NFSe: salva na pasta raiz fileName = documento.ChaveAcesso; pathCompleto = config.pathXMLAutorizacao; } else { // Outro tipo de documento: salva na pasta raiz com prefixo const nsu = documento.NSU || 0; fileName = `${documento.ChaveAcesso}_${documento.TipoDocumento}_${nsu}`; pathCompleto = config.pathXMLAutorizacao; } logger.info('Tentando salvar XML do documento de DistribuicaoPorNSU', { context: 'NFSeDistribuicaoService', fileName: fileName, path: pathCompleto, tipoDocumento: documento.TipoDocumento, tipoEvento: documento.TipoEvento, nsu: documento.NSU }); this.utility.salvaXML({ data: documentoXml, fileName: fileName, metodo: this.metodo, path: pathCompleto, }); logger.info('XML do documento de DistribuicaoPorNSU salvo com sucesso', { context: 'NFSeDistribuicaoService', fileName: fileName, path: pathCompleto, tipoDocumento: documento.TipoDocumento }); } } catch (error: any) { logger.error('Erro ao processar XML do documento de DistribuicaoPorNSU', { context: 'NFSeDistribuicaoService', message: error.message, stack: error.stack, documento: documento }); } } } } return response; } async EventosPorChave(data: NFSeEventosPorChave): Promise { this.metodo = 'NFSe_EventosPorChave'; const response = await super.Exec(data) as NFSeDistribuicaoResponse; // Se a resposta contém eventos, processa e salva cada um if (response.LoteDFe && Array.isArray(response.LoteDFe) && response.LoteDFe.length > 0) { const config = this.environment.getConfig(); // Processa cada evento retornado for (const evento of response.LoteDFe) { // Apenas processa eventos (TipoDocumento = 'EVENTO' e TipoEvento presente) if (evento.TipoDocumento === 'EVENTO' && evento.TipoEvento) { if (evento.ArquivoXml) { try { // O ArquivoXml pode estar em dois formatos: // 1. Base64 simples → GZip direto (EventosPorChave) // 2. Base64 duplo → Base64 → GZip (ConsultarEvento) let eventoXml: string; try { // Tenta primeiro: Base64 simples → GZip direto const eventoBuffer = Buffer.from(evento.ArquivoXml, 'base64'); eventoXml = gunzipSync(eventoBuffer).toString('utf-8'); } catch (gzipError) { // Se falhar, tenta: Base64 duplo → Base64 → GZip const primeiraDecodificacao = Buffer.from(evento.ArquivoXml, 'base64').toString('utf-8'); const eventoBuffer = Buffer.from(primeiraDecodificacao, 'base64'); eventoXml = gunzipSync(eventoBuffer).toString('utf-8'); } // Mapeia o TipoEvento de string para número // O TipoEvento vem como string (ex: "CANCELAMENTO") e precisa ser convertido para número (ex: 101101) const tipoEventoMap: Record = { 'CANCELAMENTO': 101101, 'CANCELAMENTO_POR_SUBSTITUICAO': 105102, 'SOLICITACAO_CANCELAMENTO_ANALISE_FISCAL': 101103, 'CANCELAMENTO_DEFERIDO_ANALISE_FISCAL': 105104, 'CANCELAMENTO_INDEFERIDO_ANALISE_FISCAL': 105105, 'CONFIRMACAO_PRESTADOR': 202201, 'REJEICAO_PRESTADOR': 202205, 'CONFIRMACAO_TOMADOR': 203202, 'REJEICAO_TOMADOR': 203206, }; // Tenta converter string para número, ou usa o mapeamento let tipoEvento: number; if (typeof evento.TipoEvento === 'string') { tipoEvento = tipoEventoMap[evento.TipoEvento] || parseInt(evento.TipoEvento, 10) || 101101; } else { tipoEvento = evento.TipoEvento as number; } // Salva o XML do evento (apenas se estiver configurado) // Eventos de NFSe autorizada devem ser salvos em Autorizacao/NFSe/{tipoEvento}/ if (config.armazenarXMLAutorizacao && config.pathXMLAutorizacao && evento.ChaveAcesso) { const nsu = evento.NSU || 0; const fileName = `${evento.ChaveAcesso}_evento_${tipoEvento}_${nsu}`; // Cria o path com a subpasta do tipo de evento em Autorizacao/NFSe const pathComTipoEvento = `${config.pathXMLAutorizacao}/${tipoEvento}`; logger.info('Tentando salvar XML do evento de EventosPorChave', { context: 'NFSeDistribuicaoService', fileName: fileName, path: pathComTipoEvento, tipoEvento: tipoEvento, tipoEventoOriginal: evento.TipoEvento, nsu: nsu }); this.utility.salvaXML({ data: eventoXml, fileName: fileName, metodo: this.metodo, path: pathComTipoEvento, }); logger.info('XML do evento de EventosPorChave salvo com sucesso', { context: 'NFSeDistribuicaoService', fileName: fileName, path: pathComTipoEvento, tipoEvento: tipoEvento }); } } catch (error: any) { logger.error('Erro ao processar XML do evento de EventosPorChave', { context: 'NFSeDistribuicaoService', message: error.message, stack: error.stack, evento: evento }); } } } } } return response; } } export default NFSeDistribuicaoService;