chore: import maquina-comercial-pessoal history via subtree

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>

git-subtree-dir: maquina-comercial-pessoal
git-subtree-mainline: f5af038e19
git-subtree-split: a9ef4d776d
This commit is contained in:
Gustavo
2026-09-28 15:50:17 -03:00
co-authored by Claude Sonnet 5
150 changed files with 33449 additions and 0 deletions
@@ -0,0 +1,241 @@
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();
});