mirror of
https://github.com/Luchexx/wacrm.git
synced 2026-11-02 09:19:51 +00:00
Root cause (error 23503):
broadcast_recipients.contact_id and deals.contact_id were declared
NOT NULL REFERENCES contacts(id) with no ON DELETE clause, so
Postgres defaulted to NO ACTION. Any contact who'd ever received a
broadcast or been attached to a deal became un-deletable with
"violates foreign key constraint broadcast_recipients_contact_id_fkey".
Fix (migration 004):
Relax both FKs to ON DELETE SET NULL + drop NOT NULL on both
columns. History rows survive with contact_id = NULL. The UI is
already null-safe:
- broadcast detail: contact?.name ?? 'Unknown'
- deal card: contact?.name || contact?.phone || 'No contact'
- CSV export: contact?.name ?? ''
Why not CASCADE:
CASCADE would wipe broadcast recipient rows and trigger the
aggregate recount on broadcasts (migration 003), retroactively
changing sent_count / delivered_count / read_count. It would also
delete deal history. SET NULL preserves both counts and history.
Type/code:
- Deal.contact_id + BroadcastRecipient.contact_id widen to
string | null
- DealForm: setContactId(deal.contact_id ?? "") — the contact
picker uses "" to mean unselected
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
66 lines
2.3 KiB
SQL
66 lines
2.3 KiB
SQL
-- ============================================================
|
|
-- Allow contact deletion without wiping history.
|
|
--
|
|
-- broadcast_recipients.contact_id and deals.contact_id were declared
|
|
-- NOT NULL REFERENCES contacts(id) with no ON DELETE action, so
|
|
-- Postgres defaults to NO ACTION. The first time a user tried to
|
|
-- delete a contact that had ever received a broadcast or been
|
|
-- attached to a deal, the delete failed with:
|
|
--
|
|
-- ERROR 23503: update or delete on table "contacts" violates
|
|
-- foreign key constraint ... on table <other>
|
|
--
|
|
-- CASCADE is the wrong fix — it would silently wipe historical
|
|
-- broadcast recipient rows (breaking audit + retroactively moving
|
|
-- broadcasts.sent_count / delivered_count / read_count etc. via the
|
|
-- aggregate trigger) and deal rows.
|
|
--
|
|
-- SET NULL is the right fix: history rows survive with a NULL
|
|
-- contact_id. The UI is already null-safe (contact?.name ?? 'Unknown',
|
|
-- contact?.phone, etc.).
|
|
--
|
|
-- Idempotent — safe to run multiple times.
|
|
-- ============================================================
|
|
|
|
-- ── broadcast_recipients.contact_id ────────────────────────────
|
|
ALTER TABLE broadcast_recipients
|
|
ALTER COLUMN contact_id DROP NOT NULL;
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (
|
|
SELECT 1 FROM pg_constraint
|
|
WHERE conname = 'broadcast_recipients_contact_id_fkey'
|
|
AND conrelid = 'broadcast_recipients'::regclass
|
|
) THEN
|
|
ALTER TABLE broadcast_recipients
|
|
DROP CONSTRAINT broadcast_recipients_contact_id_fkey;
|
|
END IF;
|
|
END $$;
|
|
|
|
ALTER TABLE broadcast_recipients
|
|
ADD CONSTRAINT broadcast_recipients_contact_id_fkey
|
|
FOREIGN KEY (contact_id) REFERENCES contacts(id)
|
|
ON DELETE SET NULL;
|
|
|
|
-- ── deals.contact_id ───────────────────────────────────────────
|
|
ALTER TABLE deals
|
|
ALTER COLUMN contact_id DROP NOT NULL;
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (
|
|
SELECT 1 FROM pg_constraint
|
|
WHERE conname = 'deals_contact_id_fkey'
|
|
AND conrelid = 'deals'::regclass
|
|
) THEN
|
|
ALTER TABLE deals
|
|
DROP CONSTRAINT deals_contact_id_fkey;
|
|
END IF;
|
|
END $$;
|
|
|
|
ALTER TABLE deals
|
|
ADD CONSTRAINT deals_contact_id_fkey
|
|
FOREIGN KEY (contact_id) REFERENCES contacts(id)
|
|
ON DELETE SET NULL;
|