| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220 |
- 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");
- }
|