Add shared-number detection and override data layer
- Add migration v19: creates outreach_shared_number_overrides table - Implement 7 new data-layer functions: - leadsSharingNumber: find leads sharing a phone number variant - isNumberSharedAcrossLeads: detect if number is shared by 2+ leads - getSharedNumberOverride: retrieve override entry for a number - isSharedNumberOverridden: check if number has an override - addSharedNumberOverride: create or update override with reason - removeSharedNumberOverride: delete override record - listSharedNumberGroups: group leads by shared numbers, reflect override state - Add 4 comprehensive tests covering migration, detection, override crud, and grouping - Update version assertions from 18 to 19 - Fix v17 migration test to properly clean up v19 table for schema reset Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
d330bb5ff0
commit
fe70436dbb
+68
-3
@@ -58,7 +58,7 @@ test('openDb cria esquema e aplica migrations uma unica vez', () => {
|
||||
]) {
|
||||
assert.ok(tables.includes(table), `tabela ${table} ausente`);
|
||||
}
|
||||
assert.equal(version, 18);
|
||||
assert.equal(version, 19);
|
||||
for (const table of ['capture_runs', 'extractions']) {
|
||||
const columns = db.prepare(`PRAGMA table_info(${table})`).all().map((row) => row.name);
|
||||
assert.ok(columns.includes('owner_token'));
|
||||
@@ -69,6 +69,68 @@ test('openDb cria esquema e aplica migrations uma unica vez', () => {
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('migracao v19 cria tabela de overrides de numero compartilhado', () => {
|
||||
const db = openDb(':memory:');
|
||||
assert.equal(db.prepare('SELECT MAX(version) v FROM schema_migrations').get().v, 19);
|
||||
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map((r) => r.name);
|
||||
assert.ok(tables.includes('outreach_shared_number_overrides'));
|
||||
const columns = db.prepare('PRAGMA table_info(outreach_shared_number_overrides)').all().map((r) => r.name);
|
||||
assert.deepEqual(columns, ['numero', 'motivo', 'created_at']);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('isNumberSharedAcrossLeads detecta duplicidade exata e por variante 8/9 digitos', () => {
|
||||
const db = openDb(':memory:');
|
||||
const insert = createLeadInserter(db);
|
||||
insert(sampleLead({ dedup_key: 'shared-a', empresa: 'Empresa A', whatsapp: '5575999091780' }));
|
||||
insert(sampleLead({ dedup_key: 'shared-b', empresa: 'Empresa B', whatsapp: '557599091780' }));
|
||||
insert(sampleLead({ dedup_key: 'solo', empresa: 'Empresa C', whatsapp: '5511999990001' }));
|
||||
|
||||
assert.equal(database.isNumberSharedAcrossLeads(db, '5575999091780'), true);
|
||||
assert.equal(database.isNumberSharedAcrossLeads(db, '557599091780'), true);
|
||||
assert.equal(database.isNumberSharedAcrossLeads(db, '5511999990001'), false);
|
||||
const sharing = database.leadsSharingNumber(db, '5575999091780');
|
||||
assert.deepEqual(sharing.map((l) => l.empresa).sort(), ['Empresa A', 'Empresa B']);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('override libera e remover override rebloqueia numero compartilhado', () => {
|
||||
const db = openDb(':memory:');
|
||||
const insert = createLeadInserter(db);
|
||||
insert(sampleLead({ dedup_key: 'shared-a', empresa: 'Empresa A', whatsapp: '5575999091780' }));
|
||||
insert(sampleLead({ dedup_key: 'shared-b', empresa: 'Empresa B', whatsapp: '5575999091780' }));
|
||||
|
||||
assert.equal(database.isSharedNumberOverridden(db, '5575999091780'), false);
|
||||
const override = database.addSharedNumberOverride(db, '5575999091780', 'confirmado contato direto');
|
||||
assert.equal(override.numero, '5575999091780');
|
||||
assert.equal(override.motivo, 'confirmado contato direto');
|
||||
assert.equal(database.isSharedNumberOverridden(db, '5575999091780'), true);
|
||||
|
||||
assert.equal(database.removeSharedNumberOverride(db, '5575999091780'), true);
|
||||
assert.equal(database.isSharedNumberOverridden(db, '5575999091780'), false);
|
||||
db.close();
|
||||
});
|
||||
|
||||
test('listSharedNumberGroups agrupa por equivalencia e reflete override', () => {
|
||||
const db = openDb(':memory:');
|
||||
const insert = createLeadInserter(db);
|
||||
insert(sampleLead({ dedup_key: 'shared-a', empresa: 'Contabilidade Cliente A', whatsapp: '5575999091780' }));
|
||||
insert(sampleLead({ dedup_key: 'shared-b', empresa: 'Contabilidade Cliente B', whatsapp: '557599091780' }));
|
||||
insert(sampleLead({ dedup_key: 'solo', empresa: 'Empresa Unica', whatsapp: '5511999990001' }));
|
||||
|
||||
let groups = database.listSharedNumberGroups(db);
|
||||
assert.equal(groups.length, 1);
|
||||
assert.equal(groups[0].leads.length, 2);
|
||||
assert.deepEqual(groups[0].leads.map((l) => l.empresa).sort(), ['Contabilidade Cliente A', 'Contabilidade Cliente B']);
|
||||
assert.equal(groups[0].liberado, false);
|
||||
|
||||
database.addSharedNumberOverride(db, '5575999091780', 'revisado');
|
||||
groups = database.listSharedNumberGroups(db);
|
||||
assert.equal(groups[0].liberado, true);
|
||||
assert.equal(groups[0].motivo, 'revisado');
|
||||
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');
|
||||
@@ -94,7 +156,7 @@ test('v15 preserva linhas legadas ao migrar uma base populada em v14', () => {
|
||||
db = null;
|
||||
|
||||
db = openDb(dbPath);
|
||||
assert.equal(db.prepare('SELECT MAX(version) version FROM schema_migrations').get().version, 18);
|
||||
assert.equal(db.prepare('SELECT MAX(version) version FROM schema_migrations').get().version, 19);
|
||||
assert.deepEqual({ ...db.prepare('SELECT dedup_key, empresa FROM leads').get() }, {
|
||||
dedup_key: 'legacy-v14',
|
||||
empresa: 'Empresa Legada'
|
||||
@@ -152,6 +214,7 @@ test('v17 corrige trigger v16 ja aplicada sem bloquear agendamento legitimo', ()
|
||||
|
||||
db.exec(`
|
||||
DELETE FROM schema_migrations WHERE version > 16;
|
||||
DROP TABLE IF EXISTS outreach_shared_number_overrides;
|
||||
DROP TRIGGER outreach_campaign_queue_payload_immutable;
|
||||
CREATE TRIGGER outreach_campaign_queue_payload_immutable
|
||||
BEFORE UPDATE OF variant_id, rendered_message, scheduled_at ON outreach_campaign_queue
|
||||
@@ -186,7 +249,7 @@ test('v17 corrige trigger v16 ja aplicada sem bloquear agendamento legitimo', ()
|
||||
assert.doesNotThrow(() => db.prepare(
|
||||
"UPDATE outreach_campaign_queue SET status = 'scheduled', scheduled_at = ? WHERE id = ?"
|
||||
).run('2026-07-30T09:30:00.000Z', queueId));
|
||||
assert.equal(db.prepare('SELECT MAX(version) version FROM schema_migrations').get().version, 18);
|
||||
assert.equal(db.prepare('SELECT MAX(version) version FROM schema_migrations').get().version, 19);
|
||||
} finally {
|
||||
db.close();
|
||||
}
|
||||
@@ -688,6 +751,7 @@ test('reset apaga outreach antes de leads e preserva opt-outs', () => {
|
||||
db.prepare("INSERT INTO outreach_stage_events (lead_id, campaign_id, stage) VALUES (1, 1, 'respondeu')").run();
|
||||
db.prepare("INSERT INTO outreach_appointments (lead_id, campaign_id, kind) VALUES (1, 1, 'demonstracao')").run();
|
||||
db.prepare("INSERT INTO outreach_opt_outs (numero) VALUES ('5511999990001')").run();
|
||||
db.prepare("INSERT INTO outreach_shared_number_overrides (numero, motivo) VALUES ('5511999990002', 'revisado')").run();
|
||||
resetLeadsBase(db);
|
||||
assert.equal(db.prepare('SELECT COUNT(*) total FROM outreach_sends').get().total, 0);
|
||||
assert.equal(db.prepare('SELECT COUNT(*) total FROM leads').get().total, 0);
|
||||
@@ -702,6 +766,7 @@ test('reset apaga outreach antes de leads e preserva opt-outs', () => {
|
||||
}
|
||||
assert.equal(db.prepare('SELECT COUNT(*) total FROM outreach_campaigns').get().total, 1);
|
||||
assert.equal(db.prepare('SELECT COUNT(*) total FROM outreach_opt_outs').get().total, 1);
|
||||
assert.equal(db.prepare('SELECT COUNT(*) total FROM outreach_shared_number_overrides').get().total, 1);
|
||||
db.close();
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user