Files
COMERCIAL-GTI/tests/pipeline.test.mjs
T
Gustavo d330bb5ff0 Baseline commit of the existing Maquina Comercial Pessoal project
Tracks the app that was already in place (capture, treatment, outreach,
WhatsApp integration, server, frontend, tests, docs) so future feature
work can branch and use isolated worktrees.
2026-07-31 10:54:07 -03:00

242 lines
9.7 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { openDb, addOptOut } from '../src/db.mjs';
import { seedCampaigns, setLeadCommercialStage, saveAppointment, listPendingReplies, listCampaigns } from '../src/outreach/campaign-store.mjs';
import { CAMPAIGN_STAGES } from '../src/outreach/campaign-catalog.mjs';
function insertLead(db, empresa = 'Lead Teste', whatsapp = '5511999990001') {
db.prepare("INSERT INTO leads (dedup_key, empresa, whatsapp) VALUES (?, ?, ?)").run(`pipeline-${empresa}`, empresa, whatsapp);
return Number(db.prepare('SELECT id FROM leads ORDER BY id DESC LIMIT 1').get().id);
}
function insertLeadWithReply(db, empresa = 'Lead Respondeu', whatsapp = '5511999990002') {
const leadId = insertLead(db, empresa, whatsapp);
setLeadCommercialStage(db, { leadId, stage: 'enviado' });
setLeadCommercialStage(db, { leadId, stage: 'respondeu', note: 'respondeu no whatsapp' });
return leadId;
}
test('listPendingReplies retorna leads que responderam e estao em etapas iniciais', () => {
const db = openDb(':memory:');
seedCampaigns(db);
const replied = insertLeadWithReply(db, 'Lead A', '5511999990010');
const noReply = insertLead(db, 'Lead B', '5511999990011');
const movedToInterest = insertLeadWithReply(db, 'Lead C', '5511999990012');
setLeadCommercialStage(db, { leadId: movedToInterest, stage: 'interessado_gti_vendas', note: 'quer gti' });
const optedOut = insertLead(db, 'Lead D', '5511999990013');
setLeadCommercialStage(db, { leadId: optedOut, stage: 'opt_out', note: 'nao quer' });
const pending = listPendingReplies(db);
assert.equal(pending.length, 1);
assert.equal(pending[0].lead_id, replied);
assert.equal(pending[0].stage, 'respondeu');
db.close();
});
test('setLeadCommercialStage rejeita etapa fora do catalogo', () => {
const db = openDb(':memory:');
seedCampaigns(db);
const leadId = insertLead(db);
assert.throws(() => setLeadCommercialStage(db, { leadId, stage: 'etapa_inexistente' }), /etapa comercial invalida/);
db.close();
});
test('setLeadCommercialStage aceita somente as 11 etapas do catalogo', () => {
const db = openDb(':memory:');
seedCampaigns(db);
assert.deepEqual(CAMPAIGN_STAGES, [
'fila_preparada', 'aprovado_para_envio', 'enviado', 'respondeu',
'interessado_gti_vendas', 'interessado_g_delivery', 'demonstracao_agendada',
'teste_g_delivery_iniciado', 'sem_interesse', 'opt_out', 'venda_fechada'
]);
db.close();
});
test('setLeadCommercialStage atualiza estado atual e insere historico na mesma transacao', () => {
const db = openDb(':memory:');
seedCampaigns(db);
const leadId = insertLead(db);
const campaignId = listCampaigns(db)[0].id;
const result = setLeadCommercialStage(db, { leadId, campaignId, stage: 'respondeu', note: 'cliente respondeu' });
assert.equal(result.lead_id, leadId);
assert.equal(result.stage, 'respondeu');
const events = db.prepare('SELECT stage, note, campaign_id FROM outreach_stage_events WHERE lead_id = ? ORDER BY id').all(leadId);
assert.equal(events.length, 1);
assert.equal(events[0].stage, 'respondeu');
assert.equal(events[0].note, 'cliente respondeu');
assert.equal(events[0].campaign_id, campaignId);
db.close();
});
test('setLeadCommercialStage com opt_out adiciona opt-out do telefone equivalente', () => {
const db = openDb(':memory:');
seedCampaigns(db);
const leadId = insertLead(db, 'Lead Opt', '5575999091780');
const result = setLeadCommercialStage(db, { leadId, stage: 'opt_out', note: 'solicitou remocao' });
assert.equal(result.stage, 'opt_out');
const opt = db.prepare("SELECT numero, motivo FROM outreach_opt_outs WHERE numero = '5575999091780'").get();
assert.ok(opt);
assert.equal(opt.motivo, 'solicitou remocao');
db.close();
});
test('setLeadCommercialStage transition de opt_out para outra etapa nao recria opt-out', () => {
const db = openDb(':memory:');
seedCampaigns(db);
const leadId = insertLead(db, 'Lead Opt Back', '557599091780');
setLeadCommercialStage(db, { leadId, stage: 'opt_out', note: 'solicitou' });
const updated = setLeadCommercialStage(db, { leadId, stage: 'interessado_gti_vendas', note: 'mudou de ideia' });
assert.equal(updated.stage, 'interessado_gti_vendas');
const opts = db.prepare('SELECT COUNT(*) total FROM outreach_opt_outs').get().total;
assert.equal(opts, 1);
db.close();
});
test('setLeadCommercialStage armazena historico completo ordenado por id ascendente', () => {
const db = openDb(':memory:');
seedCampaigns(db);
const leadId = insertLead(db, 'Lead Historico', '5511999990020');
const stages = ['fila_preparada', 'aprovado_para_envio', 'enviado', 'respondeu', 'interessado_gti_vendas', 'demonstracao_agendada'];
for (const stage of stages) {
setLeadCommercialStage(db, { leadId, stage, note: `etapa ${stage}` });
}
const events = db.prepare('SELECT stage FROM outreach_stage_events WHERE lead_id = ? ORDER BY id').all(leadId).map((r) => r.stage);
assert.deepEqual(events, stages);
const state = db.prepare('SELECT stage, lead_id FROM outreach_lead_states WHERE lead_id = ?').get(leadId);
assert.equal(state.stage, 'demonstracao_agendada');
db.close();
});
test('saveAppointment persiste agendamento com campos obrigatorios', () => {
const db = openDb(':memory:');
seedCampaigns(db);
const leadId = insertLead(db, 'Lead App', '5511999990030');
const campaignId = listCampaigns(db)[0].id;
const app = saveAppointment(db, {
leadId,
campaignId,
kind: 'demonstracao',
participantName: 'Joao',
companyName: 'Empresa App',
preferredDay: '2026-08-05',
preferredTime: '14:30'
});
assert.equal(app.lead_id, leadId);
assert.equal(app.kind, 'demonstracao');
assert.equal(app.participant_name, 'Joao');
assert.equal(app.company_name, 'Empresa App');
assert.equal(app.preferred_day, '2026-08-05');
assert.equal(app.preferred_time, '14:30');
const stored = db.prepare('SELECT * FROM outreach_appointments WHERE id = ?').get(app.id);
assert.equal(stored.lead_id, leadId);
db.close();
});
test('saveAppointment cria appointment kind teste_g_delivery sem preferred_day/preferred_time', () => {
const db = openDb(':memory:');
seedCampaigns(db);
const leadId = insertLead(db, 'Lead Teste', '5511999990031');
const app = saveAppointment(db, { leadId, kind: 'teste_g_delivery' });
assert.equal(app.kind, 'teste_g_delivery');
assert.equal(app.participant_name, null);
assert.equal(app.company_name, null);
assert.equal(app.preferred_day, null);
assert.equal(app.preferred_time, null);
db.close();
});
test('listPendingReplies inclui campo produto do ultimo envio da campanha', () => {
const db = openDb(':memory:');
seedCampaigns(db);
const campaign = listCampaigns(db)[0];
const leadId = insertLead(db, 'Lead Produto', '5511999990040');
setLeadCommercialStage(db, { leadId, campaignId: campaign.id, stage: 'enviado' });
setLeadCommercialStage(db, { leadId, campaignId: campaign.id, stage: 'respondeu' });
const pending = listPendingReplies(db);
assert.equal(pending.length, 1);
assert.equal(pending[0].produto, campaign.produto);
db.close();
});
test('setLeadCommercialStage com campaignId nulo e aceito', () => {
const db = openDb(':memory:');
seedCampaigns(db);
const leadId = insertLead(db, 'Lead NoCamp', '5511999990050');
const result = setLeadCommercialStage(db, { leadId, stage: 'sem_interesse' });
assert.equal(result.stage, 'sem_interesse');
db.close();
});
test('historico de etapas retorna ordenado newest first via rota', () => {
const db = openDb(':memory:');
seedCampaigns(db);
const leadId = insertLead(db, 'Lead Order', '5511999990060');
setLeadCommercialStage(db, { leadId, stage: 'enviado' });
setLeadCommercialStage(db, { leadId, stage: 'respondeu' });
const events = db.prepare('SELECT stage FROM outreach_stage_events WHERE lead_id = ? ORDER BY id DESC').all(leadId).map((r) => r.stage);
assert.deepEqual(events, ['respondeu', 'enviado']);
db.close();
});
test('F1: setLeadCommercialStage cancela itens da fila do lead em preview/approved/scheduled', () => {
const db = openDb(':memory:');
seedCampaigns(db);
const leadId = insertLead(db, 'Lead Fila', '5511999990080');
const campaign = listCampaigns(db)[0];
const variant = campaign.variants[0];
db.prepare(
`INSERT INTO outreach_campaign_queue
(campaign_id, lead_id, variant_id, produto, numero, segmento, rendered_message, selection_reason, status)
VALUES (?, ?, ?, ?, ?, 'restaurantes', 'msg', 'test', 'approved')`
).run(campaign.id, leadId, variant.id, campaign.produto, '5511999990080');
setLeadCommercialStage(db, { leadId, stage: 'sem_interesse' });
const queueItem = db.prepare('SELECT status, cancel_reason FROM outreach_campaign_queue WHERE lead_id = ?').get(leadId);
assert.equal(queueItem.status, 'cancelled');
assert.equal(queueItem.cancel_reason, 'lead_stage_moved');
db.close();
});
test('F2: saveAppointment rejeita preferredDay com formato invalido', () => {
const db = openDb(':memory:');
seedCampaigns(db);
const leadId = insertLead(db, 'Lead Date', '5511999990090');
assert.throws(
() => saveAppointment(db, {
leadId,
kind: 'demonstracao',
participantName: 'Joao',
companyName: 'Empresa',
preferredDay: '2026/08/05',
preferredTime: '14:30'
}),
/formato de data invalido/i
);
db.close();
});
test('F2: saveAppointment rejeita preferredTime com formato invalido', () => {
const db = openDb(':memory:');
seedCampaigns(db);
const leadId = insertLead(db, 'Lead Time', '5511999990091');
assert.throws(
() => saveAppointment(db, {
leadId,
kind: 'demonstracao',
participantName: 'Joao',
companyName: 'Empresa',
preferredDay: '2026-08-05',
preferredTime: '2:30 PM'
}),
/formato de hora invalido/i
);
db.close();
});