atendimentoIxcContextoService.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
  1. import { buscarAtendimentoOsPorId, buscarContratosCliente, buscarOsAvancado } from "./ixcIntegradorService.js";
  2. const JANELA_DIAS_ANTES_OS_TECNICA = 3;
  3. const JANELA_DIAS_DEPOIS_OS_TECNICA = 10;
  4. function dentroDaJanela(dataOs, dataAtendimento) {
  5. if (!dataOs || !dataAtendimento) return false;
  6. const os = new Date(dataOs).getTime();
  7. const atendimento = new Date(dataAtendimento).getTime();
  8. if (Number.isNaN(os) || Number.isNaN(atendimento)) return false;
  9. const diffDias = (os - atendimento) / 86400000;
  10. return diffDias >= -JANELA_DIAS_ANTES_OS_TECNICA && diffDias <= JANELA_DIAS_DEPOIS_OS_TECNICA;
  11. }
  12. export async function buscarContextoIxc(atendimento) {
  13. const cliente = atendimento?.cliente;
  14. if (!cliente?.ErpProtocolo) return { status: "sem_erp_protocolo", contexto: null, erro: null };
  15. let tickets;
  16. try {
  17. tickets = await buscarAtendimentoOsPorId(cliente.ErpProtocolo);
  18. } catch (err) {
  19. console.warn("[atendimentoIxcContextoService] buscarAtendimentoOsPorId falhou:", err.message);
  20. return { status: "erro", contexto: null, erro: err.message };
  21. }
  22. const ticket = Array.isArray(tickets) ? tickets[0] : null;
  23. if (!ticket) return { status: "sem_ticket", contexto: null, erro: null };
  24. const osList = (ticket.os ?? [])
  25. .filter((os) => dentroDaJanela(os.data_abertura, atendimento?.Abertura))
  26. .map((os) => ({
  27. status: os.status ?? null,
  28. assunto: os.assunto?.assunto ?? null,
  29. tecnico: os.id_atendente ?? null,
  30. dataAbertura: os.data_abertura ?? null,
  31. dataFechamento: os.data_fechamento ?? null,
  32. mensagemResposta: os.mensagem_resposta ?? null
  33. }));
  34. return {
  35. status: "ok",
  36. erro: null,
  37. contexto: {
  38. clienteNome: ticket.cliente?.razao ?? null,
  39. ticketStatus: ticket.status ?? null,
  40. osList
  41. }
  42. };
  43. }
  44. // IDs de assunto do catálogo os/assuntos do IXC relacionados a cancelamento, reversão de
  45. // cancelamento e retenção/mudança de plano — servem para checar se um pedido de cancelamento
  46. // ouvido na conversa foi de fato efetivado no ERP ou se o cliente foi retido (ex.: fez upgrade).
  47. // 59/73 (reagendamento de retirada, sem qualificador) foram incluídos porque na prática são usados
  48. // como continuação de uma OS de retirada por cancelamento que precisou ser remarcada (ex.: "não
  49. // tinha ninguém no local") — o texto livre da OS confirma isso, mas o assunto sozinho perde a
  50. // palavra "cancelamento"; podem gerar falso positivo raro com retirada por outro motivo (troca de
  51. // equipamento, downgrade), por isso são tratados como indício, nunca como confirmação.
  52. export const ASSUNTOS_CANCELAMENTO_RETENCAO = [
  53. 90, 237, 22, 31, 103, 62, 179, 108, 144, 209, 193, 115, 159, 154, // cancelamento / alerta / pós-retenção
  54. 216, 224, 35, 102, // reversão de cancelamento
  55. 217, 99, 12, // retirada de equipamento por cancelamento
  56. 59, 73, // reagendamento de retirada (continuação de cancelamento, sem qualificador no assunto)
  57. 54, 155, 36, 10, 222, 97, 58, 181 // upgrade / alteração de plano
  58. ];
  59. // OS comerciais só valem a pena buscar até esse número de dias após o atendimento — depois
  60. // disso, qualquer cancelamento real já teria virado OS, e ampliar a janela só aumenta o risco
  61. // de pegar um cancelamento não relacionado a este atendimento específico.
  62. const JANELA_DIAS_POS_ATENDIMENTO = 60;
  63. const REGEX_MENCAO_CANCELAMENTO = /cancelar|cancelamento|encerrar\s+(o\s+)?(contrato|plano|servi[cç]o)|desist(ir|iu)/i;
  64. // checagem barata pra decidir se vale a pena buscar contrato/OS comercial no ERP — evita 2
  65. // chamadas extras ao Alfred em toda avaliação quando a conversa não tem nada a ver com cancelamento
  66. export function conversaMencionaCancelamento(texto) {
  67. return REGEX_MENCAO_CANCELAMENTO.test(texto ?? "");
  68. }
  69. function toDateOnly(value) {
  70. return new Date(value).toISOString().slice(0, 10);
  71. }
  72. // contexto comercial (contrato + OS de cancelamento/retenção) usado para validar se um
  73. // cancelamento mencionado na conversa foi de fato efetivado no ERP; melhor-esforço — falha
  74. // silenciosamente (retorna null) sem derrubar a avaliação, igual ao contexto técnico acima
  75. export async function buscarContextoCancelamentoIxc(cliente, dataAtendimento) {
  76. if (!cliente?.IdClienteIxc) return null;
  77. const inicio = toDateOnly(dataAtendimento);
  78. const limiteJanela = new Date(dataAtendimento).getTime() + JANELA_DIAS_POS_ATENDIMENTO * 86400000;
  79. const fim = toDateOnly(Math.min(Date.now(), limiteJanela));
  80. try {
  81. const [contratos, osResultado] = await Promise.all([
  82. buscarContratosCliente(cliente.IdClienteIxc),
  83. buscarOsAvancado({
  84. id_cliente: cliente.IdClienteIxc,
  85. assunto: ASSUNTOS_CANCELAMENTO_RETENCAO,
  86. status: ["A", "AN", "EN", "AS", "AG", "DS", "EX", "RAG", "F"],
  87. inicio,
  88. fim,
  89. limit: 20
  90. })
  91. ]);
  92. const contratosList = Array.isArray(contratos) ? contratos : [];
  93. const osComerciais = Array.isArray(osResultado?.data)
  94. ? osResultado.data.map((os) => ({
  95. assunto: os.assunto?.assunto ?? null,
  96. status: os.status ?? null,
  97. dataAbertura: os.data_abertura ?? null,
  98. notaResolucao: os.mensagem_resposta ?? null
  99. }))
  100. : [];
  101. return {
  102. contratoAtivo: contratosList.some((c) => c.status === "A"),
  103. contratos: contratosList.map((c) => ({
  104. contrato: c.contrato ?? null,
  105. status: c.status ?? null,
  106. dataAtivacao: c.data_ativacao ?? null
  107. })),
  108. osComerciais
  109. };
  110. } catch (err) {
  111. console.warn("[atendimentoIxcContextoService] buscarContextoCancelamentoIxc falhou:", err.message);
  112. return null;
  113. }
  114. }