Files
COMERCIAL-GTI/maquina-comercial-pessoal/tests/inbound-outreach.test.mjs
T
GustavoandClaude Sonnet 5 417da56bd6 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
2026-09-28 15:50:17 -03:00

182 lines
7.2 KiB
JavaScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { openDb, saveWhatsAppVerification } from '../src/db.mjs';
import { seedCampaigns } from '../src/outreach/campaign-store.mjs';
import { runDueCampaignQueue } from '../src/outreach/campaign-worker.mjs';
import { recordInboundReply } from '../src/outreach/inbound.mjs';
function setup() {
const db = openDb(':memory:');
seedCampaigns(db);
const campaigns = db.prepare('SELECT * FROM outreach_campaigns ORDER BY id').all();
return { db, campaigns };
}
function addLead(db, suffix, numero) {
return Number(db.prepare(
`INSERT INTO leads (dedup_key, empresa, whatsapp, segmento, telefone_valido)
VALUES (?, ?, ?, 'restaurantes', 1)`
).run(`inbound-${suffix}`, `Empresa ${suffix}`, numero).lastInsertRowid);
}
function addQueue(db, campaign, { leadId, numero, suffix, status = 'preview' }) {
const variantId = db.prepare(
'SELECT id FROM outreach_campaign_variants WHERE campaign_id = ? ORDER BY id LIMIT 1'
).get(campaign.id).id;
const queueId = Number(db.prepare(
`INSERT INTO outreach_campaign_queue
(campaign_id, lead_id, variant_id, produto, numero, segmento,
rendered_message, selection_reason, status)
VALUES (?, ?, ?, ?, ?, 'restaurantes', ?, 'inbound-test', 'preview')`
).run(campaign.id, leadId, variantId, campaign.produto, numero, `Mensagem ${suffix}`).lastInsertRowid);
if (status === 'approved') {
db.prepare("UPDATE outreach_campaign_queue SET status = 'approved' WHERE id = ?").run(queueId);
} else if (status === 'scheduled') {
db.prepare(
"UPDATE outreach_campaign_queue SET status = 'scheduled', scheduled_at = '2026-07-29T12:00:00.000Z' WHERE id = ?"
).run(queueId);
}
return queueId;
}
function addHistoricalSend(db, campaign, { leadId, numero, suffix, sentAt }) {
const queueId = addQueue(db, campaign, { leadId, numero, suffix, status: 'scheduled' });
const sendId = Number(db.prepare(
`INSERT INTO outreach_sends
(lead_id, produto, numero, mensagem_enviada, status, origem, sent_at, provider_message_id)
VALUES (?, ?, ?, ?, 'aceito', 'automatico', ?, ?)`
).run(leadId, campaign.produto, numero, `Mensagem ${suffix}`, sentAt, `outbound-${suffix}`).lastInsertRowid);
db.prepare(
"UPDATE outreach_campaign_queue SET status = 'reserved', outreach_send_id = ? WHERE id = ?"
).run(sendId, queueId);
db.prepare("UPDATE outreach_campaign_queue SET status = 'sent' WHERE id = ?").run(queueId);
return queueId;
}
test('inbound usa envio de campanha mais recente entre variantes e aplica pausa idempotente', () => {
const { db, campaigns } = setup();
const withNine = '5575999091780';
const withoutNine = '557599091780';
const oldLeadId = addLead(db, 'old', withNine);
const latestLeadId = addLead(db, 'latest', withoutNine);
addHistoricalSend(db, campaigns[0], {
leadId: oldLeadId,
numero: withNine,
suffix: 'old',
sentAt: '2026-07-29T10:00:00.000Z'
});
addHistoricalSend(db, campaigns[1], {
leadId: latestLeadId,
numero: withoutNine,
suffix: 'latest',
sentAt: '2026-07-29T11:00:00.000Z'
});
db.prepare(
`INSERT INTO outreach_lead_states (lead_id, stage, first_reply_at, updated_at)
VALUES (?, 'interessado_gti_vendas', '2026-07-20T09:00:00.000Z', '2026-07-20T09:00:00.000Z')`
).run(latestLeadId);
const mutable = ['preview', 'approved', 'scheduled'].map((status, index) => {
const leadId = addLead(db, `mutable-${status}`, index % 2 ? withoutNine : withNine);
return addQueue(db, campaigns[index], {
leadId,
numero: index % 2 ? withoutNine : withNine,
suffix: status,
status
});
});
const event = {
messageId: 'provider-inbound-1',
jid: `${withNine}@s.whatsapp.net`,
number: withNine,
receivedAt: '2026-07-29T12:00:00.000Z'
};
assert.deepEqual(recordInboundReply(db, event), {
inserted: true,
leadId: latestLeadId,
cancelled: 3
});
assert.deepEqual(recordInboundReply(db, event), {
inserted: false,
leadId: latestLeadId,
cancelled: 0
});
assert.deepEqual(
{ ...db.prepare('SELECT lead_id, numero, received_at FROM outreach_inbound_replies').get() },
{ lead_id: latestLeadId, numero: withNine, received_at: event.receivedAt }
);
assert.deepEqual(
{ ...db.prepare('SELECT stage, first_reply_at, updated_at FROM outreach_lead_states WHERE lead_id = ?').get(latestLeadId) },
{ stage: 'respondeu', first_reply_at: '2026-07-20T09:00:00.000Z', updated_at: event.receivedAt }
);
assert.deepEqual(
db.prepare('SELECT lead_id, campaign_id, stage, created_at FROM outreach_stage_events').all().map((row) => ({ ...row })),
[{ lead_id: latestLeadId, campaign_id: campaigns[1].id, stage: 'respondeu', created_at: event.receivedAt }]
);
assert.deepEqual(
db.prepare(`SELECT status, cancel_reason FROM outreach_campaign_queue WHERE id IN (${mutable.map(() => '?').join(',')}) ORDER BY id`).all(...mutable).map((row) => ({ ...row })),
mutable.map(() => ({ status: 'cancelled', cancel_reason: 'inbound_reply' }))
);
assert.equal(db.prepare('SELECT COUNT(*) AS total FROM outreach_opt_outs').get().total, 0);
assert.equal(db.prepare("SELECT COUNT(*) AS total FROM outreach_lead_states WHERE stage <> 'respondeu' AND lead_id = ?").get(latestLeadId).total, 0);
db.close();
});
test('inbound vence corrida no beforeSend e impede chamada ao provedor', async () => {
const { db, campaigns } = setup();
const campaign = campaigns[0];
db.prepare("UPDATE outreach_campaigns SET status = 'running' WHERE id = ?").run(campaign.id);
const numero = '5575999091780';
const leadId = addLead(db, 'race', numero);
saveWhatsAppVerification(db, { numero, status: 'confirmado' });
const queueId = addQueue(db, campaign, { leadId, numero, suffix: 'race', status: 'scheduled' });
let releaseGuard;
let guardStarted;
const blocked = new Promise((resolve) => { guardStarted = resolve; });
const gate = new Promise((resolve) => { releaseGuard = resolve; });
let providerCalls = 0;
const whatsapp = {
getStatus: () => ({ state: 'READY' }),
sendText: async (args) => {
const originalBeforeSend = args.beforeSend;
args.beforeSend = async () => {
guardStarted();
await gate;
return originalBeforeSend();
};
const context = await args.beforeSend();
providerCalls += 1;
const result = { messageId: 'must-not-send' };
await args.afterSend(result, null, context);
return result;
}
};
const running = runDueCampaignQueue({
db,
whatsapp,
now: new Date('2026-07-29T12:00:00.000Z'),
log: () => {}
});
await blocked;
assert.deepEqual(recordInboundReply(db, {
messageId: 'provider-race-1',
jid: `${numero}@s.whatsapp.net`,
number: numero,
receivedAt: '2026-07-29T11:59:59.000Z'
}), { inserted: true, leadId: null, cancelled: 1 });
releaseGuard();
const summary = await running;
assert.equal(providerCalls, 0);
assert.equal(summary.enviados, 0);
assert.deepEqual(
{ ...db.prepare('SELECT status, cancel_reason FROM outreach_campaign_queue WHERE id = ?').get(queueId) },
{ status: 'cancelled', cancel_reason: 'inbound_reply' }
);
assert.equal(db.prepare('SELECT COUNT(*) AS total FROM outreach_sends').get().total, 0);
db.close();
});