Fix final-review findings: last-mile shared-number recheck, symmetric canonical key, per-row treatment query, JS/SQL agreement test

The last-mile validator in campaign-worker.mjs revalidated opt-out/inbound/
dedupe right before sending but never rechecked shared-number state, so
re-blocking a released number after scheduling didn't stop the send.

equivalentPhoneNumbers isn't symmetric for all 8/9-digit pairs, which made
isNumberSharedAcrossLeads and listSharedNumberGroups disagree for a 12-digit
local outside the 6-9 range vs its 13-digit counterpart: one side of the pair
could be enforcement-blocked while invisible and unreleasable in the review
screen. A new canonicalPhoneKey/canonicalPhoneVariants pair in phone.mjs
(plus SQL mirrors) makes the matching relation symmetric by construction.

listSharedNumberGroups did a full leads-table scan on every listLeads/getLead
call, the app's primary paginated screen. Replaced with per-row correlated
subqueries against idx_leads_whatsapp so cost tracks the page, not the table.

normalizedSql/equivalentSql (SQL) duplicated equivalentPhoneNumbers (JS) with
no test proving they agreed; moved into phone.mjs alongside their canonical
counterparts and added tests/phone.test.mjs asserting agreement across a
shared number set, including the asymmetric case.
This commit is contained in:
Gustavo
2026-07-31 20:31:31 -03:00
parent cb03597435
commit afa350de5e
10 changed files with 573 additions and 52 deletions
+42
View File
@@ -131,6 +131,48 @@ test('listSharedNumberGroups agrupa por equivalencia e reflete override', () =>
db.close();
});
// equivalentPhoneNumbers nao e simetrica: 5575933334444 expande para
// 557533334444 (remove o nono digito sem condicao), mas 557533334444 nao volta
// para 5575933334444 (o local '33334444' esta fora da faixa 6-9). O par
// precisa ser detectado, agrupado e bloqueado nos dois sentidos.
test('par 12/13 digitos assimetrico compartilha numero nos dois sentidos', () => {
const db = openDb(':memory:');
const insert = createLeadInserter(db);
insert(sampleLead({ dedup_key: 'assim-a', empresa: 'Empresa A', whatsapp: '557533334444' }));
insert(sampleLead({ dedup_key: 'assim-b', empresa: 'Empresa B', whatsapp: '5575933334444' }));
insert(sampleLead({ dedup_key: 'assim-solo', empresa: 'Empresa Solo', whatsapp: '5511999990001' }));
for (const numero of ['557533334444', '5575933334444']) {
assert.equal(database.isNumberSharedAcrossLeads(db, numero), true, numero);
assert.deepEqual(
database.leadsSharingNumber(db, numero).map((lead) => lead.empresa).sort(),
['Empresa A', 'Empresa B'],
numero
);
}
assert.equal(database.isNumberSharedAcrossLeads(db, '5511999990001'), false);
const groups = database.listSharedNumberGroups(db);
assert.equal(groups.length, 1);
assert.deepEqual(groups[0].leads.map((lead) => lead.empresa).sort(), ['Empresa A', 'Empresa B']);
// Envio manual bloqueado pelas duas formas, e liberado pelas duas assim que o
// operador revisa o grupo unico da tela de Configuracoes.
for (const numero of ['557533334444', '5575933334444']) {
assert.throws(
() => reserveOutreachSend(db, { numero, mensagem: 'oi' }),
(error) => error.statusCode === 409,
numero
);
}
database.addSharedNumberOverride(db, groups[0].numero, 'contabilidade confirmada');
for (const numero of ['557533334444', '5575933334444']) {
assert.equal(database.isSharedNumberOverridden(db, numero), true, numero);
assert.ok(reserveOutreachSend(db, { numero, mensagem: 'oi' }) > 0, numero);
}
db.close();
});
test('v15 preserva linhas legadas ao migrar uma base populada em v14', () => {
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'campaign-v15-'));
const dbPath = path.join(tempDir, 'legacy.sqlite');