From d85fb12991c6dd8133252a744740e7f6f189b071 Mon Sep 17 00:00:00 2001 From: Gustavo Date: Fri, 31 Jul 2026 11:18:41 -0300 Subject: [PATCH] Exclude leads sharing a WhatsApp number from the automatic campaign queue 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. --- src/outreach/campaign-queue.mjs | 9 ++++++ tests/campaign-queue.test.mjs | 49 +++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+) diff --git a/src/outreach/campaign-queue.mjs b/src/outreach/campaign-queue.mjs index ebb6df4..a91ea94 100644 --- a/src/outreach/campaign-queue.mjs +++ b/src/outreach/campaign-queue.mjs @@ -63,6 +63,15 @@ function candidateQuery(confirmed) { SELECT 1 FROM outreach_opt_outs opt_out WHERE opt_out.numero IN (${leadNumber}, ${leadEquivalent}) ) + AND ( + (SELECT COUNT(DISTINCT other.id) FROM leads other + WHERE other.whatsapp IN (${leadNumber}, ${leadEquivalent}) + ) < 2 + OR EXISTS ( + SELECT 1 FROM outreach_shared_number_overrides ov + WHERE ov.numero IN (${leadNumber}, ${leadEquivalent}) + ) + ) AND NOT EXISTS ( SELECT 1 FROM outreach_inbound_replies replies WHERE replies.lead_id = leads.id diff --git a/tests/campaign-queue.test.mjs b/tests/campaign-queue.test.mjs index 3bb86a0..b7ceef9 100644 --- a/tests/campaign-queue.test.mjs +++ b/tests/campaign-queue.test.mjs @@ -5,6 +5,7 @@ import os from 'node:os'; import path from 'node:path'; import { addOptOut, + addSharedNumberOverride, openDb, saveWhatsAppVerification } from '../src/db.mjs'; @@ -304,6 +305,7 @@ test('persiste erro temporario e nao repete equivalente na mesma preparacao', as 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); @@ -544,3 +546,50 @@ test('usa somente a lista de segmentos configurada na campanha', async () => { 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(); +});