CREATE EXTENSION IF NOT EXISTS "pgcrypto"; DO $$ BEGIN CREATE TYPE event_status AS ENUM ('draft', 'published', 'closed', 'archived'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE token_status AS ENUM ('active', 'used', 'revoked', 'expired'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE rsvp_response AS ENUM ('attending', 'declined', 'maybe'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE notification_channel AS ENUM ('sms', 'email'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE notification_type AS ENUM ('invitation', 'verification', 'confirmation', 'reminder'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; DO $$ BEGIN CREATE TYPE delivery_status AS ENUM ('queued', 'sent', 'delivered', 'failed', 'bounced'); EXCEPTION WHEN duplicate_object THEN NULL; END $$; CREATE TABLE IF NOT EXISTS users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email VARCHAR(255) UNIQUE NOT NULL, name VARCHAR(255) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE TABLE IF NOT EXISTS events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), host_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, name VARCHAR(255) NOT NULL, slug VARCHAR(100) UNIQUE NOT NULL, event_date TIMESTAMPTZ NOT NULL, venue TEXT NOT NULL DEFAULT '', max_capacity INTEGER NOT NULL DEFAULT 0, settings JSONB NOT NULL DEFAULT '{}'::jsonb, status event_status NOT NULL DEFAULT 'draft', created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_events_host ON events(host_id); CREATE INDEX IF NOT EXISTS idx_events_status ON events(status); CREATE TABLE IF NOT EXISTS guests ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE, name VARCHAR(255) NOT NULL, email VARCHAR(255), phone VARCHAR(20), plus_ones INTEGER NOT NULL DEFAULT 0, dietary_notes TEXT, table_number INTEGER, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_guests_event ON guests(event_id); CREATE TABLE IF NOT EXISTS tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), guest_id UUID NOT NULL UNIQUE REFERENCES guests(id) ON DELETE CASCADE, token_hash VARCHAR(64) NOT NULL, expires_at TIMESTAMPTZ NOT NULL, status token_status NOT NULL DEFAULT 'active', used_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_tokens_hash ON tokens(token_hash) WHERE status = 'active'; CREATE TABLE IF NOT EXISTS rsvps ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), guest_id UUID NOT NULL UNIQUE REFERENCES guests(id) ON DELETE CASCADE, response rsvp_response NOT NULL, plus_ones INTEGER NOT NULL DEFAULT 0, dietary_notes TEXT, submitted_at TIMESTAMPTZ NOT NULL DEFAULT now(), device_fingerprint JSONB, ip_address INET, risk_score SMALLINT ); CREATE INDEX IF NOT EXISTS idx_rsvps_guest ON rsvps(guest_id); CREATE TABLE IF NOT EXISTS access_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), guest_id UUID NOT NULL REFERENCES guests(id) ON DELETE CASCADE, token_id UUID REFERENCES tokens(id) ON DELETE SET NULL, fingerprint JSONB, ip_address INET, geo_location JSONB, risk_score SMALLINT, risk_reasons TEXT[], flagged BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_access_logs_guest ON access_logs(guest_id, created_at DESC); CREATE INDEX IF NOT EXISTS idx_access_logs_flagged ON access_logs(flagged) WHERE flagged = TRUE; CREATE TABLE IF NOT EXISTS notifications ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), guest_id UUID NOT NULL REFERENCES guests(id) ON DELETE CASCADE, channel notification_channel NOT NULL, type notification_type NOT NULL, status delivery_status NOT NULL DEFAULT 'queued', provider_id VARCHAR(100), attempts SMALLINT NOT NULL DEFAULT 0, last_attempt TIMESTAMPTZ, delivered_at TIMESTAMPTZ, error TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_notifications_status ON notifications(status) WHERE status IN ('queued', 'failed');