Files
COMERCIAL-GTI/gti-maquina-comercial/tests/campaign-metrics.test.mjs
T
GustavoandClaude Sonnet 5 f5af038e19 chore: import gti-maquina-comercial history via subtree
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

git-subtree-dir: gti-maquina-comercial
git-subtree-mainline: 93ca82979e
git-subtree-split: b77484946f
2026-09-28 15:50:08 -03:00

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();
});