import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { AtendimentoAvaliacao } from "../src/models/AtendimentoAvaliacao.model.js"; import { AtendimentoMensagem } from "../src/models/AtendimentoMensagem.model.js"; import { avaliarPendentes, contarPendentes } from "../src/services/atendimentoAvaliacaoService.js"; import { sincronizarTodosProtocolosAbertos } from "../src/services/atendimentosService.js"; import { EXPEDIENTE_POR_SETOR, estaForaExpediente } from "../src/config/expediente.js"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const APLICAR = process.argv.includes("--apply"); const FORCAR_TODOS = process.argv.includes("--forcar-todos"); const SEM_SYNC = process.argv.includes("--sem-sync"); const loteIdx = process.argv.indexOf("--lote"); const LOTE = loteIdx > -1 ? Number(process.argv[loteIdx + 1]) : 20; const setorIdx = process.argv.indexOf("--setor"); const SETOR = setorIdx > -1 ? process.argv[setorIdx + 1] : null; const limiteIdx = process.argv.indexOf("--limite"); const LIMITE = limiteIdx > -1 ? Number(process.argv[limiteIdx + 1]) : null; const expectIdx = process.argv.indexOf("--expect-count"); const EXPECT_COUNT = expectIdx > -1 ? Number(process.argv[expectIdx + 1]) : null; const SETORES_COM_GRADE = Object.keys(EXPEDIENTE_POR_SETOR); async function buscarCandidatos() { const query = AtendimentoAvaliacao.query() .where("Avaliavel", true) .join("atendimentos as a", "a.Id", "atendimento_avaliacoes.AtendimentoId") .select("atendimento_avaliacoes.AtendimentoId", "atendimento_avaliacoes.ScoreAtendente", "a.Setor") .orderBy("atendimento_avaliacoes.AtendimentoId", "asc"); if (SETOR) query.where("a.Setor", SETOR); if (!FORCAR_TODOS) query.whereIn("a.Setor", SETORES_COM_GRADE); return query; } async function temMensagemForaExpediente(atendimentoId, setor) { const mensagensCliente = await AtendimentoMensagem.query() .where("AtendimentoId", atendimentoId) .where("Resposta", 0) .select("Timestamp"); return mensagensCliente.some((m) => estaForaExpediente(setor, m.Timestamp)); } async function filtrarAlvos(candidatos) { if (FORCAR_TODOS) return { alvos: candidatos, semMensagemForaExpediente: 0 }; const alvos = []; let semMensagemForaExpediente = 0; for (const c of candidatos) { if (await temMensagemForaExpediente(c.AtendimentoId, c.Setor)) { alvos.push(c); } else { semMensagemForaExpediente += 1; } } return { alvos, semMensagemForaExpediente }; } async function main() { const totalQuery = AtendimentoAvaliacao.query() .where("Avaliavel", true) .join("atendimentos as a", "a.Id", "atendimento_avaliacoes.AtendimentoId"); if (SETOR) totalQuery.where("a.Setor", SETOR); const totalAvaliavel = await totalQuery.resultSize(); const candidatos = await buscarCandidatos(); const descartadosSetorSemGrade = totalAvaliavel - candidatos.length; console.log(`[reavaliar-expediente] ${totalAvaliavel} avaliações Avaliavel=true${SETOR ? ` no setor ${SETOR}` : ""}.`); if (!FORCAR_TODOS) { console.log(`[reavaliar-expediente] setores com grade de horário: ${SETORES_COM_GRADE.join(", ")}.`); console.log(`[reavaliar-expediente] ${descartadosSetorSemGrade} descartados: setor sem grade de horário.`); } console.log(`[reavaliar-expediente] ${candidatos.length} candidatos após filtro de setor.`); const { alvos: todosAlvos, semMensagemForaExpediente } = await filtrarAlvos(candidatos); if (!FORCAR_TODOS) { console.log(`[reavaliar-expediente] ${semMensagemForaExpediente} descartados: nenhuma mensagem do cliente fora do expediente.`); } const alvos = LIMITE ? todosAlvos.slice(0, LIMITE) : todosAlvos; console.log(`[reavaliar-expediente] ${alvos.length} avaliações serão resetadas para pendente${LIMITE ? ` (--limite ${LIMITE})` : ""}.`); if (EXPECT_COUNT !== null && alvos.length !== EXPECT_COUNT) { console.error(`[reavaliar-expediente] abortando: esperava ${EXPECT_COUNT}, achei ${alvos.length}.`); process.exit(1); } if (!APLICAR) { console.log("[reavaliar-expediente] dry-run (nenhuma alteração feita). Use --apply para executar."); process.exit(0); } if (alvos.length === 0) { console.log("[reavaliar-expediente] nada para reavaliar."); process.exit(0); } const backupDir = path.resolve(__dirname, "logs"); fs.mkdirSync(backupDir, { recursive: true }); const backupPath = path.join(backupDir, `reavaliar-expediente-${new Date().toISOString().replace(/[:.]/g, "-")}.json`); fs.writeFileSync(backupPath, JSON.stringify(alvos, null, 2)); console.log(`[reavaliar-expediente] backup dos scores antigos salvo em ${backupPath}`); if (!SEM_SYNC) { console.log("[reavaliar-expediente] passo 1/3: sincronizando protocolos abertos (sincronizarTodosProtocolosAbertos)..."); try { const sync = await sincronizarTodosProtocolosAbertos(); console.log( `[reavaliar-expediente] sync concluído: ${sync.importados}/${sync.processados} importados, ` + `${sync.mensagens} mensagens, ${sync.erros.length} erros, ${sync.paginas} página(s).` ); } catch (err) { console.error(`[reavaliar-expediente] sync falhou (${err.message}), seguindo mesmo assim.`); } } else { console.log("[reavaliar-expediente] passo 1/3: pulado (--sem-sync)."); } const ids = alvos.map((a) => a.AtendimentoId); const pendentesAntes = await contarPendentes(); await AtendimentoAvaliacao.query().whereIn("AtendimentoId", ids).patch({ UltimaMensagemId: null }); const pendentesDepois = await contarPendentes(); console.log(`[reavaliar-expediente] passo 2/3: ${ids.length} avaliações resetadas — fila de pendentes ${pendentesAntes} -> ${pendentesDepois}.`); console.log(`[reavaliar-expediente] passo 3/3: drenando fila de pendentes (mesmo fluxo de avaliarTudo.js, lotes de ${LOTE})...`); let restantes = pendentesDepois; let totalAvaliados = 0; let totalSemDialogo = 0; const erros = []; const inicio = Date.now(); while (restantes > 0) { const t0 = Date.now(); const r = await avaliarPendentes({ limite: LOTE }); restantes = await contarPendentes(); totalAvaliados += r.avaliados; totalSemDialogo += r.semDialogo; erros.push(...r.erros); console.log( `[reavaliar-expediente] +${r.avaliados} avaliados, ${r.semDialogo} sem diálogo, ${r.erros.length} erros ` + `(${((Date.now() - t0) / 1000).toFixed(0)}s) — restam ${restantes}` ); if (r.erros.length) r.erros.slice(0, 3).forEach((e) => console.log(` erro: ${e.atendimentoId} ${e.erro}`)); if (r.avaliados === 0 && r.semDialogo === 0 && r.erros.length === 0) { console.error("[reavaliar-expediente] lote sem nenhum progresso, abortando para evitar loop infinito."); break; } } console.log( `[reavaliar-expediente] fila drenada em ${(((Date.now() - inicio)) / 1000 / 60).toFixed(1)}min: ` + `${totalAvaliados} avaliados, ${totalSemDialogo} sem diálogo, ${erros.length} erros.` ); const idsSet = new Set(ids); const scoreAntigoPorId = new Map(alvos.map((a) => [a.AtendimentoId, a.ScoreAtendente])); const atuais = await AtendimentoAvaliacao.query().whereIn("AtendimentoId", ids).select("AtendimentoId", "ScoreAtendente"); const mudouScore = atuais.filter((a) => idsSet.has(a.AtendimentoId) && a.ScoreAtendente !== scoreAntigoPorId.get(a.AtendimentoId)).length; console.log(`[reavaliar-expediente] dos ${ids.length} alvos do backfill, ${mudouScore} tiveram o score alterado.`); process.exit(erros.length ? 1 : 0); } main().catch((e) => { console.error("[reavaliar-expediente] falha:", e.message); process.exit(1); });