Fix opt-out asymmetric 8/9-digit bypass letting sends reach blocked numbers

isOptedOut/addOptOut/removeOptOut used equivalentPhoneNumbers, which is not
symmetric for a 12-digit local outside the 6-9 range vs its 13-digit
counterpart (same root cause already fixed for shared-number detection).
An opt-out registered against one form of such a number silently failed to
block sends to a lead whose number was stored in the other form, on both
the manual send path (reserveOutreachSend) and, independently, in two
inlined checks that bypassed isOptedOut entirely: the last-mile recheck in
campaign-worker.mjs's reserveCampaignQueueSend, and the opt-out NOT EXISTS
clause in campaign-queue.mjs's candidate query. All three now use the
symmetric canonicalPhoneVariants key already introduced for shared-number
overrides, and the worker's last-mile check now calls isOptedOut directly
instead of re-deriving its own number list.

Found via a full-project audit; reproduced and verified before fixing.
This commit is contained in:
Gustavo
2026-07-31 22:04:19 -03:00
parent 29f9f3a905
commit 0c36f27cb3
6 changed files with 123 additions and 10 deletions
+28
View File
@@ -624,3 +624,31 @@ test('variantes 8 e 9 digitos do mesmo numero contam como compartilhado na fila'
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();
}
});