reavaliarComExpediente.js 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import fs from "node:fs";
  2. import path from "node:path";
  3. import { fileURLToPath } from "node:url";
  4. import { AtendimentoAvaliacao } from "../src/models/AtendimentoAvaliacao.model.js";
  5. import { AtendimentoMensagem } from "../src/models/AtendimentoMensagem.model.js";
  6. import { avaliarPendentes, contarPendentes } from "../src/services/atendimentoAvaliacaoService.js";
  7. import { sincronizarTodosProtocolosAbertos } from "../src/services/atendimentosService.js";
  8. import { EXPEDIENTE_POR_SETOR, estaForaExpediente } from "../src/config/expediente.js";
  9. const __dirname = path.dirname(fileURLToPath(import.meta.url));
  10. const APLICAR = process.argv.includes("--apply");
  11. const FORCAR_TODOS = process.argv.includes("--forcar-todos");
  12. const SEM_SYNC = process.argv.includes("--sem-sync");
  13. const loteIdx = process.argv.indexOf("--lote");
  14. const LOTE = loteIdx > -1 ? Number(process.argv[loteIdx + 1]) : 20;
  15. const setorIdx = process.argv.indexOf("--setor");
  16. const SETOR = setorIdx > -1 ? process.argv[setorIdx + 1] : null;
  17. const limiteIdx = process.argv.indexOf("--limite");
  18. const LIMITE = limiteIdx > -1 ? Number(process.argv[limiteIdx + 1]) : null;
  19. const expectIdx = process.argv.indexOf("--expect-count");
  20. const EXPECT_COUNT = expectIdx > -1 ? Number(process.argv[expectIdx + 1]) : null;
  21. const SETORES_COM_GRADE = Object.keys(EXPEDIENTE_POR_SETOR);
  22. async function buscarCandidatos() {
  23. const query = AtendimentoAvaliacao.query()
  24. .where("Avaliavel", true)
  25. .join("atendimentos as a", "a.Id", "atendimento_avaliacoes.AtendimentoId")
  26. .select("atendimento_avaliacoes.AtendimentoId", "atendimento_avaliacoes.ScoreAtendente", "a.Setor")
  27. .orderBy("atendimento_avaliacoes.AtendimentoId", "asc");
  28. if (SETOR) query.where("a.Setor", SETOR);
  29. if (!FORCAR_TODOS) query.whereIn("a.Setor", SETORES_COM_GRADE);
  30. return query;
  31. }
  32. async function temMensagemForaExpediente(atendimentoId, setor) {
  33. const mensagensCliente = await AtendimentoMensagem.query()
  34. .where("AtendimentoId", atendimentoId)
  35. .where("Resposta", 0)
  36. .select("Timestamp");
  37. return mensagensCliente.some((m) => estaForaExpediente(setor, m.Timestamp));
  38. }
  39. async function filtrarAlvos(candidatos) {
  40. if (FORCAR_TODOS) return { alvos: candidatos, semMensagemForaExpediente: 0 };
  41. const alvos = [];
  42. let semMensagemForaExpediente = 0;
  43. for (const c of candidatos) {
  44. if (await temMensagemForaExpediente(c.AtendimentoId, c.Setor)) {
  45. alvos.push(c);
  46. } else {
  47. semMensagemForaExpediente += 1;
  48. }
  49. }
  50. return { alvos, semMensagemForaExpediente };
  51. }
  52. async function main() {
  53. const totalQuery = AtendimentoAvaliacao.query()
  54. .where("Avaliavel", true)
  55. .join("atendimentos as a", "a.Id", "atendimento_avaliacoes.AtendimentoId");
  56. if (SETOR) totalQuery.where("a.Setor", SETOR);
  57. const totalAvaliavel = await totalQuery.resultSize();
  58. const candidatos = await buscarCandidatos();
  59. const descartadosSetorSemGrade = totalAvaliavel - candidatos.length;
  60. console.log(`[reavaliar-expediente] ${totalAvaliavel} avaliações Avaliavel=true${SETOR ? ` no setor ${SETOR}` : ""}.`);
  61. if (!FORCAR_TODOS) {
  62. console.log(`[reavaliar-expediente] setores com grade de horário: ${SETORES_COM_GRADE.join(", ")}.`);
  63. console.log(`[reavaliar-expediente] ${descartadosSetorSemGrade} descartados: setor sem grade de horário.`);
  64. }
  65. console.log(`[reavaliar-expediente] ${candidatos.length} candidatos após filtro de setor.`);
  66. const { alvos: todosAlvos, semMensagemForaExpediente } = await filtrarAlvos(candidatos);
  67. if (!FORCAR_TODOS) {
  68. console.log(`[reavaliar-expediente] ${semMensagemForaExpediente} descartados: nenhuma mensagem do cliente fora do expediente.`);
  69. }
  70. const alvos = LIMITE ? todosAlvos.slice(0, LIMITE) : todosAlvos;
  71. console.log(`[reavaliar-expediente] ${alvos.length} avaliações serão resetadas para pendente${LIMITE ? ` (--limite ${LIMITE})` : ""}.`);
  72. if (EXPECT_COUNT !== null && alvos.length !== EXPECT_COUNT) {
  73. console.error(`[reavaliar-expediente] abortando: esperava ${EXPECT_COUNT}, achei ${alvos.length}.`);
  74. process.exit(1);
  75. }
  76. if (!APLICAR) {
  77. console.log("[reavaliar-expediente] dry-run (nenhuma alteração feita). Use --apply para executar.");
  78. process.exit(0);
  79. }
  80. if (alvos.length === 0) {
  81. console.log("[reavaliar-expediente] nada para reavaliar.");
  82. process.exit(0);
  83. }
  84. const backupDir = path.resolve(__dirname, "logs");
  85. fs.mkdirSync(backupDir, { recursive: true });
  86. const backupPath = path.join(backupDir, `reavaliar-expediente-${new Date().toISOString().replace(/[:.]/g, "-")}.json`);
  87. fs.writeFileSync(backupPath, JSON.stringify(alvos, null, 2));
  88. console.log(`[reavaliar-expediente] backup dos scores antigos salvo em ${backupPath}`);
  89. if (!SEM_SYNC) {
  90. console.log("[reavaliar-expediente] passo 1/3: sincronizando protocolos abertos (sincronizarTodosProtocolosAbertos)...");
  91. try {
  92. const sync = await sincronizarTodosProtocolosAbertos();
  93. console.log(
  94. `[reavaliar-expediente] sync concluído: ${sync.importados}/${sync.processados} importados, ` +
  95. `${sync.mensagens} mensagens, ${sync.erros.length} erros, ${sync.paginas} página(s).`
  96. );
  97. } catch (err) {
  98. console.error(`[reavaliar-expediente] sync falhou (${err.message}), seguindo mesmo assim.`);
  99. }
  100. } else {
  101. console.log("[reavaliar-expediente] passo 1/3: pulado (--sem-sync).");
  102. }
  103. const ids = alvos.map((a) => a.AtendimentoId);
  104. const pendentesAntes = await contarPendentes();
  105. await AtendimentoAvaliacao.query().whereIn("AtendimentoId", ids).patch({ UltimaMensagemId: null });
  106. const pendentesDepois = await contarPendentes();
  107. console.log(`[reavaliar-expediente] passo 2/3: ${ids.length} avaliações resetadas — fila de pendentes ${pendentesAntes} -> ${pendentesDepois}.`);
  108. console.log(`[reavaliar-expediente] passo 3/3: drenando fila de pendentes (mesmo fluxo de avaliarTudo.js, lotes de ${LOTE})...`);
  109. let restantes = pendentesDepois;
  110. let totalAvaliados = 0;
  111. let totalSemDialogo = 0;
  112. const erros = [];
  113. const inicio = Date.now();
  114. while (restantes > 0) {
  115. const t0 = Date.now();
  116. const r = await avaliarPendentes({ limite: LOTE });
  117. restantes = await contarPendentes();
  118. totalAvaliados += r.avaliados;
  119. totalSemDialogo += r.semDialogo;
  120. erros.push(...r.erros);
  121. console.log(
  122. `[reavaliar-expediente] +${r.avaliados} avaliados, ${r.semDialogo} sem diálogo, ${r.erros.length} erros ` +
  123. `(${((Date.now() - t0) / 1000).toFixed(0)}s) — restam ${restantes}`
  124. );
  125. if (r.erros.length) r.erros.slice(0, 3).forEach((e) => console.log(` erro: ${e.atendimentoId} ${e.erro}`));
  126. if (r.avaliados === 0 && r.semDialogo === 0 && r.erros.length === 0) {
  127. console.error("[reavaliar-expediente] lote sem nenhum progresso, abortando para evitar loop infinito.");
  128. break;
  129. }
  130. }
  131. console.log(
  132. `[reavaliar-expediente] fila drenada em ${(((Date.now() - inicio)) / 1000 / 60).toFixed(1)}min: ` +
  133. `${totalAvaliados} avaliados, ${totalSemDialogo} sem diálogo, ${erros.length} erros.`
  134. );
  135. const idsSet = new Set(ids);
  136. const scoreAntigoPorId = new Map(alvos.map((a) => [a.AtendimentoId, a.ScoreAtendente]));
  137. const atuais = await AtendimentoAvaliacao.query().whereIn("AtendimentoId", ids).select("AtendimentoId", "ScoreAtendente");
  138. const mudouScore = atuais.filter((a) => idsSet.has(a.AtendimentoId) && a.ScoreAtendente !== scoreAntigoPorId.get(a.AtendimentoId)).length;
  139. console.log(`[reavaliar-expediente] dos ${ids.length} alvos do backfill, ${mudouScore} tiveram o score alterado.`);
  140. process.exit(erros.length ? 1 : 0);
  141. }
  142. main().catch((e) => {
  143. console.error("[reavaliar-expediente] falha:", e.message);
  144. process.exit(1);
  145. });