diff --git a/backend/handlers/scheduling/time_blockers_test.go b/backend/handlers/scheduling/time_blockers_test.go index 46dfd14..711199d 100644 --- a/backend/handlers/scheduling/time_blockers_test.go +++ b/backend/handlers/scheduling/time_blockers_test.go @@ -2688,16 +2688,12 @@ func TestCleanupExpiredGiftCards(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - // Make account_id nullable for this test — CleanupExpiredGiftCards inserts NULL + // account_id is nullable in the schema — CleanupExpiredGiftCards inserts NULL // for unredeemed gift cards (no user account to reference). - _, err := tx.Exec(ctx, `ALTER TABLE gift_card_expired_balances ALTER COLUMN account_id DROP NOT NULL`) - if err != nil { - t.Fatalf("failed to alter gift_card_expired_balances: %v", err) - } // Create expired gift card (unused for 25 months) var expiredCardID string - err = tx.QueryRow(ctx, ` + err := tx.QueryRow(ctx, ` INSERT INTO gift_cards (total_funds_added, amount_remaining, last_used_at) VALUES (100.00, 50.00, NOW() - INTERVAL '25 months') RETURNING id @@ -2759,16 +2755,9 @@ func TestCleanupExpiredGiftCards_SkipRecentlyUsed(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - // Make account_id nullable for this test — CleanupExpiredGiftCards inserts NULL - // for unredeemed gift cards (no user account to reference). - _, err := tx.Exec(ctx, `ALTER TABLE gift_card_expired_balances ALTER COLUMN account_id DROP NOT NULL`) - if err != nil { - t.Fatalf("failed to alter gift_card_expired_balances: %v", err) - } - // Create recently used gift card var recentCardID string - err = tx.QueryRow(ctx, ` + err := tx.QueryRow(ctx, ` INSERT INTO gift_cards (total_funds_added, amount_remaining, last_used_at) VALUES (100.00, 75.00, NOW()) RETURNING id @@ -2810,14 +2799,6 @@ func TestCleanupExpiredGiftCards_SkipRedeemed(t *testing.T) { t.Parallel() ctx, tx := resetTestData(t) - // Make account_id nullable for this test — CleanupExpiredGiftCards inserts NULL - // for unredeemed gift cards (no user account to reference). Even though this - // card is redeemed, the function may also match other cards; ensure schema allows it. - _, err := tx.Exec(ctx, `ALTER TABLE gift_card_expired_balances ALTER COLUMN account_id DROP NOT NULL`) - if err != nil { - t.Fatalf("failed to alter gift_card_expired_balances: %v", err) - } - // Create a user to be the redeemer userID, err := fixtures.CreateTestUser(tx) if err != nil { diff --git a/init-scripts/init-script.sql b/init-scripts/init-script.sql index f4a956a..7b3d737 100644 --- a/init-scripts/init-script.sql +++ b/init-scripts/init-script.sql @@ -255,26 +255,6 @@ CREATE INDEX IF NOT EXISTS idx_users_n_last_name_trgm ON users USING GIN (n_last -- Guest accounts can share emails; registered accounts cannot CREATE UNIQUE INDEX idx_users_email_registered ON users (email) WHERE account_role != 'guest'; --- ======================================= --- NAME HISTORY TABLE --- ======================================= --- WHY: Tracks user-initiated first/last name changes for display in booking modals --- and GDPR export. History is used to show "(formerly [old first] [old last])" --- on completed booking receipts and admin views. - -CREATE TABLE name_history ( - id CHAR(12) PRIMARY KEY DEFAULT generate_name_history_id(), - user_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE, - previous_first_name VARCHAR(50) NOT NULL, - previous_last_name VARCHAR(50) NOT NULL, - booking_id CHAR(12), - changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -); - -CREATE INDEX idx_name_history_user_id ON name_history(user_id); -CREATE INDEX idx_name_history_changed_at ON name_history(changed_at); -CREATE INDEX idx_name_history_user_active ON name_history(user_id) WHERE booking_id IS NULL; - -- ======================================= -- VERIFICATION CODES TABLE -- ======================================= @@ -395,8 +375,26 @@ CREATE INDEX idx_bookings_status_end_time ON bookings(status, end_time); CREATE INDEX IF NOT EXISTS idx_bookings_notes_trgm ON bookings USING GIN (notes gin_trgm_ops); CREATE INDEX IF NOT EXISTS idx_bookings_start_time ON bookings(start_time); -ALTER TABLE name_history ADD CONSTRAINT fk_name_history_booking_id - FOREIGN KEY (booking_id) REFERENCES bookings(id) ON DELETE SET NULL; +-- ======================================= +-- NAME HISTORY TABLE +-- ======================================= +-- WHY: Tracks user-initiated first/last name changes for display in booking modals +-- and GDPR export. History is used to show "(formerly [old first] [old last])" +-- on completed booking receipts and admin views. +-- NOTE: booking_id FK references bookings, so this table is created after bookings. + +CREATE TABLE name_history ( + id CHAR(12) PRIMARY KEY DEFAULT generate_name_history_id(), + user_id CHAR(12) NOT NULL REFERENCES users(id) ON DELETE CASCADE, + previous_first_name VARCHAR(50) NOT NULL, + previous_last_name VARCHAR(50) NOT NULL, + booking_id CHAR(12) CONSTRAINT fk_name_history_booking_id REFERENCES bookings(id) ON DELETE SET NULL, + changed_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +CREATE INDEX idx_name_history_user_id ON name_history(user_id); +CREATE INDEX idx_name_history_changed_at ON name_history(changed_at); +CREATE INDEX idx_name_history_user_active ON name_history(user_id) WHERE booking_id IS NULL; -- ======================================= -- BOOKING SERVICES JUNCTION TABLE @@ -842,15 +840,8 @@ INSERT INTO business_settings ( -- 'critical_payment_log' surfaces unresolved money events (stale pending -- payments/till sales, refunds at the retry cap) in the admin notification -- centre — the DB-backed stand-in for the un-watched CRITICAL payment logs. --- Fresh installs get it from the CREATE TYPE below; existing deployments must --- apply the ALTER TYPE after it (NOTE: ALTER TYPE ... ADD VALUE cannot run --- inside a transaction block — run on a connection with autocommit). The --- value is a no-op here on fresh installs (it is already in the CREATE TYPE). CREATE TYPE admin_notification_reason AS ENUM ('pending_booking', 'cancelled_booking', 'rescheduled_booking', '1_week_no_pay', '1_month_no_pay', 'affiliate_claim', 'late_cancellation', 'deposit_paid', 'edit_request', 'edit_requested', 'new_booking', 'deposit_not_paid_by_deadline', 'gift_card_purchased_for_friend', 'default_hours_changed', 'refund_failed', 'critical_payment_log'); --- Existing-deployment migration for the value added to the CREATE TYPE above. -ALTER TYPE admin_notification_reason ADD VALUE IF NOT EXISTS 'critical_payment_log'; - CREATE TABLE admin_notifications ( id CHAR(12) PRIMARY KEY DEFAULT generate_admin_notifications_id(), reason admin_notification_reason NOT NULL, @@ -2244,13 +2235,16 @@ CREATE INDEX idx_gift_card_transactions_type ON gift_card_transactions(transacti -- before transfer to reclaim fund). No claim deadline imposed — consumer can recover -- at any time with valid account ID. -- --- No PII stored: only account ID + balance amount. After account anonymization, --- we cannot verify claims without the account ID (by design — GDPR compliance). +-- No PII stored: only account ID (nullable — unredeemed gift cards have no +-- account) + balance amount. Account-anonymized claims are verified with the +-- account ID; unredeemed-card expiries carry NULL (no account to reference). -- ======================================= CREATE TABLE gift_card_expired_balances ( id CHAR(12) PRIMARY KEY DEFAULT generate_gift_card_expired_balances_id(), - account_id CHAR(12) NOT NULL, + -- NULL for unredeemed gift-card expiries (no user account to reference); + -- account-based expiries always store the account ID. + account_id CHAR(12), original_balance NUMERIC(10,2) NOT NULL, expired_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), claimed_at TIMESTAMPTZ, @@ -2517,5 +2511,3 @@ CREATE TABLE IF NOT EXISTS refresh_tokens ( CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user_id ON refresh_tokens (user_id); CREATE INDEX IF NOT EXISTS idx_refresh_tokens_expires_at ON refresh_tokens (expires_at); --- Migration: add voucher_type_at_purchase to gift_cards (for SPV/MPV per-card tracking) -ALTER TABLE gift_cards ADD COLUMN IF NOT EXISTS voucher_type_at_purchase VARCHAR(3);