import { buscarBi } from "./ixcBiService.js"; import { buscarOsAvancado } from "./ixcIntegradorService.js"; import { ASSUNTOS_CANCELAMENTO_RETENCAO } from "./atendimentoIxcContextoService.js"; import { Atendimento } from "../models/Atendimento.model.js"; import { AtendimentoCliente } from "../models/AtendimentoCliente.model.js"; import { formatDateTime } from "../utils/atendimentoFormat.js"; const STATUS_RISCO = ["ameacou", "cancelou"]; const LISTA_LIMIT = 20; const STATUS_IA_LIMIT = 3; const JANELA_DIAS_OS_CANCELAMENTO_CLIENTE = 180; const OS_STATUS_RELEVANTES = ["A", "AN", "EN", "AS", "AG", "DS", "EX", "RAG", "F"]; export async function buscarContratosCancelados({ dataInicio, dataFim }) { const query = {}; if (dataInicio) query.inicio = dataInicio; if (dataFim) query.fim = dataFim; const resultado = await buscarBi("cancelamentos/detalhe", query); return resultado?.contratos ?? []; } // Busca OS de cancelamento/retenção AO VIVO no ERP para um cliente, independente de já constarem // em "cancelamentos/detalhe" — esse endpoint de BI só reflete cancelamento já EFETIVADO/fechado no // contrato; uma OS de cancelamento aberta/em andamento (ex.: aguardando retirada de equipamento) // pode não aparecer lá ainda, mas já é sinal real de que o cliente pediu para cancelar. export async function buscarOsCancelamentoPorNomeCliente(clienteNome) { const termo = String(clienteNome ?? "").trim(); if (!termo) return []; const clientes = await AtendimentoCliente.query() .where("Nome", "like", `%${termo}%`) .whereNotNull("IdClienteIxc") .select("IdClienteIxc") .groupBy("IdClienteIxc"); if (!clientes.length) return []; const fim = new Date().toISOString().slice(0, 10); const inicio = new Date(Date.now() - JANELA_DIAS_OS_CANCELAMENTO_CLIENTE * 86400000).toISOString().slice(0, 10); const listas = await Promise.all( clientes.map(async ({ IdClienteIxc }) => { try { const resultado = await buscarOsAvancado({ id_cliente: IdClienteIxc, assunto: ASSUNTOS_CANCELAMENTO_RETENCAO, status: OS_STATUS_RELEVANTES, inicio, fim, limit: 20 }); return Array.isArray(resultado?.data) ? resultado.data : []; } catch (err) { console.warn("[cancelamentosQueryService] buscarOsCancelamentoPorNomeCliente falhou:", err.message); return []; } }) ); return listas.flat().map((os) => ({ assunto: os.assunto?.assunto ?? null, status: os.status ?? null, dataAbertura: os.data_abertura ?? null, dataFechamento: os.data_fechamento ?? null, notaResolucao: os.mensagem_resposta ?? null })); } export function formatarOsCancelamento(osList) { if (!osList.length) return null; const linhas = osList.map( (os) => `- ${os.assunto ?? "?"}: status ${os.status ?? "?"}, aberta em ${os.dataAbertura ?? "?"}` + (os.dataFechamento ? `, fechada em ${os.dataFechamento}` : ", ainda em aberto") + (os.notaResolucao ? ` — "${os.notaResolucao}"` : "") ); return [ "Ordens de serviço (OS) reais no ERP (IXC) relacionadas a cancelamento/retenção/retirada de " + "equipamento encontradas para este cliente — podem indicar um cancelamento SOLICITADO/EM ANDAMENTO " + "mesmo quando ainda não consta como cancelamento EFETIVADO no registro acima (o contrato só fecha " + "formalmente depois que o processo é concluído, ex.: após a retirada do equipamento):", ...linhas ].join("\n"); } function normalizar(texto) { return String(texto ?? "") .toLowerCase() .normalize("NFD") .replace(/[̀-ͯ]/g, ""); } export function aplicarFiltrosCancelamento(contratos, { clienteNome, motivo, cidade, bairro, vendedor }) { return contratos.filter((c) => { if (clienteNome && !normalizar(c.cliente).includes(normalizar(clienteNome))) return false; if (motivo && !normalizar(c.motivo).includes(normalizar(motivo))) return false; if (cidade && !normalizar(c.cidade).includes(normalizar(cidade))) return false; if (bairro && !normalizar(c.bairro).includes(normalizar(bairro))) return false; if (vendedor && !normalizar(c.vendedor).includes(normalizar(vendedor))) return false; return true; }); } function descreverFiltros(filtros) { const partes = []; if (filtros.clienteNome) partes.push(`cliente "${filtros.clienteNome}"`); if (filtros.motivo) partes.push(`motivo contendo "${filtros.motivo}"`); if (filtros.cidade) partes.push(`cidade "${filtros.cidade}"`); if (filtros.bairro) partes.push(`bairro "${filtros.bairro}"`); if (filtros.vendedor) partes.push(`vendedor "${filtros.vendedor}"`); if (filtros.dataInicio || filtros.dataFim) { partes.push(`período ${filtros.dataInicio ?? "início"} a ${filtros.dataFim ?? "hoje"}`); } return partes.length ? `filtrando por ${partes.join(", ")}` : null; } function formatarValor(v) { return typeof v === "number" ? `R$ ${v.toFixed(2)}` : String(v ?? "não informado"); } function formatarContrato(c) { return [ `Cliente: ${c.cliente}`, `Plano: ${c.plano ?? "não informado"} (${formatarValor(c.valorContrato)}/mês)`, `Ativado em: ${c.ativacao ?? "?"} — Cancelado em: ${c.cancelamento ?? "?"} (${c.mesesContrato ?? "?"} meses de contrato)`, `Motivo do cancelamento: ${c.motivo ?? "não informado"}`, `Vendedor: ${c.vendedor ?? "não informado"} — Local: ${c.bairro ?? "?"}, ${c.cidade ?? "?"}/${c.uf ?? "?"}` ].join("\n"); } export function formatarListaCancelamentos(contratos, filtros) { const descricao = descreverFiltros(filtros); if (!contratos.length) { return descricao ? `Nenhum cancelamento encontrado no ERP (IXC) para esse filtro (${descricao}).` : "Nenhum cancelamento encontrado no ERP (IXC)."; } const amostra = contratos.slice(0, LISTA_LIMIT); const cabecalho = `Cancelamentos reais encontrados no ERP (dados ao vivo do IXC)${descricao ? ` ${descricao}` : ""}: ` + `${contratos.length} contrato${contratos.length === 1 ? "" : "s"}` + (contratos.length > amostra.length ? ` — mostrando os ${amostra.length} primeiros` : "") + "."; return [cabecalho, "", ...amostra.map((c, i) => `--- ${i + 1} ---\n${formatarContrato(c)}`)].join("\n"); } function agrupar(contratos, campo) { const mapa = new Map(); for (const c of contratos) { const chave = c[campo] || "não informado"; mapa.set(chave, (mapa.get(chave) ?? 0) + 1); } return [...mapa.entries()].sort((a, b) => b[1] - a[1]); } function formatarRanking(pares, limit = 10) { return pares .slice(0, limit) .map(([chave, total]) => `${chave}: ${total}`) .join("\n"); } export function formatarResumoCancelamentos(contratos, filtros) { const descricao = descreverFiltros(filtros); if (!contratos.length) { return descricao ? `Nenhum cancelamento encontrado no ERP (IXC) para esse filtro (${descricao}).` : "Nenhum cancelamento encontrado no ERP (IXC)."; } const cabecalho = `Cancelamentos reais no ERP (dados ao vivo do IXC)${descricao ? ` ${descricao}` : ""}: ` + `${contratos.length} contrato${contratos.length === 1 ? "" : "s"}.`; return [ cabecalho, "", "Por motivo:", formatarRanking(agrupar(contratos, "motivo")), "", "Por cidade:", formatarRanking(agrupar(contratos, "cidade")), "", "Por vendedor:", formatarRanking(agrupar(contratos, "vendedor")) ].join("\n"); } export async function buscarStatusCancelamentoIA(clienteNome) { const termo = String(clienteNome ?? "").trim(); if (!termo) return []; return Atendimento.query() .join("atendimento_clientes", "atendimento_clientes.Id", "atendimentos.ClienteId") .join("atendimento_avaliacoes", "atendimento_avaliacoes.AtendimentoId", "atendimentos.Id") .where("atendimento_clientes.Nome", "like", `%${termo}%`) .whereIn("atendimento_avaliacoes.StatusCancelamento", STATUS_RISCO) .select( "atendimentos.Codigo as codigo", "atendimentos.Abertura as abertura", "atendimento_clientes.Nome as clienteNome", "atendimento_avaliacoes.StatusCancelamento as status", "atendimento_avaliacoes.MotivoCancelamento as motivo" ) .orderBy("atendimentos.Abertura", "desc") .limit(STATUS_IA_LIMIT); } const STATUS_LABEL = { ameacou: "ameaçou cancelar", cancelou: "cancelou (segundo a IA)" }; export function formatarStatusIA(linhas) { if (!linhas.length) return null; const cabecalho = "Histórico de risco de cancelamento inferido por IA em atendimentos deste cliente " + "(avaliação automática de conversas de suporte — não é o registro real confirmado pelo ERP):"; const itens = linhas.map((l) => { const data = formatDateTime(l.abertura); const status = STATUS_LABEL[l.status] ?? l.status; return `- Atendimento ${l.codigo}${data ? ` (${data})` : ""}: cliente ${status}${l.motivo ? ` — motivo apontado pela IA: ${l.motivo}` : ""}`; }); return [cabecalho, ...itens].join("\n"); }