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.
547 lines
20 KiB
JavaScript
547 lines
20 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import {
|
|
addOptOut,
|
|
openDb,
|
|
saveWhatsAppVerification
|
|
} from '../src/db.mjs';
|
|
import { prepareDailyQueue } from '../src/outreach/campaign-queue.mjs';
|
|
import {
|
|
getCampaign,
|
|
listCampaignQueue,
|
|
listCampaigns,
|
|
seedCampaigns
|
|
} from '../src/outreach/campaign-store.mjs';
|
|
|
|
function setupCampaign(code = 'gdelivery-food') {
|
|
const db = openDb(':memory:');
|
|
seedCampaigns(db);
|
|
const campaign = listCampaigns(db).find((item) => item.code === code);
|
|
return { db, campaign };
|
|
}
|
|
|
|
function insertLead(db, {
|
|
key,
|
|
empresa = '',
|
|
whatsapp,
|
|
segmento = 'pizzarias',
|
|
prioridade = 1,
|
|
score = 0
|
|
}) {
|
|
return Number(db.prepare(
|
|
`INSERT INTO leads
|
|
(dedup_key, empresa, whatsapp, segmento, prioridade, score, telefone_valido)
|
|
VALUES (?, ?, ?, ?, ?, ?, 1)`
|
|
).run(key, empresa || key, whatsapp, segmento, prioridade, score).lastInsertRowid);
|
|
}
|
|
|
|
function confirmed(db, numero, {
|
|
resolvedNumber = numero,
|
|
checkedAt = null,
|
|
status = 'confirmado'
|
|
} = {}) {
|
|
saveWhatsAppVerification(db, {
|
|
numero,
|
|
status,
|
|
resolvedNumber: status === 'confirmado' ? resolvedNumber : null,
|
|
variant: 'exata'
|
|
});
|
|
if (checkedAt) {
|
|
db.prepare('UPDATE whatsapp_number_verifications SET checked_at = ? WHERE numero = ?')
|
|
.run(checkedAt, numero);
|
|
}
|
|
}
|
|
|
|
function noSendWhatsapp(checkNumber = async (numero) => ({
|
|
numero,
|
|
status: 'nao_cadastrado',
|
|
registered: false,
|
|
resolvedNumber: null,
|
|
variant: null,
|
|
candidatesChecked: 2
|
|
})) {
|
|
return {
|
|
checkNumber,
|
|
sendText() {
|
|
throw new Error('prepareDailyQueue nao pode enviar mensagens');
|
|
}
|
|
};
|
|
}
|
|
|
|
function traceCandidateQuery(db) {
|
|
const trace = { sql: null, args: null };
|
|
const tracedDb = new Proxy(db, {
|
|
get(target, property) {
|
|
if (property === 'prepare') {
|
|
return (sql) => {
|
|
const statement = target.prepare(sql);
|
|
if (!sql.includes('campaign-candidates')) return statement;
|
|
trace.sql = sql;
|
|
return new Proxy(statement, {
|
|
get(statementTarget, statementProperty) {
|
|
if (statementProperty === 'all') {
|
|
return (...args) => {
|
|
trace.args = args;
|
|
return statementTarget.all(...args);
|
|
};
|
|
}
|
|
const value = statementTarget[statementProperty];
|
|
return typeof value === 'function' ? value.bind(statementTarget) : value;
|
|
}
|
|
});
|
|
};
|
|
}
|
|
const value = target[property];
|
|
return typeof value === 'function' ? value.bind(target) : value;
|
|
}
|
|
});
|
|
return { tracedDb, trace };
|
|
}
|
|
|
|
test('prepara confirmados na ordem comercial e exclui inelegiveis em SQL', async () => {
|
|
const { db, campaign } = setupCampaign();
|
|
const highScore = insertLead(db, {
|
|
key: 'high-score', empresa: 'Pizzaria Sol', whatsapp: '5575991234001', prioridade: 1, score: 90
|
|
});
|
|
const lowScore = insertLead(db, {
|
|
key: 'low-score', whatsapp: '5575991234002', prioridade: 1, score: 40
|
|
});
|
|
const lowerPriority = insertLead(db, {
|
|
key: 'lower-priority', whatsapp: '5575991234003', prioridade: 2, score: 100
|
|
});
|
|
const priorProduct = insertLead(db, {
|
|
key: 'prior-product', whatsapp: '5575991234004', prioridade: 1, score: 99
|
|
});
|
|
const optedOut = insertLead(db, {
|
|
key: 'opted-out', whatsapp: '5575991234005', prioridade: 1, score: 98
|
|
});
|
|
const replied = insertLead(db, {
|
|
key: 'replied', whatsapp: '5575991234006', prioridade: 1, score: 97
|
|
});
|
|
const queued = insertLead(db, {
|
|
key: 'queued', whatsapp: '5575991234007', prioridade: 1, score: 96
|
|
});
|
|
const staleConfirmation = insertLead(db, {
|
|
key: 'stale-confirmation', whatsapp: '5575991234008', prioridade: 1, score: 95
|
|
});
|
|
const outsideSegment = insertLead(db, {
|
|
key: 'outside-segment', whatsapp: '5575991234009', segmento: 'restaurantes', prioridade: 1, score: 100
|
|
});
|
|
|
|
for (const number of [
|
|
'5575991234001', '5575991234002', '5575991234003', '5575991234004',
|
|
'5575991234005', '5575991234006', '5575991234007', '5575991234008',
|
|
'5575991234009'
|
|
]) confirmed(db, number);
|
|
confirmed(db, '557591234008', {
|
|
status: 'nao_cadastrado', checkedAt: '2026-07-29 13:00:00'
|
|
});
|
|
db.prepare("UPDATE whatsapp_number_verifications SET checked_at = '2026-07-29 12:00:00' WHERE numero = '5575991234008'").run();
|
|
|
|
db.prepare(
|
|
`INSERT INTO outreach_sends (lead_id, produto, numero, mensagem_enviada, status, origem)
|
|
VALUES (?, 'G-Delivery', '5575991234004', 'anterior', 'erro', 'manual')`
|
|
).run(priorProduct);
|
|
addOptOut(db, '557591234005', 'pedido do contato');
|
|
db.prepare(
|
|
`INSERT INTO outreach_lead_states (lead_id, stage, first_reply_at)
|
|
VALUES (?, 'respondeu', '2026-07-29 10:00:00')`
|
|
).run(replied);
|
|
const variantA = campaign.variants.find((variant) => variant.code === 'A');
|
|
db.prepare(
|
|
`INSERT INTO outreach_campaign_queue
|
|
(campaign_id, lead_id, variant_id, produto, numero, segmento, rendered_message, selection_reason, status)
|
|
VALUES (?, ?, ?, 'G-Delivery', '5575991234007', 'pizzarias', 'existente', 'existente', 'preview')`
|
|
).run(campaign.id, queued, variantA.id);
|
|
|
|
let checks = 0;
|
|
const summary = await prepareDailyQueue({
|
|
db,
|
|
whatsapp: noSendWhatsapp(async () => {
|
|
checks += 1;
|
|
throw new Error('confirmados existentes nao devem ser consultados');
|
|
}),
|
|
campaignId: campaign.id,
|
|
now: new Date('2026-07-29T14:00:00-03:00')
|
|
});
|
|
const queue = listCampaignQueue(db, campaign.id, 'preview');
|
|
|
|
assert.deepEqual(summary, { checked: 0, previewed: 3, skipped: 0 });
|
|
assert.equal(checks, 0);
|
|
assert.deepEqual(queue.map((item) => item.lead_id), [queued, highScore, lowScore, lowerPriority]);
|
|
assert.deepEqual(queue.slice(1).map((item) => item.variant_code), ['B', 'C', 'A']);
|
|
assert.match(queue[1].rendered_message, /^Olá, pessoal da Pizzaria Sol, tudo bem\?/);
|
|
assert.deepEqual(queue.slice(1).map((item) => item.selection_reason), [
|
|
'gdelivery-food:prioridade-1',
|
|
'gdelivery-food:prioridade-1',
|
|
'gdelivery-food:prioridade-2'
|
|
]);
|
|
assert.equal(queue.some((item) => [priorProduct, optedOut, replied, staleConfirmation, outsideSegment].includes(item.lead_id)), false);
|
|
assert.equal(new Set(queue.map((item) => item.lead_id)).size, queue.length);
|
|
db.close();
|
|
});
|
|
|
|
test('estado mais recente de numero equivalente confirma e persiste o numero resolvido', async () => {
|
|
const { db, campaign } = setupCampaign();
|
|
const leadId = insertLead(db, {
|
|
key: 'equivalent-confirmed', whatsapp: '5575998765432', score: 50
|
|
});
|
|
confirmed(db, '5575998765432', {
|
|
status: 'nao_cadastrado', checkedAt: '2026-07-29 10:00:00'
|
|
});
|
|
confirmed(db, '557598765432', {
|
|
resolvedNumber: '557598765432', checkedAt: '2026-07-29 11:00:00'
|
|
});
|
|
|
|
const summary = await prepareDailyQueue({
|
|
db,
|
|
whatsapp: noSendWhatsapp(async () => {
|
|
throw new Error('estado equivalente confirmado nao deve consumir verificacao');
|
|
}),
|
|
campaignId: campaign.id,
|
|
now: new Date('2026-07-29T14:00:00-03:00')
|
|
});
|
|
const queue = listCampaignQueue(db, campaign.id);
|
|
|
|
assert.deepEqual(summary, { checked: 0, previewed: 1, skipped: 0 });
|
|
assert.equal(queue[0].lead_id, leadId);
|
|
assert.equal(queue[0].numero, '557598765432');
|
|
db.close();
|
|
});
|
|
|
|
test('verifica sequencialmente no maximo o teto diario sem cobrar confirmados existentes', async () => {
|
|
const { db, campaign } = setupCampaign();
|
|
const confirmedLead = insertLead(db, {
|
|
key: 'already-confirmed', whatsapp: '5575992222000', score: 100
|
|
});
|
|
confirmed(db, '5575992222000');
|
|
for (let index = 1; index <= 20; index += 1) {
|
|
insertLead(db, {
|
|
key: `unverified-${index}`,
|
|
whatsapp: `55759922${String(index).padStart(4, '0')}`,
|
|
score: 100 - index
|
|
});
|
|
}
|
|
|
|
const calls = [];
|
|
let inFlight = 0;
|
|
let maxInFlight = 0;
|
|
const whatsapp = noSendWhatsapp(async (numero) => {
|
|
calls.push(numero);
|
|
inFlight += 1;
|
|
maxInFlight = Math.max(maxInFlight, inFlight);
|
|
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
inFlight -= 1;
|
|
return {
|
|
numero,
|
|
status: 'nao_cadastrado',
|
|
registered: false,
|
|
resolvedNumber: null,
|
|
variant: null,
|
|
candidatesChecked: 2
|
|
};
|
|
});
|
|
|
|
const summary = await prepareDailyQueue({
|
|
db,
|
|
whatsapp,
|
|
campaignId: campaign.id,
|
|
now: new Date('2026-07-29T14:00:00-03:00')
|
|
});
|
|
const queue = listCampaignQueue(db, campaign.id);
|
|
|
|
assert.deepEqual(summary, { checked: 15, previewed: 1, skipped: 15 });
|
|
assert.equal(summary.checked, Math.ceil(getCampaign(db, campaign.id).approved_daily_limit * 1.5));
|
|
assert.equal(maxInFlight, 1);
|
|
assert.equal(queue.length, 1);
|
|
assert.equal(queue[0].lead_id, confirmedLead);
|
|
assert.equal(db.prepare('SELECT COUNT(*) total FROM whatsapp_number_verifications').get().total, 16);
|
|
assert.deepEqual(calls, Array.from({ length: 15 }, (_, index) => `55759922${String(index + 1).padStart(4, '0')}`));
|
|
db.close();
|
|
});
|
|
|
|
test('limite de verificacao ignora multiplicador adulterado acima de 1.5', async () => {
|
|
const { db, campaign } = setupCampaign();
|
|
db.exec('PRAGMA ignore_check_constraints = ON');
|
|
db.exec('DROP TRIGGER outreach_campaigns_verification_multiplier_insert');
|
|
db.exec('DROP TRIGGER outreach_campaigns_verification_multiplier_update');
|
|
db.prepare('UPDATE outreach_campaigns SET verification_multiplier = 99 WHERE id = ?').run(campaign.id);
|
|
for (let index = 1; index <= 30; index += 1) {
|
|
insertLead(db, {
|
|
key: `malicious-cap-${index}`,
|
|
whatsapp: `55759888${String(index).padStart(4, '0')}`,
|
|
score: 100 - index
|
|
});
|
|
}
|
|
const calls = [];
|
|
|
|
const summary = await prepareDailyQueue({
|
|
db,
|
|
whatsapp: noSendWhatsapp(async (numero) => {
|
|
calls.push(numero);
|
|
return {
|
|
numero,
|
|
status: 'nao_cadastrado',
|
|
registered: false,
|
|
resolvedNumber: null,
|
|
variant: null,
|
|
candidatesChecked: 2
|
|
};
|
|
}),
|
|
campaignId: campaign.id,
|
|
now: new Date('2026-07-29T14:00:00-03:00')
|
|
});
|
|
|
|
assert.equal(summary.checked, 15);
|
|
assert.equal(calls.length, 15);
|
|
db.close();
|
|
});
|
|
|
|
test('persiste erro temporario e nao repete equivalente na mesma preparacao', async () => {
|
|
const { db, campaign } = setupCampaign();
|
|
insertLead(db, { key: 'temporary-a', whatsapp: '5575999091780', score: 100 });
|
|
insertLead(db, { key: 'temporary-b', whatsapp: '557599091780', score: 90 });
|
|
const calls = [];
|
|
const whatsapp = noSendWhatsapp(async (numero) => {
|
|
calls.push(numero);
|
|
throw Object.assign(new Error('consulta excedeu o prazo'), {
|
|
numero,
|
|
status: 'erro_temporario',
|
|
registered: false,
|
|
resolvedNumber: null,
|
|
variant: null,
|
|
candidatesChecked: 2
|
|
});
|
|
});
|
|
|
|
const summary = await prepareDailyQueue({
|
|
db,
|
|
whatsapp,
|
|
campaignId: campaign.id,
|
|
now: new Date('2026-07-29T14:00:00-03:00')
|
|
});
|
|
|
|
assert.deepEqual(summary, { checked: 1, previewed: 0, skipped: 2 });
|
|
assert.deepEqual(calls, ['5575999091780']);
|
|
assert.deepEqual({ ...db.prepare(
|
|
'SELECT numero, status, erro FROM whatsapp_number_verifications'
|
|
).get() }, {
|
|
numero: '5575999091780',
|
|
status: 'erro_temporario',
|
|
erro: 'consulta excedeu o prazo'
|
|
});
|
|
db.close();
|
|
});
|
|
|
|
test('duas conexoes reais recontam a mesma campanha sem SQLITE_BUSY ou excesso', async () => {
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'campaign-queue-race-'));
|
|
const dbPath = path.join(tempDir, 'race.sqlite');
|
|
let firstDb;
|
|
let secondDb;
|
|
try {
|
|
firstDb = openDb(dbPath);
|
|
seedCampaigns(firstDb);
|
|
secondDb = openDb(dbPath);
|
|
const firstCampaign = listCampaigns(firstDb).find(({ code }) => code === 'gti-food');
|
|
for (let index = 0; index < 12; index += 1) {
|
|
insertLead(firstDb, {
|
|
key: `real-race-${index}`,
|
|
whatsapp: `5575993300${String(index).padStart(2, '0')}`,
|
|
segmento: 'restaurantes',
|
|
score: 100 - index
|
|
});
|
|
}
|
|
|
|
let release;
|
|
const gate = new Promise((resolve) => { release = resolve; });
|
|
let arrivals = 0;
|
|
const whatsapp = noSendWhatsapp(async (numero) => {
|
|
arrivals += 1;
|
|
if (arrivals === 2) release();
|
|
await gate;
|
|
return {
|
|
numero,
|
|
status: 'confirmado',
|
|
registered: true,
|
|
resolvedNumber: numero,
|
|
variant: 'exata',
|
|
candidatesChecked: 2
|
|
};
|
|
});
|
|
|
|
const results = await Promise.allSettled([
|
|
prepareDailyQueue({ first: true, db: firstDb, whatsapp, campaignId: firstCampaign.id, now: new Date('2026-07-29T14:00:00-03:00') }),
|
|
prepareDailyQueue({ db: secondDb, whatsapp, campaignId: firstCampaign.id, now: new Date('2026-07-29T14:00:00-03:00') })
|
|
]);
|
|
|
|
assert.equal(results.every(({ status }) => status === 'fulfilled'), true, results.map((item) => item.reason?.message));
|
|
const queue = firstDb.prepare(
|
|
"SELECT produto, lead_id FROM outreach_campaign_queue WHERE status = 'preview' ORDER BY lead_id"
|
|
).all();
|
|
assert.ok(queue.length <= 10);
|
|
assert.equal(new Set(queue.map(({ lead_id }) => lead_id)).size, queue.length);
|
|
assert.equal(queue.every(({ produto }) => produto === 'GTI Vendas'), true);
|
|
} finally {
|
|
secondDb?.close();
|
|
firstDb?.close();
|
|
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test('campanhas sobrepostas do mesmo produto nao criam duas filas ativas', async () => {
|
|
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'campaign-product-race-'));
|
|
const dbPath = path.join(tempDir, 'race.sqlite');
|
|
let firstDb;
|
|
let secondDb;
|
|
try {
|
|
firstDb = openDb(dbPath);
|
|
seedCampaigns(firstDb);
|
|
secondDb = openDb(dbPath);
|
|
const firstCampaign = listCampaigns(firstDb).find(({ code }) => code === 'gti-food');
|
|
const secondCampaign = listCampaigns(secondDb).find(({ code }) => code === 'gti-retail');
|
|
secondDb.prepare("UPDATE outreach_campaigns SET segments_json = '[\"restaurantes\"]' WHERE id = ?")
|
|
.run(secondCampaign.id);
|
|
insertLead(firstDb, {
|
|
key: 'same-product-race', whatsapp: '557599771001', segmento: 'restaurantes', score: 100
|
|
});
|
|
let release;
|
|
const gate = new Promise((resolve) => { release = resolve; });
|
|
let arrivals = 0;
|
|
const whatsapp = noSendWhatsapp(async (numero) => {
|
|
arrivals += 1;
|
|
if (arrivals === 2) release();
|
|
await gate;
|
|
return {
|
|
numero,
|
|
status: 'confirmado',
|
|
registered: true,
|
|
resolvedNumber: numero,
|
|
variant: 'exata',
|
|
candidatesChecked: 2
|
|
};
|
|
});
|
|
|
|
const results = await Promise.allSettled([
|
|
prepareDailyQueue({ db: firstDb, whatsapp, campaignId: firstCampaign.id, now: new Date('2026-07-29T14:00:00-03:00') }),
|
|
prepareDailyQueue({ db: secondDb, whatsapp, campaignId: secondCampaign.id, now: new Date('2026-07-29T14:00:00-03:00') })
|
|
]);
|
|
|
|
assert.equal(results.every(({ status }) => status === 'fulfilled'), true, results.map((item) => item.reason?.message));
|
|
assert.deepEqual(firstDb.prepare(
|
|
"SELECT produto, lead_id, COUNT(*) total FROM outreach_campaign_queue WHERE status = 'preview' GROUP BY produto, lead_id"
|
|
).all().map((row) => ({ ...row })), [{ produto: 'GTI Vendas', lead_id: 1, total: 1 }]);
|
|
} finally {
|
|
secondDb?.close();
|
|
firstDb?.close();
|
|
fs.rmSync(tempDir, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 });
|
|
}
|
|
});
|
|
|
|
test('consulta 1.2m irrelevantes e 100k correspondentes com indice, limite e tempo pratico', async (t) => {
|
|
const { db, campaign } = setupCampaign();
|
|
const fixtureStartedAt = performance.now();
|
|
db.exec(`
|
|
WITH RECURSIVE sequence(value) AS (
|
|
VALUES(1)
|
|
UNION ALL
|
|
SELECT value + 1 FROM sequence WHERE value < 1200000
|
|
)
|
|
INSERT INTO leads
|
|
(dedup_key, empresa, whatsapp, segmento, prioridade, score, telefone_valido)
|
|
SELECT 'outside-' || value, 'Outside ' || value,
|
|
'5511' || printf('%08d', value), 'segmento_externo', 1, value % 100, 1
|
|
FROM sequence;
|
|
WITH RECURSIVE sequence(value) AS (
|
|
VALUES(1)
|
|
UNION ALL
|
|
SELECT value + 1 FROM sequence WHERE value < 100000
|
|
)
|
|
INSERT INTO leads
|
|
(dedup_key, empresa, whatsapp, segmento, prioridade, score, telefone_valido)
|
|
SELECT 'matching-' || value, 'Matching ' || value,
|
|
'5575' || printf('%08d', value), 'pizzarias', 1 + (value % 4), value % 101, 1
|
|
FROM sequence;
|
|
`);
|
|
const fixtureElapsedMs = performance.now() - fixtureStartedAt;
|
|
const { tracedDb, trace } = traceCandidateQuery(db);
|
|
const startedAt = performance.now();
|
|
const summary = await prepareDailyQueue({
|
|
db: tracedDb,
|
|
whatsapp: noSendWhatsapp(),
|
|
campaignId: campaign.id,
|
|
now: new Date('2026-07-29T14:00:00-03:00')
|
|
});
|
|
const elapsedMs = performance.now() - startedAt;
|
|
|
|
assert.equal(summary.checked, 15);
|
|
assert.ok(trace.sql, 'consulta real de candidatos nao foi observada');
|
|
const plan = db.prepare(`EXPLAIN QUERY PLAN ${trace.sql}`).all(...trace.args)
|
|
.map((row) => row.detail).join('\n');
|
|
assert.match(plan, /idx_leads_campaign_order/i);
|
|
assert.match(trace.sql, /LIMIT\s+\?/i);
|
|
assert.ok(trace.args.at(-1) <= 25);
|
|
assert.ok(elapsedMs < 8_000, `consulta limitada levou ${elapsedMs.toFixed(1)}ms`);
|
|
t.diagnostic(`fixture_1_3m_ms=${fixtureElapsedMs.toFixed(1)} candidate_query_ms=${elapsedMs.toFixed(1)}`);
|
|
db.close();
|
|
});
|
|
|
|
test('exclui envios bem-sucedidos, desconhecidos e respostas inbound explicitas', async () => {
|
|
const { db, campaign } = setupCampaign();
|
|
const delivered = insertLead(db, { key: 'sent-delivered', whatsapp: '557599550001', score: 100 });
|
|
const unknown = insertLead(db, { key: 'sent-unknown', whatsapp: '557599550002', score: 99 });
|
|
const inboundLead = insertLead(db, { key: 'inbound-lead', whatsapp: '557599550003', score: 98 });
|
|
const inboundEquivalent = insertLead(db, { key: 'inbound-equivalent', whatsapp: '5575999550004', score: 97 });
|
|
const eligible = insertLead(db, { key: 'eligibility-control', whatsapp: '557599550005', score: 96 });
|
|
for (const numero of ['557599550001', '557599550002', '557599550003', '5575999550004', '557599550005']) {
|
|
confirmed(db, numero);
|
|
}
|
|
db.prepare(
|
|
"INSERT INTO outreach_sends (lead_id, produto, numero, mensagem_enviada, status) VALUES (?, 'G-Delivery', ?, 'anterior', ?)"
|
|
).run(delivered, '557599550001', 'entregue');
|
|
db.prepare(
|
|
"INSERT INTO outreach_sends (lead_id, produto, numero, mensagem_enviada, status) VALUES (?, 'G-Delivery', ?, 'anterior', ?)"
|
|
).run(unknown, '557599550002', 'desconhecido');
|
|
db.prepare(
|
|
"INSERT INTO outreach_inbound_replies (provider_message_id, lead_id, numero, received_at) VALUES ('inbound-by-lead', ?, '557599550003', '2026-07-29T12:00:00Z')"
|
|
).run(inboundLead);
|
|
db.prepare(
|
|
"INSERT INTO outreach_inbound_replies (provider_message_id, numero, received_at) VALUES ('inbound-by-number', '557599550004', '2026-07-29T12:01:00Z')"
|
|
).run();
|
|
|
|
const summary = await prepareDailyQueue({
|
|
db,
|
|
whatsapp: noSendWhatsapp(async () => { throw new Error('todos os numeros possuem cache'); }),
|
|
campaignId: campaign.id,
|
|
now: new Date('2026-07-29T14:00:00-03:00')
|
|
});
|
|
const queue = listCampaignQueue(db, campaign.id);
|
|
|
|
assert.deepEqual(summary, { checked: 0, previewed: 1, skipped: 0 });
|
|
assert.deepEqual(queue.map(({ lead_id }) => lead_id), [eligible]);
|
|
assert.equal(queue.some(({ lead_id }) => [delivered, unknown, inboundLead, inboundEquivalent].includes(lead_id)), false);
|
|
db.close();
|
|
});
|
|
|
|
test('usa somente a lista de segmentos configurada na campanha', async () => {
|
|
const { db, campaign } = setupCampaign();
|
|
db.prepare("UPDATE outreach_campaigns SET segments_json = '[\"pizzarias\"]' WHERE id = ?").run(campaign.id);
|
|
const configured = insertLead(db, { key: 'configured-segment', whatsapp: '557599660001', segmento: 'pizzarias' });
|
|
const catalogButRemoved = insertLead(db, { key: 'removed-segment', whatsapp: '557599660002', segmento: 'hamburguerias' });
|
|
confirmed(db, '557599660001');
|
|
confirmed(db, '557599660002');
|
|
|
|
await prepareDailyQueue({
|
|
db,
|
|
whatsapp: noSendWhatsapp(async () => { throw new Error('todos os numeros possuem cache'); }),
|
|
campaignId: campaign.id,
|
|
now: new Date('2026-07-29T14:00:00-03:00')
|
|
});
|
|
|
|
assert.deepEqual(listCampaignQueue(db, campaign.id).map(({ lead_id }) => lead_id), [configured]);
|
|
assert.notEqual(configured, catalogButRemoved);
|
|
db.close();
|
|
});
|