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(); }); // 557533334444 vs 5575933334444: equivalentPhoneNumbers so relaciona o segundo // com o primeiro. Os dois precisam ficar fora da fila. test('par 12/13 digitos assimetrico fica fora da fila nos dois sentidos', async () => { const { db, campaign } = setupCampaign(); const doze = insertLead(db, { key: 'assim-12', whatsapp: '557533334444', score: 100 }); const treze = insertLead(db, { key: 'assim-13', whatsapp: '5575933334444', score: 99 }); const solo = insertLead(db, { key: 'assim-solo', whatsapp: '5575992220000', score: 80 }); for (const numero of ['557533334444', '5575933334444', '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') }); assert.deepEqual(listCampaignQueue(db, campaign.id).map(({ lead_id }) => lead_id), [solo]); addSharedNumberOverride(db, '557533334444', 'contabilidade confirmada'); 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).sort((a, b) => a - b), [doze, treze, 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(); }); // Opt-out registrado na outra forma de um par 12/13 digitos assimetrico // precisa excluir o lead da fila nos dois sentidos (mesma logica de chave // canonica ja usada para numero compartilhado nesta consulta). test('opt-out na outra forma de um par 12/13 digitos assimetrico exclui o lead da fila', async () => { for (const [leadNumero, optOutNumero] of [ ['557533334444', '5575933334444'], ['5575933334444', '557533334444'] ]) { const { db, campaign } = setupCampaign(); const lead = insertLead(db, { key: 'assim-optout', whatsapp: leadNumero, score: 100 }); confirmed(db, leadNumero); addOptOut(db, optOutNumero, 'pediu para nao receber mais'); await prepareDailyQueue({ db, whatsapp: noSendWhatsapp(async () => { throw new Error('numero possui cache'); }), campaignId: campaign.id, now: new Date('2026-07-29T14:00:00-03:00') }); assert.equal( listCampaignQueue(db, campaign.id).some((row) => row.lead_id === lead), false, `${leadNumero} / opt-out em ${optOutNumero}` ); db.close(); } });