/*
* 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, NFSeEventosServiceImpl, SaveFilesImpl } from '@treeunfe/types/interfaces';
import { LayoutPedRegEvento, NFSeEventoConsulta, NFSeEventoRequest, NFSeEventoResponse, TipoEventoNFSe } from '@treeunfe/types';
import { AxiosInstance } from 'axios';
import { gunzipSync, gzipSync } from 'zlib';
class NFSeEventosService extends BaseNFSe implements NFSeEventosServiceImpl {
private pedidoRegistroEventoXmlGZipB64: string = '';
constructor(environment: Environment, utility: Utility, xmlBuilder: XmlBuilder, axios: AxiosInstance, saveFiles: SaveFilesImpl, gerarConsulta: GerarConsultaImpl) {
super(environment, utility, xmlBuilder, 'NFSe_Eventos', axios, saveFiles, gerarConsulta);
}
/**
* Gera o ID do pedido de registro de evento
* Formato: "PRE" + Chave de Acesso NFS-e + Tipo do evento + Número do Pedido de Registro do Evento (nPedRegEvento)
* Padrão esperado: PRE[0-9]{56} = 59 caracteres total (PRE + 56 dígitos)
* Como temos: chave (50) + tipo (6) + nPed (3) = 59 dígitos, mas o padrão espera 56
* A chave de acesso deve ser truncada para 47 dígitos: 47 + 6 + 3 = 56 dígitos
*/
private gerarIdPedRegEvento(chaveAcesso: string, tipoEvento: TipoEventoNFSe, nPedRegEvento: number): string {
// Remove todos os caracteres não numéricos da chave de acesso
const chaveLimpa = chaveAcesso.replace(/\D/g, '');
// O padrão espera PRE + 56 dígitos = 59 caracteres total
// Como temos tipo (6) + nPed (3) = 9 dígitos fixos, a chave deve ter 47 dígitos
// Trunca a chave para 47 dígitos (pega os primeiros 47)
const chave47 = chaveLimpa.substring(0, 47).padStart(47, '0');
// Tipo do evento com 6 dígitos
const tipoEventoStr = tipoEvento.toString().padStart(6, '0');
// Número do pedido com 3 dígitos
const nPedRegEventoStr = nPedRegEvento.toString().padStart(3, '0');
const id = `PRE${chave47}${tipoEventoStr}${nPedRegEventoStr}`;
// Validação: deve ter exatamente 59 caracteres (PRE + 56 dígitos)
if (id.length !== 59) {
logger.warn(`ID do pedido de registro de evento tem tamanho incorreto: ${id.length} (esperado: 59)`, { context: 'gerarIdPedRegEvento', id });
}
return id;
}
/**
* Gera o XML do pedido de registro de evento
*/
private gerarXmlPedRegEvento(pedRegEvento: LayoutPedRegEvento, chaveAcesso: string, tipoEvento: TipoEventoNFSe, nPedRegEvento: number): string {
// Gera o ID do pedido
const idPedReg = this.gerarIdPedRegEvento(chaveAcesso, tipoEvento, nPedRegEvento);
// Monta o objeto XML
// IMPORTANTE: A ordem dos elementos deve seguir o schema:
// tpAmb, verAplic, dhEvento, (CNPJAutor ou CPFAutor), chNFSe, (evento específico)
// Construímos o objeto na ordem correta para garantir que o xml2js respeite a sequência
const infPedRegObj: any = {
$: {
Id: idPedReg
},
tpAmb: pedRegEvento.infPedReg.tpAmb,
verAplic: pedRegEvento.infPedReg.verAplic,
dhEvento: pedRegEvento.infPedReg.dhEvento
};
// Adiciona CNPJ ou CPF do autor (deve vir ANTES de chNFSe)
if (pedRegEvento.infPedReg.CNPJAutor) {
infPedRegObj.CNPJAutor = pedRegEvento.infPedReg.CNPJAutor;
} else if (pedRegEvento.infPedReg.CPFAutor) {
infPedRegObj.CPFAutor = pedRegEvento.infPedReg.CPFAutor;
}
// Adiciona chNFSe (deve vir DEPOIS de CNPJAutor/CPFAutor)
infPedRegObj.chNFSe = pedRegEvento.infPedReg.chNFSe;
// Adiciona o evento específico (e101101 para cancelamento)
if (pedRegEvento.infPedReg.e101101) {
infPedRegObj.e101101 = {
xDesc: pedRegEvento.infPedReg.e101101.xDesc,
cMotivo: pedRegEvento.infPedReg.e101101.cMotivo,
xMotivo: pedRegEvento.infPedReg.e101101.xMotivo
};
} else if (pedRegEvento.infPedReg.e105102) {
infPedRegObj.e105102 = {
xDesc: pedRegEvento.infPedReg.e105102.xDesc,
chNFSeSubst: pedRegEvento.infPedReg.e105102.chNFSeSubst
};
}
// O xml2js.Builder cria o elemento raiz automaticamente a partir do rootTag
// Então passamos apenas o conteúdo interno com os atributos
const pedRegEventoObject: any = {
$: {
versao: '1.01',
xmlns: 'http://www.sped.fazenda.gov.br/nfse'
},
infPedReg: infPedRegObj
};
// Gera o XML (o rootTag 'pedRegEvento' será usado como elemento raiz)
let xml = this.xmlBuilder.gerarXml(pedRegEventoObject, 'pedRegEvento', this.metodo);
// Adiciona a declaração XML com encoding UTF-8 se não existir
if (!xml.trim().startsWith('\n' + xml;
} else {
xml = xml.replace(/^<\?xml[^>]*\?>/, '');
}
// Assina o XML
let xmlAssinado = this.xmlBuilder.assinarXML(xml, 'infPedReg');
// Garante que o XML assinado tenha a declaração UTF-8
if (!xmlAssinado.trim().startsWith('\n' + xmlAssinado;
} else {
xmlAssinado = xmlAssinado.replace(/^<\?xml[^>]*\?>/, '');
}
return xmlAssinado;
}
/**
* Processa o pedido de registro de evento: monta XML, assina, compacta e codifica
*/
private processarPedRegEvento(data: NFSeEventoRequest, tipoEvento: TipoEventoNFSe, nPedRegEvento: number = 1): string {
// Se já foi fornecido o Base64, usa diretamente
if (data.pedidoRegistroEventoXmlGZipB64) {
return data.pedidoRegistroEventoXmlGZipB64;
}
// Se foi fornecido o JSON, processa
if (data.pedRegEvento) {
const xml = this.gerarXmlPedRegEvento(data.pedRegEvento, data.chaveAcesso, tipoEvento, nPedRegEvento);
// Compacta em GZip
const xmlGzip = gzipSync(Buffer.from(xml, 'utf-8'));
// Codifica em Base64
const xmlGzipB64 = xmlGzip.toString('base64');
return xmlGzipB64;
}
throw new Error('É necessário fornecer pedRegEvento (JSON) ou pedidoRegistroEventoXmlGZipB64 (Base64)');
}
protected prepararDados(data?: any): any {
// Para consulta (GET), não precisa preparar dados
if (this.metodo.includes('Consultar') || this.getHttpMethod() === 'GET') {
return null;
}
// Para registro de evento (POST), processa os dados
const eventoData = data as NFSeEventoRequest;
// Determina o tipo de evento baseado no pedRegEvento
let tipoEvento: TipoEventoNFSe = TipoEventoNFSe.CANCELAMENTO; // Default
if (eventoData.pedRegEvento) {
if (eventoData.pedRegEvento.infPedReg.e101101) {
tipoEvento = TipoEventoNFSe.CANCELAMENTO;
} else if (eventoData.pedRegEvento.infPedReg.e105102) {
tipoEvento = TipoEventoNFSe.CANCELAMENTO_POR_SUBSTITUICAO;
}
}
// Processa o pedido de registro de evento antes de enviar
this.pedidoRegistroEventoXmlGZipB64 = this.processarPedRegEvento(eventoData, tipoEvento);
return {
pedidoRegistroEventoXmlGZipB64: this.pedidoRegistroEventoXmlGZipB64
};
}
/**
* Método para obter a URL base do webservice
* Consulta de evento usa a mesma URL base que registro de evento
*/
protected getWebServiceUrl(): string {
// Consulta de evento usa a mesma URL base que registro de evento
const metodoUrl = this.metodo.includes('Consultar') ? 'NFSe_Eventos' : this.metodo;
return this.utility.getWebServiceUrlNFSe(metodoUrl);
}
protected getHttpMethod(): 'GET' | 'POST' | 'PUT' | 'DELETE' {
if (this.metodo.includes('Consultar')) {
return 'GET';
}
return 'POST';
}
protected getUrlPath(data?: any): string {
if (data && 'tipoEvento' in data && 'numSeqEvento' in data) {
const eventoData = data as NFSeEventoConsulta;
return `/${eventoData.chaveAcesso}/eventos/${eventoData.tipoEvento}/${eventoData.numSeqEvento}`;
}
if (data && 'chaveAcesso' in data) {
return `/${(data as NFSeEventoRequest).chaveAcesso}/eventos`;
}
return '';
}
async RegistrarEvento(data: NFSeEventoRequest): Promise {
const response = await super.Exec(data) as NFSeEventoResponse;
// Se a resposta contém evento em base64 gzip, descompacta e salva
if (response.eventoXmlGZipB64) {
try {
const eventoBuffer = Buffer.from(response.eventoXmlGZipB64, 'base64');
const eventoXml = gunzipSync(eventoBuffer).toString('utf-8');
// Determina o tipo de evento baseado no pedRegEvento
let tipoEvento: TipoEventoNFSe = TipoEventoNFSe.CANCELAMENTO; // Default
if (data.pedRegEvento) {
if (data.pedRegEvento.infPedReg.e101101) {
tipoEvento = TipoEventoNFSe.CANCELAMENTO;
} else if (data.pedRegEvento.infPedReg.e105102) {
tipoEvento = TipoEventoNFSe.CANCELAMENTO_POR_SUBSTITUICAO;
} else if (data.pedRegEvento.infPedReg.e101103) {
tipoEvento = TipoEventoNFSe.SOLICITACAO_CANCELAMENTO_ANALISE_FISCAL;
} else if (data.pedRegEvento.infPedReg.e105104) {
tipoEvento = TipoEventoNFSe.CANCELAMENTO_DEFERIDO_ANALISE_FISCAL;
} else if (data.pedRegEvento.infPedReg.e105105) {
tipoEvento = TipoEventoNFSe.CANCELAMENTO_INDEFERIDO_ANALISE_FISCAL;
} else if (data.pedRegEvento.infPedReg.e202201) {
tipoEvento = TipoEventoNFSe.CONFIRMACAO_PRESTADOR;
} else if (data.pedRegEvento.infPedReg.e202205) {
tipoEvento = TipoEventoNFSe.REJEICAO_PRESTADOR;
} else if (data.pedRegEvento.infPedReg.e203202) {
tipoEvento = TipoEventoNFSe.CONFIRMACAO_TOMADOR;
} else if (data.pedRegEvento.infPedReg.e203206) {
tipoEvento = TipoEventoNFSe.REJEICAO_TOMADOR;
}
}
// Salva o XML do evento descompactado
const config = this.environment.getConfig();
if (config.armazenarXMLAutorizacao && data.chaveAcesso) {
const fileName = `${data.chaveAcesso}_evento_${tipoEvento}_${Date.now()}`;
// Cria o path com a subpasta do tipo de evento
const pathComTipoEvento = `${config.pathXMLAutorizacao}/${tipoEvento}`;
this.utility.salvaXML({
data: eventoXml,
fileName: fileName,
metodo: this.metodo,
path: pathComTipoEvento,
});
logger.info('XML do evento salvo com sucesso', {
context: 'NFSeEventosService',
fileName: fileName,
path: pathComTipoEvento,
tipoEvento: tipoEvento
});
}
} catch (error: any) {
logger.warn('Aviso ao processar XML do evento descompactado', {
context: 'NFSeEventosService',
message: error.message,
note: 'O evento foi registrado com sucesso, mas o XML não pôde ser salvo automaticamente'
});
}
}
return response;
}
async ConsultarEvento(data: NFSeEventoConsulta): Promise {
// Salva o método original
const metodoOriginal = this.metodo;
// Muda temporariamente para consulta
this.metodo = 'NFSe_ConsultarEvento';
const response = await super.Exec(data) as NFSeEventoResponse;
// A consulta retorna um array de eventos com arquivoXml (Base64)
if (response.eventos && Array.isArray(response.eventos) && response.eventos.length > 0) {
const config = this.environment.getConfig();
// Processa cada evento retornado
for (const evento of response.eventos) {
if (evento.arquivoXml) {
try {
// O arquivoXml está em Base64 duplo (Base64 dentro de Base64)
// Primeira decodificação: Base64 → string que contém outro Base64
const primeiraDecodificacao = Buffer.from(evento.arquivoXml, 'base64').toString('utf-8');
// Segunda decodificação: Base64 → buffer GZip
const eventoBuffer = Buffer.from(primeiraDecodificacao, 'base64');
// Descompacta o GZip para obter o XML
const eventoXml = gunzipSync(eventoBuffer).toString('utf-8');
// Salva o XML do evento consultado (apenas se estiver configurado)
// Eventos de NFSe autorizada devem ser salvos em Autorizacao/NFSe/{tipoEvento}/
logger.info('Verificando configuração para salvar XML do evento consultado', {
context: 'NFSeEventosService',
armazenarXMLAutorizacao: config.armazenarXMLAutorizacao,
pathXMLAutorizacao: config.pathXMLAutorizacao,
chaveAcesso: evento.chaveAcesso,
eventoXmlLength: eventoXml.length
});
if (config.armazenarXMLAutorizacao && config.pathXMLAutorizacao && evento.chaveAcesso) {
const fileName = `${evento.chaveAcesso}_evento_${evento.tipoEvento}_${evento.numeroPedidoRegistroEvento}`;
// Cria o path com a subpasta do tipo de evento em Autorizacao/NFSe
const pathComTipoEvento = `${config.pathXMLAutorizacao}/${evento.tipoEvento}`;
logger.info('Tentando salvar XML do evento consultado', {
context: 'NFSeEventosService',
fileName: fileName,
path: pathComTipoEvento,
tipoEvento: evento.tipoEvento,
metodo: this.metodo
});
this.utility.salvaXML({
data: eventoXml,
fileName: fileName,
metodo: this.metodo,
path: pathComTipoEvento,
});
logger.info('XML do evento consultado salvo com sucesso', {
context: 'NFSeEventosService',
fileName: fileName,
path: pathComTipoEvento,
tipoEvento: evento.tipoEvento
});
} else {
logger.warn('XML do evento consultado não foi salvo - configuração não permite', {
context: 'NFSeEventosService',
armazenarXMLAutorizacao: config.armazenarXMLAutorizacao,
pathXMLAutorizacao: config.pathXMLAutorizacao,
chaveAcesso: evento.chaveAcesso
});
}
} catch (error: any) {
logger.error('Erro ao processar XML do evento consultado', {
context: 'NFSeEventosService',
message: error.message,
stack: error.stack,
evento: evento
});
}
}
}
} else if (response.eventoXmlGZipB64) {
// Fallback: se tiver eventoXmlGZipB64 (formato de registro)
try {
const eventoBuffer = Buffer.from(response.eventoXmlGZipB64, 'base64');
const eventoXml = gunzipSync(eventoBuffer).toString('utf-8');
const config = this.environment.getConfig();
// Eventos de NFSe autorizada devem ser salvos em Autorizacao/NFSe/{tipoEvento}/
if (config.armazenarXMLAutorizacao && config.pathXMLAutorizacao && data.chaveAcesso) {
const fileName = `${data.chaveAcesso}_evento_${data.tipoEvento}_${data.numSeqEvento}`;
// Cria o path com a subpasta do tipo de evento em Autorizacao/NFSe
const pathComTipoEvento = `${config.pathXMLAutorizacao}/${data.tipoEvento}`;
this.utility.salvaXML({
data: eventoXml,
fileName: fileName,
metodo: this.metodo,
path: pathComTipoEvento,
});
logger.info('XML do evento consultado salvo com sucesso', {
context: 'NFSeEventosService',
fileName: fileName,
path: pathComTipoEvento,
tipoEvento: data.tipoEvento
});
}
} catch (error: any) {
logger.error('Erro ao processar XML do evento consultado', {
context: 'NFSeEventosService',
message: error.message,
stack: error.stack
});
}
} else {
logger.warn('Resposta da consulta não contém eventos ou eventoXmlGZipB64', {
context: 'NFSeEventosService',
response: response
});
}
// Restaura o método original após salvar
this.metodo = metodoOriginal;
return response;
}
}
export default NFSeEventosService;