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:
@@ -63,6 +63,15 @@ function candidateQuery(confirmed) {
|
|||||||
SELECT 1 FROM outreach_opt_outs opt_out
|
SELECT 1 FROM outreach_opt_outs opt_out
|
||||||
WHERE opt_out.numero IN (${leadNumber}, ${leadEquivalent})
|
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 (
|
AND NOT EXISTS (
|
||||||
SELECT 1 FROM outreach_inbound_replies replies
|
SELECT 1 FROM outreach_inbound_replies replies
|
||||||
WHERE replies.lead_id = leads.id
|
WHERE replies.lead_id = leads.id
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import os from 'node:os';
|
|||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import {
|
import {
|
||||||
addOptOut,
|
addOptOut,
|
||||||
|
addSharedNumberOverride,
|
||||||
openDb,
|
openDb,
|
||||||
saveWhatsAppVerification
|
saveWhatsAppVerification
|
||||||
} from '../src/db.mjs';
|
} from '../src/db.mjs';
|
||||||
@@ -304,6 +305,7 @@ test('persiste erro temporario e nao repete equivalente na mesma preparacao', as
|
|||||||
const { db, campaign } = setupCampaign();
|
const { db, campaign } = setupCampaign();
|
||||||
insertLead(db, { key: 'temporary-a', whatsapp: '5575999091780', score: 100 });
|
insertLead(db, { key: 'temporary-a', whatsapp: '5575999091780', score: 100 });
|
||||||
insertLead(db, { key: 'temporary-b', whatsapp: '557599091780', score: 90 });
|
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 calls = [];
|
||||||
const whatsapp = noSendWhatsapp(async (numero) => {
|
const whatsapp = noSendWhatsapp(async (numero) => {
|
||||||
calls.push(numero);
|
calls.push(numero);
|
||||||
@@ -544,3 +546,50 @@ test('usa somente a lista de segmentos configurada na campanha', async () => {
|
|||||||
assert.notEqual(configured, catalogButRemoved);
|
assert.notEqual(configured, catalogButRemoved);
|
||||||
db.close();
|
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();
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user