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.
This commit is contained in:
Gustavo
2026-07-31 11:18:41 -03:00
parent 3d2144eefc
commit d85fb12991
2 changed files with 58 additions and 0 deletions
+9
View File
@@ -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
+49
View File
@@ -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();
});