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