candidateQuery now blocks a lead whose WhatsApp number is shared by 2+ leads, unless a shared-number override exists. The inner correlated subquery compares the bare other.whatsapp column (not normalizedSql()) against the outer row's two computed number forms so SQLite can use idx_leads_whatsapp instead of a full table scan, keeping the 1.3M-row perf test within budget. Also updates the pre-existing 'persiste erro temporario e nao repete equivalente' test: its two leads use 8/9-digit equivalent numbers, which the new filter now blocks by default, so an override is added to keep exercising the intended equivalent-number dedup behavior.
596 lines
22 KiB
JavaScript
596 lines
22 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,
|
|
addSharedNumberOverride,
|
|
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 });
|
|
addSharedNumberOverride(db, '5575999091780', 'numeros equivalentes usados apenas para testar o dedup de verificacao');
|
|
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();
|
|
});
|
|
|
|
test('numero compartilhado por mais de um lead fica fora da fila ate liberacao manual', async () => {
|
|
const { db, campaign } = setupCampaign();
|
|
const sharedA = insertLead(db, { key: 'shared-a', empresa: 'Contabilidade Cliente A', whatsapp: '5575999091780', score: 100 });
|
|
const sharedB = insertLead(db, { key: 'shared-b', empresa: 'Contabilidade Cliente B', whatsapp: '5575999091780', score: 90 });
|
|
const solo = insertLead(db, { key: 'solo', whatsapp: '5575992220000', score: 80 });
|
|
for (const numero of ['5575999091780', '5575992220000']) confirmed(db, numero);
|
|
|
|
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')
|
|
});
|
|
let queue = listCampaignQueue(db, campaign.id);
|
|
assert.deepEqual(queue.map(({ lead_id }) => lead_id), [solo]);
|
|
assert.equal(queue.some(({ lead_id }) => [sharedA, sharedB].includes(lead_id)), false);
|
|
|
|
addSharedNumberOverride(db, '5575999091780', 'confirmado contato direto');
|
|
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')
|
|
});
|
|
queue = listCampaignQueue(db, campaign.id);
|
|
assert.deepEqual(queue.map(({ lead_id }) => lead_id).sort((a, b) => a - b), [sharedA, sharedB, solo].sort((a, b) => a - b));
|
|
db.close();
|
|
});
|
|
|
|
test('variantes 8 e 9 digitos do mesmo numero contam como compartilhado na fila', async () => {
|
|
const { db, campaign } = setupCampaign();
|
|
const nineDigits = insertLead(db, { key: 'nine-digits', whatsapp: '5575999091780', score: 100 });
|
|
const eightDigits = insertLead(db, { key: 'eight-digits', whatsapp: '557599091780', score: 90 });
|
|
confirmed(db, '5575999091780');
|
|
confirmed(db, '557599091780');
|
|
|
|
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.equal(queue.some(({ lead_id }) => [nineDigits, eightDigits].includes(lead_id)), false);
|
|
db.close();
|
|
});
|