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; } }