Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> git-subtree-dir: gti-maquina-comercial git-subtree-mainline:93ca82979egit-subtree-split:b77484946f
171 lines
7.9 KiB
JavaScript
171 lines
7.9 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { openDb, createLeadInserter } from '../src/db.mjs';
|
|
import { createCampaign, setCampaignStatus } from '../src/outreach/campaign-store.mjs';
|
|
import { evaluateCampaignHealth, applyCampaignHealth, variantStats } from '../src/outreach/campaign-metrics.mjs';
|
|
|
|
function inserirLead(db, dedup) {
|
|
const inserir = createLeadInserter(db);
|
|
inserir({
|
|
dedup_key: dedup, empresa: 'E', legal_name: '', whatsapp: `55119999${dedup.padStart(5, '0')}`,
|
|
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
|
|
});
|
|
return db.prepare('SELECT id, whatsapp FROM leads WHERE dedup_key = ?').get(dedup);
|
|
}
|
|
|
|
function campanhaRodando(db) {
|
|
const campanha = createCampaign(db, {
|
|
nome: 'C', produto: 'ERP', segmentos: ['x'], variantes: [{ code: 'A', mensagem: 'oi' }]
|
|
});
|
|
setCampaignStatus(db, campanha.id, 'rodando');
|
|
return campanha;
|
|
}
|
|
|
|
function simularEnvio(db, campanhaId, variantId, leadId, numero, status) {
|
|
const sendId = db.prepare(
|
|
`INSERT INTO outreach_sends (lead_id, produto, numero, mensagem_enviada, status) VALUES (?, 'ERP', ?, 'oi', ?)`
|
|
).run(leadId, numero, status).lastInsertRowid;
|
|
db.prepare(
|
|
`INSERT INTO outreach_campaign_queue (campaign_id, lead_id, variant_id, numero, rendered_message, status, outreach_send_id)
|
|
VALUES (?, ?, ?, ?, 'oi', ?, ?)`
|
|
).run(campanhaId, leadId, variantId, numero, status === 'erro' ? 'error' : 'sent', sendId);
|
|
}
|
|
|
|
test('evaluateCampaignHealth nao pausa campanha saudavel', () => {
|
|
const db = openDb(':memory:');
|
|
const campanha = campanhaRodando(db);
|
|
const variant = db.prepare('SELECT id FROM outreach_campaign_variants WHERE campaign_id = ?').get(campanha.id);
|
|
for (let i = 0; i < 10; i += 1) {
|
|
const lead = inserirLead(db, `l${i}`);
|
|
simularEnvio(db, campanha.id, variant.id, lead.id, lead.whatsapp, 'enviado');
|
|
}
|
|
const resultado = evaluateCampaignHealth(db, campanha.id);
|
|
assert.equal(resultado.pause, false);
|
|
db.close();
|
|
});
|
|
|
|
test('evaluateCampaignHealth pausa por high_failure_rate quando erro > 10% com 10+ tentativas', () => {
|
|
const db = openDb(':memory:');
|
|
const campanha = campanhaRodando(db);
|
|
const variant = db.prepare('SELECT id FROM outreach_campaign_variants WHERE campaign_id = ?').get(campanha.id);
|
|
for (let i = 0; i < 8; i += 1) {
|
|
const lead = inserirLead(db, `ok${i}`);
|
|
simularEnvio(db, campanha.id, variant.id, lead.id, lead.whatsapp, 'enviado');
|
|
}
|
|
for (let i = 0; i < 2; i += 1) {
|
|
const lead = inserirLead(db, `err${i}`);
|
|
simularEnvio(db, campanha.id, variant.id, lead.id, lead.whatsapp, 'erro');
|
|
}
|
|
const resultado = evaluateCampaignHealth(db, campanha.id);
|
|
assert.equal(resultado.pause, true);
|
|
assert.equal(resultado.reason, 'high_failure_rate');
|
|
// evaluateCampaignHealth e read-only: so retorna o veredito, nao mexe no banco.
|
|
const semMutacao = db.prepare('SELECT status FROM outreach_campaigns WHERE id = ?').get(campanha.id);
|
|
assert.equal(semMutacao.status, 'rodando');
|
|
|
|
// A mutacao (pausar de verdade) so acontece via applyCampaignHealth.
|
|
const aplicado = applyCampaignHealth(db, campanha.id);
|
|
assert.equal(aplicado.pause, true);
|
|
assert.equal(aplicado.reason, 'high_failure_rate');
|
|
const atualizada = db.prepare('SELECT status FROM outreach_campaigns WHERE id = ?').get(campanha.id);
|
|
assert.equal(atualizada.status, 'pausado');
|
|
db.close();
|
|
});
|
|
|
|
test('evaluateCampaignHealth nunca muda outreach_campaigns.status, mesmo quando retornaria pause:true (GET /saude nao deve pausar como efeito colateral de uma leitura)', () => {
|
|
const db = openDb(':memory:');
|
|
const campanha = campanhaRodando(db);
|
|
const variant = db.prepare('SELECT id FROM outreach_campaign_variants WHERE campaign_id = ?').get(campanha.id);
|
|
for (let i = 0; i < 8; i += 1) {
|
|
const lead = inserirLead(db, `puro-ok${i}`);
|
|
simularEnvio(db, campanha.id, variant.id, lead.id, lead.whatsapp, 'enviado');
|
|
}
|
|
for (let i = 0; i < 2; i += 1) {
|
|
const lead = inserirLead(db, `puro-err${i}`);
|
|
simularEnvio(db, campanha.id, variant.id, lead.id, lead.whatsapp, 'erro');
|
|
}
|
|
|
|
for (let i = 0; i < 5; i += 1) {
|
|
const resultado = evaluateCampaignHealth(db, campanha.id);
|
|
assert.equal(resultado.pause, true, 'deveria continuar sinalizando pause:true a cada chamada');
|
|
}
|
|
const aindaRodando = db.prepare('SELECT status FROM outreach_campaigns WHERE id = ?').get(campanha.id);
|
|
assert.equal(aindaRodando.status, 'rodando', 'chamar evaluateCampaignHealth repetidamente nao deveria ter pausado a campanha');
|
|
db.close();
|
|
});
|
|
|
|
test('evaluateCampaignHealth nao pausa por falha com menos de 10 tentativas no dia', () => {
|
|
const db = openDb(':memory:');
|
|
const campanha = campanhaRodando(db);
|
|
const variant = db.prepare('SELECT id FROM outreach_campaign_variants WHERE campaign_id = ?').get(campanha.id);
|
|
const lead = inserirLead(db, 'so1');
|
|
simularEnvio(db, campanha.id, variant.id, lead.id, lead.whatsapp, 'erro');
|
|
const resultado = evaluateCampaignHealth(db, campanha.id);
|
|
assert.equal(resultado.pause, false);
|
|
db.close();
|
|
});
|
|
|
|
test('evaluateCampaignHealth pausa por high_negative_rate quando >10% dos 20+ contatados estao em sem_interesse/opt_out', () => {
|
|
const db = openDb(':memory:');
|
|
const campanha = campanhaRodando(db);
|
|
const variant = db.prepare('SELECT id FROM outreach_campaign_variants WHERE campaign_id = ?').get(campanha.id);
|
|
for (let i = 0; i < 20; i += 1) {
|
|
const lead = inserirLead(db, `neg${i}`);
|
|
simularEnvio(db, campanha.id, variant.id, lead.id, lead.whatsapp, 'enviado');
|
|
if (i < 3) {
|
|
db.prepare(`INSERT INTO outreach_lead_states (lead_id, stage) VALUES (?, 'sem_interesse')`).run(lead.id);
|
|
}
|
|
}
|
|
const resultado = evaluateCampaignHealth(db, campanha.id);
|
|
assert.equal(resultado.pause, true);
|
|
assert.equal(resultado.reason, 'high_negative_rate');
|
|
db.close();
|
|
});
|
|
|
|
test('evaluateCampaignHealth pausa por low_reply_rate quando <5% dos 50+ contatados responderam', () => {
|
|
const db = openDb(':memory:');
|
|
const campanha = campanhaRodando(db);
|
|
const variant = db.prepare('SELECT id FROM outreach_campaign_variants WHERE campaign_id = ?').get(campanha.id);
|
|
for (let i = 0; i < 50; i += 1) {
|
|
const lead = inserirLead(db, `rep${i}`);
|
|
simularEnvio(db, campanha.id, variant.id, lead.id, lead.whatsapp, 'enviado');
|
|
}
|
|
const resultado = evaluateCampaignHealth(db, campanha.id);
|
|
assert.equal(resultado.pause, true);
|
|
assert.equal(resultado.reason, 'low_reply_rate');
|
|
db.close();
|
|
});
|
|
|
|
test('evaluateCampaignHealth em campanha ja pausada nao faz nada', () => {
|
|
const db = openDb(':memory:');
|
|
const campanha = campanhaRodando(db);
|
|
setCampaignStatus(db, campanha.id, 'pausado', 'manual');
|
|
const resultado = evaluateCampaignHealth(db, campanha.id);
|
|
assert.equal(resultado.pause, false);
|
|
db.close();
|
|
});
|
|
|
|
test('variantStats calcula taxa de resposta e elegibilidade (limiar 40 envios)', () => {
|
|
const db = openDb(':memory:');
|
|
const campanha = campanhaRodando(db);
|
|
const variant = db.prepare('SELECT id FROM outreach_campaign_variants WHERE campaign_id = ?').get(campanha.id);
|
|
for (let i = 0; i < 5; i += 1) {
|
|
const lead = inserirLead(db, `v${i}`);
|
|
simularEnvio(db, campanha.id, variant.id, lead.id, lead.whatsapp, 'enviado');
|
|
if (i < 1) {
|
|
db.prepare(`INSERT INTO outreach_stage_events (lead_id, campaign_id, stage) VALUES (?, ?, 'interessado')`).run(lead.id, campanha.id);
|
|
}
|
|
}
|
|
const stats = variantStats(db, campanha.id);
|
|
assert.equal(stats.length, 1);
|
|
assert.equal(stats[0].enviados, 5);
|
|
assert.equal(stats[0].respostas, 1);
|
|
assert.equal(stats[0].taxaResposta, 0.2);
|
|
assert.equal(stats[0].elegivelParaComparacao, false);
|
|
db.close();
|
|
});
|