/*
* 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, BaseNFe, logger } from '@treeunfe/shared';
import { GerarConsultaImpl, NFERecepcaoEventoServiceImpl, SaveFilesImpl } from '@treeunfe/types/interfaces';
import { EventoNFe, GenericObject, TipoEvento } from '@treeunfe/types';
import { AxiosInstance, AxiosResponse } from 'axios';
import { Agent } from 'http';
class NFERecepcaoEventoService extends BaseNFe implements NFERecepcaoEventoServiceImpl {
tpEvento: string;
modelo?: string;
xmlEventosNacionais: string[];
xmlEventosRegionais: string[];
xMotivoPorEvento: any[];
constructor(environment: Environment, utility: Utility, xmlBuilder: XmlBuilder, axios: AxiosInstance, saveFiles: SaveFilesImpl, gerarConsulta: GerarConsultaImpl) {
super(environment, utility, xmlBuilder, 'RecepcaoEvento', axios, saveFiles, gerarConsulta);
this.tpEvento = '';
this.modelo = 'NFe';
this.xmlEventosNacionais = [];
this.xmlEventosRegionais = [];
this.xMotivoPorEvento = [];
console.log('constructor recepcao evento service');
}
/**
* Método para gerar o Id do evento
*/
private getID(evento: TipoEvento) {
const { tpEvento, chNFe, nSeqEvento } = evento;
// Validação do tipo do evento (tpEvento)
if (typeof tpEvento !== 'string' || !/^\d{6}$/.test(tpEvento)) {
throw new Error('tpEvento deve ser uma string com 6 dígitos.');
}
// Validação da chave da NF-e (chNFe)
if (typeof chNFe !== 'string' || !/^\d{44}$/.test(chNFe)) {
throw new Error('chNFe deve ser uma string com 44 dígitos.');
}
// Validação do número sequencial do evento (nSeqEvento)
if (!Number.isInteger(nSeqEvento) || nSeqEvento < 1 || nSeqEvento > 99) {
throw new Error('nSeqEvento deve ser um número entre 1 e 99.');
}
// Preenchendo o número sequencial do evento com zeros à esquerda
const nSeqEventoPadded = nSeqEvento.toString().padStart(2, '0');
// Construção do ID
const id = `ID${tpEvento}${chNFe}${nSeqEventoPadded}`;
// Verificação do comprimento do ID
if (id.length !== 54) {
throw new Error('O ID construído não tem 54 caracteres.');
}
return id; // Retorna o ID validado
}
/**
* Verifica se o evento será disparado para o ambiente nacional ou para o estado pré-definido
*/
private isAmbienteNacional(tpEvento: string) {
switch (tpEvento) {
case '210210':
return true;
case '210200':
return true;
case '210220':
return true;
case '210240':
return true;
case '110140':
return true;
case '110110':
return false;
default:
return false;
}
}
/**
* Retorna o nome do Evento
*/
private getTipoEventoName(tpEvento: string) {
switch (tpEvento) {
case '210210':
return 'Ciência da Operação';
case '210200':
return 'Confirmação da Operaçã';
case '210220':
return 'Desconhecimento da Operação';
case '210240':
return 'Operação não Realizada';
case '110110':
return 'Carta de Correção';
case '110111':
return 'Cancelamento';
case '110140':
return 'EPEC';
default:
return 'Desconhecido';
}
}
private separaEventosPorAmbiente(evento: TipoEvento[]) {
logger.info('Dividindo eventos por ambiente', {
context: 'NFERecepcaoEventoService',
});
const nacional = evento.filter(event => ['210210', '210200', '210220', '210240', '110140'].includes(event.tpEvento));
const regional = evento.filter(event => !['210210', '210200', '210220', '210240', '110140'].includes(event.tpEvento));
return { nacional, regional };
}
/**
* Criação do XML
*/
private gerarXmlRecepcaoEvento(evento: TipoEvento[], idLote: number, ambienteNacional: boolean) {
const { ambiente } = this.environment.getConfig();
for (let i = 0; i < evento.length; i++) {
const eventoProps = evento[i];
const {
cOrgao,
tpEvento,
chNFe,
nSeqEvento,
CNPJ,
CPF,
detEvento,
dhEvento,
verEvento
} = eventoProps;
const idEvento = this.getID(eventoProps);
// const ambienteNacional = this.isAmbienteNacional(tpEvento);
const orgao = ambienteNacional ? 91 : cOrgao;
this.tpEvento = tpEvento;
// XML parte 1
const eventoObject = {
$: {
versao: "1.00",
xmlns: 'http://www.portalfiscal.inf.br/nfe'
},
infEvento: {
$: {
Id: idEvento,
},
cOrgao: orgao,
tpAmb: ambiente,
...(CNPJ ? { CNPJ } : { CPF }),
chNFe: chNFe,
dhEvento: dhEvento,
tpEvento: tpEvento,
nSeqEvento: nSeqEvento,
verEvento: verEvento,
detEvento: {
$: {
versao: "1.00",
},
...detEvento,
},
}
}
// Gera primeira parte do XML
const eventoXML = this.xmlBuilder.gerarXml(eventoObject, 'evento', this.metodo)
const xmlAssinado = this.xmlBuilder.assinarXML(eventoXML, 'infEvento');
if (ambienteNacional) {
this.xmlEventosNacionais.push(xmlAssinado);
} else {
this.xmlEventosRegionais.push(xmlAssinado);
}
}
// XML parte 2
const envEvento = {
$: {
versao: "1.00",
xmlns: 'http://www.portalfiscal.inf.br/nfe'
},
idLote,
_: '[XML]'
}
// Gera Segunda parte do XML
const xml = this.xmlBuilder.gerarXml(envEvento, 'envEvento', this.metodo)
if (ambienteNacional) {
return xml.replace('[XML]', this.xmlEventosNacionais.join(''));
}
return xml.replace('[XML]', this.xmlEventosRegionais.join(''));
}
private trataRetorno(responseInJson: GenericObject) {
logger.info('Tratando retorno dos eventos', {
context: 'NFERecepcaoEventoService',
});
const retornoEventos = this.utility.findInObj(responseInJson, 'retEvento')
if (retornoEventos instanceof Array) {
for (let i = 0; i < retornoEventos.length; i++) {
const chNFe = retornoEventos[i].infEvento.chNFe;
const xMotivo = retornoEventos[i].infEvento.xMotivo;
const cStat = retornoEventos[i].infEvento.cStat;
const tipoEvento = this.getTipoEventoName(retornoEventos[i].infEvento.tpEvento);
this.xMotivoPorEvento.push({
chNFe,
xMotivo,
cStat,
tipoEvento
})
}
return this.xMotivoPorEvento;
}
const chNFe = retornoEventos.infEvento.chNFe;
const xMotivo = retornoEventos.infEvento.xMotivo;
const cStat = retornoEventos.infEvento.cStat;
const tipoEvento = this.getTipoEventoName(retornoEventos.infEvento.tpEvento);
this.xMotivoPorEvento.push({
chNFe,
xMotivo,
cStat,
tipoEvento
})
return this.xMotivoPorEvento;
}
protected async callWebService(xmlConsulta: string, webServiceUrl: string, ContentType: string, action: string, agent: Agent): Promise> {
const startTime = Date.now();
const headers = {
'Content-Type': ContentType,
'SOAPAction': action,
};
logger.http('Iniciando comunicação com o webservice', {
context: `BaseNFe`,
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: `BaseNFe`,
method: this.metodo,
duration: `${duration}ms`,
responseSize: response.data ? JSON.stringify(response.data).length : 0
});
return response;
}
protected async enviaEvento(evento: TipoEvento[], idLote: number, tipoAmbiente: number) {
let xmlConsulta: string = '';
let xmlConsultaSoap: string = '';
// webServiceUrlTmp não é usado, mas mantido para compatibilidade
// let webServiceUrlTmp: string = '';
const ContentType = this.setContentType();
const ambienteNacional = tipoAmbiente === 0 ? true : false;
try {
// Gerando XML para consulta de Status do Serviço
xmlConsulta = this.gerarXmlRecepcaoEvento(evento, idLote, ambienteNacional);
const { xmlFormated, agent, webServiceUrl, action } = await this.gerarConsulta.gerarConsulta(xmlConsulta, this.metodo, ambienteNacional || this.isAmbienteNacional(this.tpEvento), '', this.modelo);
xmlConsultaSoap = xmlFormated;
// webServiceUrlTmp não é usado, mas mantido para compatibilidade
// webServiceUrlTmp = webServiceUrl;
const xmlRetorno = await this.callWebService(xmlFormated, webServiceUrl, ContentType, action, agent);
return xmlRetorno.data
} finally {
// Salva XML de Consulta
const fileName = ambienteNacional ? 'RecepcaoEvento[Nacional]-consulta' : 'RecepcaoEvento[Regional]-consulta'
this.utility.salvaConsulta(xmlConsulta, xmlConsultaSoap, this.metodo, fileName);
}
}
async Exec(data: EventoNFe) {
const { evento, idLote, modelo } = data;
const { nacional, regional } = this.separaEventosPorAmbiente(evento);
if (modelo === '65') this.modelo = 'NFCe';
// Enviar eventos ambiente nacional e regional separadamente
let responseNacionalInJson, responseRegionalInJson = null
let finalResponseInJson = []
if (nacional.length > 0) {
const retornoNacional = await this.enviaEvento(nacional, idLote, 0);
responseNacionalInJson = this.utility.verificaRejeicao(retornoNacional, this.metodo);
this.utility.salvaRetorno(retornoNacional, responseNacionalInJson, this.metodo, 'RecepcaoEvento[Nacional]-retorno');
this.trataRetorno(responseNacionalInJson);
finalResponseInJson.push(responseNacionalInJson)
}
if (regional.length > 0) {
const retornoRegional = await this.enviaEvento(regional, idLote, 1);
responseRegionalInJson = this.utility.verificaRejeicao(retornoRegional, this.metodo);
this.utility.salvaRetorno(retornoRegional, responseRegionalInJson, this.metodo, 'RecepcaoEvento[Regional]-retorno');
this.trataRetorno(responseRegionalInJson);
finalResponseInJson.push(responseRegionalInJson)
}
return {
success: true,
xMotivos: this.xMotivoPorEvento,
response: finalResponseInJson,
};
}
}
export default NFERecepcaoEventoService;