diff --git a/init-scripts/init-script.sql b/init-scripts/init-script.sql index a85aea4..651bbe0 100644 --- a/init-scripts/init-script.sql +++ b/init-scripts/init-script.sql @@ -196,7 +196,7 @@ CREATE INDEX idx_patch_tests_name ON patch_tests(name); CREATE TABLE user_patch_tests ( 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, tested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (user_id, patch_test_id) @@ -264,11 +264,11 @@ CREATE TABLE booking_services ( CREATE TABLE booking_edit_requests ( id CHAR(12) PRIMARY KEY DEFAULT generate_booking_id(), 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_services CHAR(12)[] DEFAULT '{}', -- Array of service IDs to replace booking_services + new_services CHAR(12)[] DEFAULT '{}', 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(), CONSTRAINT chk_at_least_one_field CHECK ( 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 TABLE user_referrals ( - referrer_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE, - referred_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + id BIGSERIAL PRIMARY KEY, + 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(), 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 ( 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, status VARCHAR(20) NOT NULL DEFAULT 'pending', 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 ( id CHAR(12) PRIMARY KEY DEFAULT generate_short_id('booking_discounts'), 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, source_id CHAR(12), campaign_type campaign_type, @@ -642,7 +643,7 @@ BEGIN -- Soft-delete all saved cards and clear PCI data UPDATE user_saved_cards SET deleted_at = NOW(), - retained_until = NOW(), + retained_until = NOW() + INTERVAL '7 years', last_4 = 'XXXX', fingerprint = NULL, exp_month = 1, @@ -668,6 +669,16 @@ BEGIN -- Clear notification preferences (no contractual basis after account closure) 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; $$ LANGUAGE plpgsql; @@ -675,9 +686,21 @@ $$ LANGUAGE plpgsql; -- WHY: Guest accounts have no ongoing contractual or legal basis for retention -- WHEN: User requests deletion or GDPR cleanup -- 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)) RETURNS VOID AS $$ 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'; END; $$ LANGUAGE plpgsql; @@ -1457,7 +1480,7 @@ $$ LANGUAGE plpgsql; CREATE TABLE user_saved_cards ( 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, brand TEXT NOT NULL, last_4 TEXT NOT NULL,