fix(db): update FK constraints to SET NULL and expand delete_user anonymization
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -196,7 +196,7 @@ CREATE INDEX idx_patch_tests_name ON patch_tests(name);
|
|||||||
|
|
||||||
CREATE TABLE user_patch_tests (
|
CREATE TABLE user_patch_tests (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
user_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
user_id CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
patch_test_id CHAR(12) NOT NULL REFERENCES patch_tests(id) ON DELETE CASCADE,
|
patch_test_id CHAR(12) NOT NULL REFERENCES patch_tests(id) ON DELETE CASCADE,
|
||||||
tested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
tested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
UNIQUE (user_id, patch_test_id)
|
UNIQUE (user_id, patch_test_id)
|
||||||
@@ -264,11 +264,11 @@ CREATE TABLE booking_services (
|
|||||||
CREATE TABLE booking_edit_requests (
|
CREATE TABLE booking_edit_requests (
|
||||||
id CHAR(12) PRIMARY KEY DEFAULT generate_booking_id(),
|
id CHAR(12) PRIMARY KEY DEFAULT generate_booking_id(),
|
||||||
booking_id CHAR(12) NOT NULL REFERENCES bookings(id) ON DELETE CASCADE,
|
booking_id CHAR(12) NOT NULL REFERENCES bookings(id) ON DELETE CASCADE,
|
||||||
requested_by CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
requested_by CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
new_start_time TIMESTAMPTZ,
|
new_start_time TIMESTAMPTZ,
|
||||||
new_services CHAR(12)[] DEFAULT '{}', -- Array of service IDs to replace booking_services
|
new_services CHAR(12)[] DEFAULT '{}',
|
||||||
notes TEXT,
|
notes TEXT,
|
||||||
has_overrides BOOLEAN NOT NULL DEFAULT FALSE, -- If TRUE, cannot change services, use existing overrides for duration
|
has_overrides BOOLEAN NOT NULL DEFAULT FALSE,
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
CONSTRAINT chk_at_least_one_field CHECK (
|
CONSTRAINT chk_at_least_one_field CHECK (
|
||||||
new_start_time IS NOT NULL OR
|
new_start_time IS NOT NULL OR
|
||||||
@@ -280,11 +280,12 @@ CREATE TABLE booking_edit_requests (
|
|||||||
CREATE INDEX idx_booking_edit_requests_booking ON booking_edit_requests(booking_id);
|
CREATE INDEX idx_booking_edit_requests_booking ON booking_edit_requests(booking_id);
|
||||||
|
|
||||||
CREATE TABLE user_referrals (
|
CREATE TABLE user_referrals (
|
||||||
referrer_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
referred_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
referrer_id CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
|
referred_id CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
referred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
referred_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
claimed_booking_id CHAR(12) REFERENCES bookings(id) ON DELETE SET NULL,
|
claimed_booking_id CHAR(12) REFERENCES bookings(id) ON DELETE SET NULL,
|
||||||
PRIMARY KEY (referrer_id, referred_id)
|
UNIQUE (referrer_id, referred_id)
|
||||||
);
|
);
|
||||||
|
|
||||||
-- =======================================
|
-- =======================================
|
||||||
@@ -419,7 +420,7 @@ CREATE INDEX idx_payments_created_at_status ON payments(created_at, status);
|
|||||||
-- =======================================
|
-- =======================================
|
||||||
CREATE TABLE loyalty_redemptions (
|
CREATE TABLE loyalty_redemptions (
|
||||||
id CHAR(12) PRIMARY KEY DEFAULT generate_short_id('loyalty_redemptions'),
|
id CHAR(12) PRIMARY KEY DEFAULT generate_short_id('loyalty_redemptions'),
|
||||||
user_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
user_id CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
stamps_redeemed INT NOT NULL DEFAULT 10,
|
stamps_redeemed INT NOT NULL DEFAULT 10,
|
||||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||||
applied_to_booking_id CHAR(12) REFERENCES bookings(id) ON DELETE SET NULL,
|
applied_to_booking_id CHAR(12) REFERENCES bookings(id) ON DELETE SET NULL,
|
||||||
@@ -467,7 +468,7 @@ CREATE INDEX idx_discount_campaigns_type ON discount_campaigns(campaign_type);
|
|||||||
CREATE TABLE booking_discounts (
|
CREATE TABLE booking_discounts (
|
||||||
id CHAR(12) PRIMARY KEY DEFAULT generate_short_id('booking_discounts'),
|
id CHAR(12) PRIMARY KEY DEFAULT generate_short_id('booking_discounts'),
|
||||||
booking_id CHAR(12) NOT NULL REFERENCES bookings(id) ON DELETE CASCADE,
|
booking_id CHAR(12) NOT NULL REFERENCES bookings(id) ON DELETE CASCADE,
|
||||||
user_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
user_id CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
discount_source VARCHAR(30) NOT NULL,
|
discount_source VARCHAR(30) NOT NULL,
|
||||||
source_id CHAR(12),
|
source_id CHAR(12),
|
||||||
campaign_type campaign_type,
|
campaign_type campaign_type,
|
||||||
@@ -642,7 +643,7 @@ BEGIN
|
|||||||
-- Soft-delete all saved cards and clear PCI data
|
-- Soft-delete all saved cards and clear PCI data
|
||||||
UPDATE user_saved_cards
|
UPDATE user_saved_cards
|
||||||
SET deleted_at = NOW(),
|
SET deleted_at = NOW(),
|
||||||
retained_until = NOW(),
|
retained_until = NOW() + INTERVAL '7 years',
|
||||||
last_4 = 'XXXX',
|
last_4 = 'XXXX',
|
||||||
fingerprint = NULL,
|
fingerprint = NULL,
|
||||||
exp_month = 1,
|
exp_month = 1,
|
||||||
@@ -668,6 +669,16 @@ BEGIN
|
|||||||
|
|
||||||
-- Clear notification preferences (no contractual basis after account closure)
|
-- Clear notification preferences (no contractual basis after account closure)
|
||||||
DELETE FROM user_notification_preferences WHERE user_id = target_id;
|
DELETE FROM user_notification_preferences WHERE user_id = target_id;
|
||||||
|
|
||||||
|
-- Anonymize patch test records (medical-adjacent PII — unlink user, preserve test history)
|
||||||
|
UPDATE user_patch_tests SET user_id = NULL WHERE user_id = target_id;
|
||||||
|
|
||||||
|
-- Anonymize referral relationships (unlink this user's side, preserve the other party's record)
|
||||||
|
UPDATE user_referrals SET referrer_id = NULL WHERE referrer_id = target_id;
|
||||||
|
UPDATE user_referrals SET referred_id = NULL WHERE referred_id = target_id;
|
||||||
|
|
||||||
|
-- Anonymize admin notification references (not customer data — just drop the user link)
|
||||||
|
UPDATE admin_notifications SET user_id = NULL WHERE user_id = target_id;
|
||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
@@ -675,9 +686,21 @@ $$ LANGUAGE plpgsql;
|
|||||||
-- WHY: Guest accounts have no ongoing contractual or legal basis for retention
|
-- WHY: Guest accounts have no ongoing contractual or legal basis for retention
|
||||||
-- WHEN: User requests deletion or GDPR cleanup
|
-- WHEN: User requests deletion or GDPR cleanup
|
||||||
-- OUTPUT: Full removal of guest account from users table
|
-- OUTPUT: Full removal of guest account from users table
|
||||||
|
-- SAFETY: FK constraints are SET NULL on financial tables — records survive deletion.
|
||||||
|
-- Saved cards are soft-deleted with 7-year retention before unlinking.
|
||||||
CREATE OR REPLACE FUNCTION delete_guest_user(target_id CHAR(12))
|
CREATE OR REPLACE FUNCTION delete_guest_user(target_id CHAR(12))
|
||||||
RETURNS VOID AS $$
|
RETURNS VOID AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
|
UPDATE user_saved_cards
|
||||||
|
SET deleted_at = NOW(),
|
||||||
|
retained_until = NOW() + INTERVAL '7 years',
|
||||||
|
last_4 = 'XXXX',
|
||||||
|
fingerprint = NULL,
|
||||||
|
exp_month = 1,
|
||||||
|
exp_year = 2000,
|
||||||
|
user_id = NULL
|
||||||
|
WHERE user_id = target_id;
|
||||||
|
|
||||||
DELETE FROM users WHERE id = target_id AND account_role = 'guest';
|
DELETE FROM users WHERE id = target_id AND account_role = 'guest';
|
||||||
END;
|
END;
|
||||||
$$ LANGUAGE plpgsql;
|
$$ LANGUAGE plpgsql;
|
||||||
@@ -1457,7 +1480,7 @@ $$ LANGUAGE plpgsql;
|
|||||||
|
|
||||||
CREATE TABLE user_saved_cards (
|
CREATE TABLE user_saved_cards (
|
||||||
id CHAR(12) PRIMARY KEY DEFAULT generate_user_saved_card_id(),
|
id CHAR(12) PRIMARY KEY DEFAULT generate_user_saved_card_id(),
|
||||||
user_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
user_id CHAR(12) REFERENCES users(id) ON DELETE SET NULL,
|
||||||
square_card_id TEXT NOT NULL UNIQUE,
|
square_card_id TEXT NOT NULL UNIQUE,
|
||||||
brand TEXT NOT NULL,
|
brand TEXT NOT NULL,
|
||||||
last_4 TEXT NOT NULL,
|
last_4 TEXT NOT NULL,
|
||||||
|
|||||||
Reference in New Issue
Block a user