| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134 |
- import { buscarAtendimentoOsPorId, buscarContratosCliente, buscarOsAvancado } from "./ixcIntegradorService.js";
- const JANELA_DIAS_ANTES_OS_TECNICA = 3;
- const JANELA_DIAS_DEPOIS_OS_TECNICA = 10;
- function dentroDaJanela(dataOs, dataAtendimento) {
- if (!dataOs || !dataAtendimento) return false;
- const os = new Date(dataOs).getTime();
- const atendimento = new Date(dataAtendimento).getTime();
- if (Number.isNaN(os) || Number.isNaN(atendimento)) return false;
- const diffDias = (os - atendimento) / 86400000;
- return diffDias >= -JANELA_DIAS_ANTES_OS_TECNICA && diffDias <= JANELA_DIAS_DEPOIS_OS_TECNICA;
- }
- export async function buscarContextoIxc(atendimento) {
- const cliente = atendimento?.cliente;
- if (!cliente?.ErpProtocolo) return { status: "sem_erp_protocolo", contexto: null, erro: null };
- let tickets;
- try {
- tickets = await buscarAtendimentoOsPorId(cliente.ErpProtocolo);
- } catch (err) {
- console.warn("[atendimentoIxcContextoService] buscarAtendimentoOsPorId falhou:", err.message);
- return { status: "erro", contexto: null, erro: err.message };
- }
- const ticket = Array.isArray(tickets) ? tickets[0] : null;
- if (!ticket) return { status: "sem_ticket", contexto: null, erro: null };
- const osList = (ticket.os ?? [])
- .filter((os) => dentroDaJanela(os.data_abertura, atendimento?.Abertura))
- .map((os) => ({
- status: os.status ?? null,
- assunto: os.assunto?.assunto ?? null,
- tecnico: os.id_atendente ?? null,
- dataAbertura: os.data_abertura ?? null,
- dataFechamento: os.data_fechamento ?? null,
- mensagemResposta: os.mensagem_resposta ?? null
- }));
- return {
- status: "ok",
- erro: null,
- contexto: {
- clienteNome: ticket.cliente?.razao ?? null,
- ticketStatus: ticket.status ?? null,
- osList
- }
- };
- }
- // IDs de assunto do catálogo os/assuntos do IXC relacionados a cancelamento, reversão de
- // cancelamento e retenção/mudança de plano — servem para checar se um pedido de cancelamento
- // ouvido na conversa foi de fato efetivado no ERP ou se o cliente foi retido (ex.: fez upgrade).
- // 59/73 (reagendamento de retirada, sem qualificador) foram incluídos porque na prática são usados
- // como continuação de uma OS de retirada por cancelamento que precisou ser remarcada (ex.: "não
- // tinha ninguém no local") — o texto livre da OS confirma isso, mas o assunto sozinho perde a
- // palavra "cancelamento"; podem gerar falso positivo raro com retirada por outro motivo (troca de
- // equipamento, downgrade), por isso são tratados como indício, nunca como confirmação.
- export const ASSUNTOS_CANCELAMENTO_RETENCAO = [
- 90, 237, 22, 31, 103, 62, 179, 108, 144, 209, 193, 115, 159, 154, // cancelamento / alerta / pós-retenção
- 216, 224, 35, 102, // reversão de cancelamento
- 217, 99, 12, // retirada de equipamento por cancelamento
- 59, 73, // reagendamento de retirada (continuação de cancelamento, sem qualificador no assunto)
- 54, 155, 36, 10, 222, 97, 58, 181 // upgrade / alteração de plano
- ];
- // OS comerciais só valem a pena buscar até esse número de dias após o atendimento — depois
- // disso, qualquer cancelamento real já teria virado OS, e ampliar a janela só aumenta o risco
- // de pegar um cancelamento não relacionado a este atendimento específico.
- const JANELA_DIAS_POS_ATENDIMENTO = 60;
- const REGEX_MENCAO_CANCELAMENTO = /cancelar|cancelamento|encerrar\s+(o\s+)?(contrato|plano|servi[cç]o)|desist(ir|iu)/i;
- // checagem barata pra decidir se vale a pena buscar contrato/OS comercial no ERP — evita 2
- // chamadas extras ao Alfred em toda avaliação quando a conversa não tem nada a ver com cancelamento
- export function conversaMencionaCancelamento(texto) {
- return REGEX_MENCAO_CANCELAMENTO.test(texto ?? "");
- }
- function toDateOnly(value) {
- return new Date(value).toISOString().slice(0, 10);
- }
- // contexto comercial (contrato + OS de cancelamento/retenção) usado para validar se um
- // cancelamento mencionado na conversa foi de fato efetivado no ERP; melhor-esforço — falha
- // silenciosamente (retorna null) sem derrubar a avaliação, igual ao contexto técnico acima
- export async function buscarContextoCancelamentoIxc(cliente, dataAtendimento) {
- if (!cliente?.IdClienteIxc) return null;
- const inicio = toDateOnly(dataAtendimento);
- const limiteJanela = new Date(dataAtendimento).getTime() + JANELA_DIAS_POS_ATENDIMENTO * 86400000;
- const fim = toDateOnly(Math.min(Date.now(), limiteJanela));
- try {
- const [contratos, osResultado] = await Promise.all([
- buscarContratosCliente(cliente.IdClienteIxc),
- buscarOsAvancado({
- id_cliente: cliente.IdClienteIxc,
- assunto: ASSUNTOS_CANCELAMENTO_RETENCAO,
- status: ["A", "AN", "EN", "AS", "AG", "DS", "EX", "RAG", "F"],
- inicio,
- fim,
- limit: 20
- })
- ]);
- const contratosList = Array.isArray(contratos) ? contratos : [];
- const osComerciais = Array.isArray(osResultado?.data)
- ? osResultado.data.map((os) => ({
- assunto: os.assunto?.assunto ?? null,
- status: os.status ?? null,
- dataAbertura: os.data_abertura ?? null,
- notaResolucao: os.mensagem_resposta ?? null
- }))
- : [];
- return {
- contratoAtivo: contratosList.some((c) => c.status === "A"),
- contratos: contratosList.map((c) => ({
- contrato: c.contrato ?? null,
- status: c.status ?? null,
- dataAtivacao: c.data_ativacao ?? null
- })),
- osComerciais
- };
- } catch (err) {
- console.warn("[atendimentoIxcContextoService] buscarContextoCancelamentoIxc falhou:", err.message);
- return null;
- }
- }
|