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.
Implement shared-number detection in reserveOutreachSend: reject manual WhatsApp sends with HTTP 409 when the destination number is shared across 2+ leads and hasn't been explicitly released via override.
- Add test: reserveOutreachSend bloqueia numero compartilhado sem override e libera com override
- Add check in reserveOutreachSend right after isOptedOut check
- Uses isNumberSharedAcrossLeads and isSharedNumberOverridden from Task 1
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The v15 test pattern (delete migration records >= 15, drop schema, reopen to replay)
was failing because outreach_shared_number_overrides table from v19 migration was not
dropped before replaying. Add DROP TABLE IF EXISTS to the schema cleanup block to
match the same fix applied to v17 test.
This resolves the 'table already exists' error during v19 migration replay.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- 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>
Tracks the app that was already in place (capture, treatment, outreach,
WhatsApp integration, server, frontend, tests, docs) so future feature
work can branch and use isolated worktrees.