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:f5af038e19git-subtree-split:a9ef4d776d
This commit is contained in:
@@ -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();
|
||||
});
|
||||
Reference in New Issue
Block a user