Merge pull request #28 from ArnasDon/fix/contact-deletion-fk-constraints

fix(contacts): allow deletion — SET NULL on broadcast_recipients + deals FKs
This commit is contained in:
Arnas Donauskas 2026-04-17 16:58:42 +03:00 committed by GitHub
commit 6bae86bd66
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 79 additions and 3 deletions

View File

@ -79,7 +79,9 @@ export function DealForm({
setTitle(deal.title);
setValue(String(deal.value ?? ""));
setCurrency(deal.currency || "USD");
setContactId(deal.contact_id);
// contact_id is nullable when the contact has been deleted
// (migration 004: ON DELETE SET NULL). "" means "no selection".
setContactId(deal.contact_id ?? "");
setStageId(deal.stage_id);
setAssignedTo(deal.assigned_to ?? "");
setExpectedCloseDate(deal.expected_close_date ?? "");

View File

@ -141,7 +141,11 @@ export interface Deal {
user_id: string;
pipeline_id: string;
stage_id: string;
contact_id: string;
/**
* Nullable after migration 004 — becomes NULL when the referenced
* contact is deleted (ON DELETE SET NULL). History preserved.
*/
contact_id: string | null;
conversation_id?: string;
assigned_to?: string;
title: string;
@ -182,7 +186,12 @@ export interface Broadcast {
export interface BroadcastRecipient {
id: string;
broadcast_id: string;
contact_id: string;
/**
* Nullable after migration 004 — becomes NULL when the referenced
* contact is deleted (ON DELETE SET NULL). History preserved; the
* UI renders "Unknown" for orphaned rows.
*/
contact_id: string | null;
status: RecipientStatus;
sent_at?: string;
delivered_at?: string;

View File

@ -0,0 +1,65 @@
-- ============================================================
-- 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;