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.
121 lines
4.9 KiB
JavaScript
121 lines
4.9 KiB
JavaScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { DatabaseSync } from 'node:sqlite';
|
|
import {
|
|
canonicalKeySql,
|
|
canonicalPhoneKey,
|
|
canonicalPhoneVariants,
|
|
canonicalVariantSql,
|
|
equivalentPhoneNumbers,
|
|
equivalentSql,
|
|
normalizePhone,
|
|
normalizedSql
|
|
} from '../src/phone.mjs';
|
|
|
|
// A regra 8/9 digitos existe duas vezes: em JS (equivalentPhoneNumbers /
|
|
// canonicalPhoneKey, usadas pelo envio manual e pela revalidacao de ultima
|
|
// milha) e em SQL (equivalentSql / canonicalKeySql, usadas pela selecao de
|
|
// candidatos da campanha e pela listagem de tratamento). Se alguem editar uma
|
|
// sem a outra, o bloqueio automatico e o manual passam a discordar em silencio.
|
|
// Estes testes rodam o mesmo conjunto de numeros pelos dois caminhos.
|
|
const NUMBERS = [
|
|
'5575999091780', // celular BR de 13 digitos com nono digito
|
|
'557599091780', // mesma linha em 8 digitos (local comeca com 9)
|
|
'5571993335451', // outro celular de 13 digitos
|
|
'557193335451', // fixo/8 digitos que comeca com 9 -> tem par
|
|
'557133334444', // fixo de 12 digitos, local comeca com 3 (sem par direto)
|
|
'5575933334444', // par assimetrico do numero abaixo
|
|
'557533334444', // 12 digitos, local '33334444' fora da faixa 6-9
|
|
'551140028922', // 12 digitos, local comeca com 4
|
|
'557561234567', // 12 digitos, local comeca com 6 (limite inferior da faixa)
|
|
'557551234567', // 12 digitos, local comeca com 5 (logo abaixo da faixa)
|
|
'557591234567', // 12 digitos, local comeca com 9 (limite superior da faixa)
|
|
'5511987654321', // 13 digitos comum
|
|
'11987654321', // sem DDI 55
|
|
'5575', // curto demais
|
|
'' // vazio
|
|
];
|
|
|
|
function sqlDb() {
|
|
const db = new DatabaseSync(':memory:');
|
|
db.exec('CREATE TABLE amostra (bruto TEXT)');
|
|
return db;
|
|
}
|
|
|
|
function sqlValues(db, numero) {
|
|
db.prepare('DELETE FROM amostra').run();
|
|
db.prepare('INSERT INTO amostra (bruto) VALUES (?)').run(numero);
|
|
return db.prepare(
|
|
`SELECT ${normalizedSql('bruto')} AS normalizado,
|
|
${equivalentSql('bruto')} AS equivalente,
|
|
${canonicalKeySql('bruto')} AS canonico,
|
|
${canonicalVariantSql('bruto')} AS variante
|
|
FROM amostra`
|
|
).get();
|
|
}
|
|
|
|
test('normalizedSql concorda com normalizePhone', () => {
|
|
const db = sqlDb();
|
|
for (const numero of [...NUMBERS, '+55 (75) 99909-1780', '55 71 9333-5451']) {
|
|
assert.equal(sqlValues(db, numero).normalizado, normalizePhone(numero), numero);
|
|
}
|
|
db.close();
|
|
});
|
|
|
|
test('equivalentSql concorda com equivalentPhoneNumbers em todas as amostras', () => {
|
|
const db = sqlDb();
|
|
for (const numero of NUMBERS) {
|
|
const equivalentes = equivalentPhoneNumbers(numero);
|
|
// equivalentPhoneNumbers devolve [proprio, alternativo?]; equivalentSql
|
|
// devolve apenas o "outro lado" (ou o proprio quando nao ha alternativa).
|
|
const esperado = equivalentes.length > 1 ? equivalentes[1] : normalizePhone(numero);
|
|
assert.equal(sqlValues(db, numero).equivalente, esperado, numero);
|
|
}
|
|
db.close();
|
|
});
|
|
|
|
test('canonicalKeySql concorda com canonicalPhoneKey em todas as amostras', () => {
|
|
const db = sqlDb();
|
|
for (const numero of NUMBERS) {
|
|
assert.equal(sqlValues(db, numero).canonico, canonicalPhoneKey(numero), numero);
|
|
}
|
|
db.close();
|
|
});
|
|
|
|
test('canonicalKeySql + canonicalVariantSql cobrem o mesmo conjunto de canonicalPhoneVariants', () => {
|
|
const db = sqlDb();
|
|
for (const numero of NUMBERS) {
|
|
const { canonico, variante } = sqlValues(db, numero);
|
|
const sqlSet = [...new Set([canonico, variante].filter(Boolean))].sort();
|
|
const jsSet = [...new Set(canonicalPhoneVariants(numero))].sort();
|
|
assert.deepEqual(sqlSet, jsSet, numero);
|
|
}
|
|
db.close();
|
|
});
|
|
|
|
test('a chave canonica e simetrica onde equivalentPhoneNumbers nao e', () => {
|
|
// Caso concreto: A tem 12 digitos com local fora da faixa 6-9, B tem 13.
|
|
assert.deepEqual(equivalentPhoneNumbers('557533334444'), ['557533334444']);
|
|
assert.deepEqual(equivalentPhoneNumbers('5575933334444'), ['5575933334444', '557533334444']);
|
|
assert.equal(canonicalPhoneKey('557533334444'), '557533334444');
|
|
assert.equal(canonicalPhoneKey('5575933334444'), '557533334444');
|
|
|
|
// E os pares 8/9 digitos ja cobertos pela suite continuam batendo entre si.
|
|
assert.equal(canonicalPhoneKey('5575999091780'), canonicalPhoneKey('557599091780'));
|
|
assert.equal(canonicalPhoneKey('5571993335451'), canonicalPhoneKey('557193335451'));
|
|
|
|
// Simetria total do conjunto de casamento nas amostras brasileiras.
|
|
for (const numero of NUMBERS) {
|
|
for (const outro of canonicalPhoneVariants(numero)) {
|
|
assert.ok(
|
|
canonicalPhoneVariants(outro).includes(normalizePhone(numero)),
|
|
`${numero} <-> ${outro}`
|
|
);
|
|
}
|
|
}
|
|
|
|
// Numeros de linhas diferentes nao podem colidir.
|
|
assert.notEqual(canonicalPhoneKey('557533334444'), canonicalPhoneKey('557133334444'));
|
|
assert.notEqual(canonicalPhoneKey('5511987654321'), canonicalPhoneKey('551140028922'));
|
|
});
|