Переглянути джерело

fix: corrige falha de ausencia de dados em avaliacoes

leonardo 1 місяць тому
батько
коміт
5990068fd4

+ 15 - 0
db/migrations/20260806120000_add_contexto_ixc_status_to_atendimento_avaliacoes.cjs

@@ -0,0 +1,15 @@
+exports.up = function (knex) {
+  return knex.schema.alterTable("atendimento_avaliacoes", (t) => {
+    // ok | sem_erp_protocolo | sem_ticket | erro | nao_verificado (avaliação sem diálogo, não chegou a buscar)
+    // null = avaliação antiga, anterior a este campo existir
+    t.string("ContextoIxcStatus", 20).nullable();
+    t.index(["ContextoIxcStatus"]);
+  });
+};
+
+exports.down = function (knex) {
+  return knex.schema.alterTable("atendimento_avaliacoes", (t) => {
+    t.dropIndex(["ContextoIxcStatus"]);
+    t.dropColumn("ContextoIxcStatus");
+  });
+};

+ 74 - 0
scripts/backfillErpProtocoloSup.js

@@ -0,0 +1,74 @@
+import "dotenv/config";
+import { config } from "../src/config/index.js";
+import { Atendimento } from "../src/models/Atendimento.model.js";
+import { importarProtocolo } from "../src/services/atendimentosService.js";
+
+const limiteIdx = process.argv.indexOf("--limite");
+const LIMITE = limiteIdx > -1 ? Number(process.argv[limiteIdx + 1]) : null;
+const CONCORRENCIA = 4;
+
+async function main() {
+  let query = Atendimento.query()
+    .where("Setor", "SUP")
+    .withGraphFetched("cliente")
+    .orderBy("Abertura", "desc");
+  if (LIMITE) query = query.limit(LIMITE);
+
+  const atendimentos = await query;
+  const total = atendimentos.length;
+  console.log(`[backfill-erp-sup] iniciando: ${total} atendimentos SUP`);
+
+  const baseUrl = config.ifbot.baseUrl.replace(/\/+$/, "");
+  let processados = 0;
+  let comVinculo = 0;
+  let erros = 0;
+  const t0 = Date.now();
+
+  async function processar(a) {
+    try {
+      const item = {
+        Id: a.Id,
+        Codigo: a.Codigo,
+        Abertura: a.Abertura,
+        Cliente: { Id: a.cliente?.Id, Nome: a.cliente?.Nome, Telefone: a.cliente?.Telefone }
+      };
+      await importarProtocolo(item, baseUrl);
+      const atualizado = await Atendimento.query()
+        .findById(a.Id)
+        .withGraphFetched("cliente(soVinculo)")
+        .modifiers({ soVinculo: (q) => q.select("ErpVinculoConfirmadoEm") });
+      if (atualizado?.cliente?.ErpVinculoConfirmadoEm) comVinculo += 1;
+    } catch (err) {
+      erros += 1;
+      console.warn(`[backfill-erp-sup] erro em ${a.Codigo}: ${err.message}`);
+    } finally {
+      processados += 1;
+      if (processados % 50 === 0 || processados === total) {
+        const decorridoS = Math.round((Date.now() - t0) / 1000);
+        console.log(
+          `[backfill-erp-sup] progresso: ${processados}/${total} | com vínculo: ${comVinculo} | erros: ${erros} | ${decorridoS}s decorridos`
+        );
+      }
+    }
+  }
+
+  let idx = 0;
+  async function worker() {
+    while (idx < atendimentos.length) {
+      const a = atendimentos[idx++];
+      await processar(a);
+    }
+  }
+  await Promise.all(Array.from({ length: CONCORRENCIA }, worker));
+
+  const totalS = Math.round((Date.now() - t0) / 1000);
+  console.log(
+    `[backfill-erp-sup] concluído: ${processados}/${total} | com vínculo: ${comVinculo} | erros: ${erros} | ${totalS}s total`
+  );
+  process.exit(0);
+}
+
+main().catch((err) => {
+  console.error("[backfill-erp-sup] falha fatal:", err);
+  process.exit(1);
+});

+ 53 - 0
scripts/reavaliarComContextoIxc.js

@@ -0,0 +1,53 @@
+import "dotenv/config";
+import { Atendimento } from "../src/models/Atendimento.model.js";
+import { avaliarAtendimento } from "../src/services/atendimentoAvaliacaoService.js";
+
+const limiteIdx = process.argv.indexOf("--limite");
+const LIMITE = limiteIdx > -1 ? Number(process.argv[limiteIdx + 1]) : null;
+
+async function main() {
+  let query = Atendimento.query()
+    .join("atendimento_avaliacoes as av", "av.AtendimentoId", "atendimentos.Id")
+    .join("atendimento_clientes as c", "c.Id", "atendimentos.ClienteId")
+    .where("atendimentos.Setor", "SUP")
+    .where("av.Avaliavel", true)
+    .whereNotNull("c.ErpProtocolo")
+    .select("atendimentos.Id", "atendimentos.Codigo")
+    .orderBy("atendimentos.Abertura", "desc");
+  if (LIMITE) query = query.limit(LIMITE);
+
+  const alvos = await query;
+  const total = alvos.length;
+  console.log(`[reavaliar-ixc] iniciando: ${total} atendimentos`);
+
+  let processados = 0;
+  let erros = 0;
+  const t0 = Date.now();
+
+  for (const a of alvos) {
+    try {
+      await avaliarAtendimento(a.Id);
+    } catch (err) {
+      erros += 1;
+      console.warn(`[reavaliar-ixc] erro em ${a.Codigo}: ${err.message}`);
+    } finally {
+      processados += 1;
+      if (processados % 25 === 0 || processados === total) {
+        const s = Math.round((Date.now() - t0) / 1000);
+        const ritmo = s / processados;
+        const restamMin = Math.round(((total - processados) * ritmo) / 60);
+        console.log(
+          `[reavaliar-ixc] progresso: ${processados}/${total} | erros: ${erros} | ${Math.round(s / 60)}min decorridos | ~${restamMin}min restantes`
+        );
+      }
+    }
+  }
+
+  console.log(`[reavaliar-ixc] concluído: ${processados}/${total} | erros: ${erros} | ${Math.round((Date.now() - t0) / 60000)}min total`);
+  process.exit(0);
+}
+
+main().catch((err) => {
+  console.error("[reavaliar-ixc] falha fatal:", err);
+  process.exit(1);
+});

+ 3 - 1
src/services/atendimentoAvaliacaoService.js

@@ -306,6 +306,7 @@ export async function avaliarAtendimento(atendimentoId) {
       StatusCancelamento: "nao",
       MotivoCancelamento: null,
       Modelo: null,
+      ContextoIxcStatus: "nao_verificado",
       UltimaMensagemId: ultimaMensagemId,
       TotalMensagens: mensagens.length
     });
@@ -318,7 +319,7 @@ export async function avaliarAtendimento(atendimentoId) {
     mensagens
   });
 
-  const contextoIxc = await buscarContextoIxc(atendimento);
+  const { status: contextoIxcStatus, contexto: contextoIxc } = await buscarContextoIxc(atendimento);
   const conteudoUsuario = contextoIxc
     ? `${formatarContextoIxc(contextoIxc)}\n\n${truncateConversa(conversaTexto)}`
     : truncateConversa(conversaTexto);
@@ -347,6 +348,7 @@ export async function avaliarAtendimento(atendimentoId) {
     Avaliavel: true,
     ...dados,
     Modelo: model,
+    ContextoIxcStatus: contextoIxcStatus,
     UltimaMensagemId: ultimaMensagemId,
     TotalMensagens: mensagens.length
   });

+ 12 - 7
src/services/atendimentoIxcContextoService.js

@@ -1,21 +1,22 @@
 import { buscarAtendimentoOsPorId } from "./ixcIntegradorService.js";
 
-
+// status: "ok" | "sem_erp_protocolo" | "sem_ticket" | "erro"
+// contexto só é preenchido quando status === "ok"; erro só quando status === "erro"
 export async function buscarContextoIxc(atendimento) {
   const cliente = atendimento?.cliente;
-  if (!cliente?.ErpProtocolo) return null;
+  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 null;
+    return { status: "erro", contexto: null, erro: err.message };
   }
 
 
   const ticket = Array.isArray(tickets) ? tickets[0] : null;
-  if (!ticket) return null;
+  if (!ticket) return { status: "sem_ticket", contexto: null, erro: null };
 
   const osList = (ticket.os ?? []).map((os) => ({
     status: os.status ?? null,
@@ -27,8 +28,12 @@ export async function buscarContextoIxc(atendimento) {
   }));
 
   return {
-    clienteNome: ticket.cliente?.razao ?? null,
-    ticketStatus: ticket.status ?? null,
-    osList
+    status: "ok",
+    erro: null,
+    contexto: {
+      clienteNome: ticket.cliente?.razao ?? null,
+      ticketStatus: ticket.status ?? null,
+      osList
+    }
   };
 }