Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> git-subtree-dir: gti-maquina-comercial git-subtree-mainline:93ca82979egit-subtree-split:b77484946f
202 lines
8.0 KiB
JavaScript
202 lines
8.0 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { openDb, createLeadInserter, setSetting } from '../src/db.mjs';
|
|
import { createCampaign, setCampaignStatus, addOptOut } from '../src/outreach/campaign-store.mjs';
|
|
import { generateCampaignQueue, listQueue, approveQueueRows } from '../src/outreach/campaign-queue.mjs';
|
|
import { runCampaignQueue } from '../src/outreach/campaign-worker.mjs';
|
|
|
|
// Testes de banco em memoria nao seedam settings.outreach.* (fica no fallback
|
|
// 'false'), entao por padrao o worker nao faria nada — precisa ligar
|
|
// explicitamente e abrir a janela de horario pro dia todo pra nao depender do
|
|
// horario real em que o teste roda.
|
|
function ativarWorker(db) {
|
|
setSetting(db, 'outreach.ativo', 'true');
|
|
setSetting(db, 'outreach.horaInicio', '00:00');
|
|
setSetting(db, 'outreach.horaFim', '23:59');
|
|
}
|
|
|
|
function inserirLead(db, dedup, whatsapp) {
|
|
const inserir = createLeadInserter(db);
|
|
inserir({
|
|
dedup_key: dedup, empresa: 'Empresa', legal_name: '', whatsapp, ddd: '11', email: '',
|
|
cidade: '', uf: '', segmento: 'x', segmento_label: '', interesse: '', cnpj: '', cnae: '',
|
|
endereco: '', website: '', pains_json: '[]', origem: 't', telefone_valido: 1, cnpj_valido: 0,
|
|
empresa_valida: 1, score: 50, classificacao: 'bom', motivo_classificacao: '', score_qualidade: 50,
|
|
score_comercial: 50, prioridade: 1, produto_principal: 'ERP', produtos_json: '[]',
|
|
capture_run_id: null, captured_at: null, exported_at: null
|
|
});
|
|
}
|
|
|
|
function campanhaAprovada(db, quantidadeLeads = 1) {
|
|
for (let i = 0; i < quantidadeLeads; i += 1) {
|
|
inserirLead(db, `lead-${i}`, `551199999000${i}`);
|
|
}
|
|
const campanha = createCampaign(db, {
|
|
nome: 'C', produto: 'ERP', segmentos: ['x'], variantes: [{ code: 'A', mensagem: 'Ola {empresa}' }],
|
|
limiteDiarioInicial: 10
|
|
});
|
|
setCampaignStatus(db, campanha.id, 'rodando');
|
|
generateCampaignQueue(db, campanha.id);
|
|
const fila = listQueue(db, campanha.id, 'preview');
|
|
approveQueueRows(db, campanha.id, fila.map((r) => r.id));
|
|
return campanha;
|
|
}
|
|
|
|
function fakeZproClient(overrides = {}) {
|
|
const enviados = [];
|
|
return {
|
|
enviados,
|
|
sendText: async (args) => { enviados.push(args); return { data: { ticketId: 999 } }; },
|
|
...overrides
|
|
};
|
|
}
|
|
|
|
test('runCampaignQueue envia linhas aprovadas via sendText e marca sent', async () => {
|
|
const db = openDb(':memory:');
|
|
ativarWorker(db);
|
|
const campanha = campanhaAprovada(db);
|
|
const zproClient = fakeZproClient();
|
|
|
|
const resumo = await runCampaignQueue({ db, zproClient, sleepMs: 0 });
|
|
assert.equal(resumo.enviados, 1);
|
|
assert.equal(zproClient.enviados.length, 1);
|
|
assert.equal(zproClient.enviados[0].message, 'Ola Empresa');
|
|
|
|
const fila = listQueue(db, campanha.id, 'sent');
|
|
assert.equal(fila.length, 1);
|
|
assert.ok(fila[0].outreach_send_id);
|
|
const send = db.prepare('SELECT * FROM outreach_sends WHERE id = ?').get(fila[0].outreach_send_id);
|
|
assert.equal(send.status, 'enviado');
|
|
assert.equal(send.origem, 'automatico');
|
|
db.close();
|
|
});
|
|
|
|
test('runCampaignQueue registra erro quando o Z-PRO falha, sem travar o restante', async () => {
|
|
const db = openDb(':memory:');
|
|
ativarWorker(db);
|
|
const campanha = campanhaAprovada(db, 2);
|
|
let chamada = 0;
|
|
const zproClient = fakeZproClient({
|
|
sendText: async (args) => {
|
|
chamada += 1;
|
|
if (chamada === 1) throw new Error('Z-PRO falhou');
|
|
return { data: { ticketId: 1 } };
|
|
}
|
|
});
|
|
|
|
const resumo = await runCampaignQueue({ db, zproClient, sleepMs: 0 });
|
|
assert.equal(resumo.enviados, 1);
|
|
assert.equal(resumo.erros, 1);
|
|
const erro = listQueue(db, campanha.id, 'error')[0];
|
|
const send = db.prepare('SELECT * FROM outreach_sends WHERE id = ?').get(erro.outreach_send_id);
|
|
assert.equal(send.status, 'erro');
|
|
db.close();
|
|
});
|
|
|
|
test('runCampaignQueue respeita o limite diario da campanha', async () => {
|
|
const db = openDb(':memory:');
|
|
ativarWorker(db);
|
|
for (let i = 0; i < 3; i += 1) inserirLead(db, `l${i}`, `551199999000${i}`);
|
|
const campanha = createCampaign(db, {
|
|
nome: 'C', produto: 'ERP', segmentos: ['x'], variantes: [{ code: 'A', mensagem: 'oi' }],
|
|
limiteDiarioInicial: 1
|
|
});
|
|
setCampaignStatus(db, campanha.id, 'rodando');
|
|
generateCampaignQueue(db, campanha.id);
|
|
approveQueueRows(db, campanha.id, listQueue(db, campanha.id, 'preview').map((r) => r.id));
|
|
|
|
const zproClient = fakeZproClient();
|
|
const resumo = await runCampaignQueue({ db, zproClient, sleepMs: 0 });
|
|
assert.equal(resumo.enviados, 1);
|
|
assert.equal(zproClient.enviados.length, 1);
|
|
assert.equal(listQueue(db, campanha.id, 'approved').length, 2);
|
|
db.close();
|
|
});
|
|
|
|
test('runCampaignQueue pula e cancela linha de numero que entrou em opt-out depois da fila gerada', async () => {
|
|
const db = openDb(':memory:');
|
|
ativarWorker(db);
|
|
const campanha = campanhaAprovada(db);
|
|
addOptOut(db, '5511999990000', 'pediu pra parar');
|
|
db.prepare(`UPDATE outreach_campaign_queue SET numero = '5511999990000' WHERE campaign_id = ?`).run(campanha.id);
|
|
|
|
const zproClient = fakeZproClient();
|
|
const resumo = await runCampaignQueue({ db, zproClient, sleepMs: 0 });
|
|
assert.equal(resumo.pulados, 1);
|
|
assert.equal(zproClient.enviados.length, 0);
|
|
const cancelada = listQueue(db, campanha.id, 'cancelled')[0];
|
|
assert.equal(cancelada.cancel_reason, 'opt_out');
|
|
db.close();
|
|
});
|
|
|
|
test('runCampaignQueue so processa campanhas com status rodando', async () => {
|
|
const db = openDb(':memory:');
|
|
ativarWorker(db);
|
|
inserirLead(db, 'l1', '5511999990001');
|
|
const campanha = createCampaign(db, {
|
|
nome: 'C', produto: 'ERP', segmentos: ['x'], variantes: [{ code: 'A', mensagem: 'oi' }]
|
|
});
|
|
generateCampaignQueue(db, campanha.id);
|
|
approveQueueRows(db, campanha.id, listQueue(db, campanha.id, 'preview').map((r) => r.id));
|
|
// campanha continua em 'rascunho' — nunca foi iniciada
|
|
|
|
const zproClient = fakeZproClient();
|
|
const resumo = await runCampaignQueue({ db, zproClient, sleepMs: 0 });
|
|
assert.equal(resumo.enviados, 0);
|
|
assert.equal(zproClient.enviados.length, 0);
|
|
db.close();
|
|
});
|
|
|
|
test('runCampaignQueue interrompe a campanha quando a saude aciona auto-pausa', async () => {
|
|
const db = openDb(':memory:');
|
|
ativarWorker(db);
|
|
const campanha = campanhaAprovada(db, 10);
|
|
db.prepare(`UPDATE outreach_campaigns SET limite_diario_atual = 10 WHERE id = ?`).run(campanha.id);
|
|
let chamada = 0;
|
|
const zproClient = fakeZproClient({
|
|
sendText: async () => {
|
|
chamada += 1;
|
|
if (chamada <= 8) return { data: { ticketId: chamada } };
|
|
throw new Error('falha simulada');
|
|
}
|
|
});
|
|
|
|
await runCampaignQueue({ db, zproClient, sleepMs: 0 });
|
|
const atualizada = db.prepare('SELECT status FROM outreach_campaigns WHERE id = ?').get(campanha.id);
|
|
// 10 tentativas, 2 erros = 20% > 10% -> deveria pausar antes de esgotar a fila
|
|
assert.equal(atualizada.status, 'pausado');
|
|
db.close();
|
|
});
|
|
|
|
test('runCampaignQueue nao envia nada quando outreach.ativo esta desligado', async () => {
|
|
const db = openDb(':memory:');
|
|
// Nao chama ativarWorker: settings.outreach.ativo fica no fallback 'false'.
|
|
const campanha = campanhaAprovada(db);
|
|
const zproClient = fakeZproClient();
|
|
|
|
const resumo = await runCampaignQueue({ db, zproClient, sleepMs: 0 });
|
|
assert.equal(resumo.enviados, 0);
|
|
assert.equal(zproClient.enviados.length, 0);
|
|
assert.equal(listQueue(db, campanha.id, 'approved').length, 1);
|
|
db.close();
|
|
});
|
|
|
|
test('runCampaignQueue nao envia nada fora do horario comercial configurado', async () => {
|
|
const db = openDb(':memory:');
|
|
setSetting(db, 'outreach.ativo', 'true');
|
|
setSetting(db, 'outreach.horaInicio', '08:00');
|
|
setSetting(db, 'outreach.horaFim', '18:00');
|
|
const campanha = campanhaAprovada(db);
|
|
const zproClient = fakeZproClient();
|
|
|
|
// 03:00 (horario local), bem fora da janela 08:00-18:00 configurada acima.
|
|
const foraDoHorario = new Date();
|
|
foraDoHorario.setHours(3, 0, 0, 0);
|
|
|
|
const resumo = await runCampaignQueue({ db, zproClient, now: foraDoHorario, sleepMs: 0 });
|
|
assert.equal(resumo.enviados, 0);
|
|
assert.equal(zproClient.enviados.length, 0);
|
|
assert.equal(listQueue(db, campanha.id, 'approved').length, 1);
|
|
db.close();
|
|
});
|