-- ============================================================================ -- Migration 000: Initial Database Schema -- ============================================================================ -- Description: Creates all base tables, enums, and indexes for the LOAF -- membership platform. This migration should be run first on -- a fresh database. -- Date: 2024-12-18 -- Author: LOAF Development Team -- ============================================================================ BEGIN; -- ============================================================================ -- SECTION 1: Create ENUM Types -- ============================================================================ -- User status enum CREATE TYPE userstatus AS ENUM ( 'pending_email', 'pending_validation', 'pre_validated', 'payment_pending', 'active', 'inactive', 'canceled', 'expired', 'abandoned', 'rejected' ); -- User role enum CREATE TYPE userrole AS ENUM ( 'guest', 'member', 'admin', 'finance', 'superadmin' ); -- RSVP status enum CREATE TYPE rsvpstatus AS ENUM ( 'yes', 'no', 'maybe' ); -- Subscription status enum CREATE TYPE subscriptionstatus AS ENUM ( 'active', 'cancelled', 'expired' ); -- Donation type enum CREATE TYPE donationtype AS ENUM ( 'member', 'public' ); -- Donation status enum CREATE TYPE donationstatus AS ENUM ( 'pending', 'completed', 'failed' ); -- Invitation status enum CREATE TYPE invitationstatus AS ENUM ( 'pending', 'accepted', 'expired', 'revoked' ); -- Import job status enum CREATE TYPE importjobstatus AS ENUM ( 'processing', 'completed', 'failed', 'partial', 'validating', 'preview_ready', 'rolled_back' ); COMMIT; -- Display progress SELECT 'Step 1/8 completed: ENUM types created' AS progress; BEGIN; -- ============================================================================ -- SECTION 2: Create Core Tables -- ============================================================================ -- Import Jobs table (must be created before users due to FK reference) CREATE TABLE IF NOT EXISTS import_jobs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), filename VARCHAR NOT NULL, status importjobstatus NOT NULL DEFAULT 'processing', total_rows INTEGER DEFAULT 0, processed_rows INTEGER DEFAULT 0, success_count INTEGER DEFAULT 0, error_count INTEGER DEFAULT 0, error_log JSONB DEFAULT '[]'::jsonb, -- WordPress import enhancements field_mapping JSONB DEFAULT '{}'::jsonb, wordpress_metadata JSONB DEFAULT '{}'::jsonb, imported_user_ids JSONB DEFAULT '[]'::jsonb, rollback_at TIMESTAMP WITH TIME ZONE, rollback_by UUID, -- Will be updated with FK after users table exists started_by UUID, -- Will be updated with FK after users table exists started_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, completed_at TIMESTAMP WITH TIME ZONE ); -- Users table CREATE TABLE IF NOT EXISTS users ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Authentication email VARCHAR NOT NULL UNIQUE, password_hash VARCHAR NOT NULL, email_verified BOOLEAN NOT NULL DEFAULT FALSE, email_verification_token VARCHAR UNIQUE, email_verification_expires TIMESTAMP WITH TIME ZONE, -- Personal Information first_name VARCHAR NOT NULL, last_name VARCHAR NOT NULL, phone VARCHAR, address VARCHAR, city VARCHAR, state VARCHAR(2), zipcode VARCHAR(10), date_of_birth DATE, -- Profile profile_photo_url VARCHAR, -- Social Media social_media_facebook VARCHAR, social_media_instagram VARCHAR, social_media_twitter VARCHAR, social_media_linkedin VARCHAR, -- Partner Information partner_first_name VARCHAR, partner_last_name VARCHAR, partner_is_member BOOLEAN DEFAULT FALSE, partner_plan_to_become_member BOOLEAN DEFAULT FALSE, -- Referral referred_by_member_name VARCHAR, lead_sources JSONB DEFAULT '[]'::jsonb, -- Status & Role status userstatus NOT NULL DEFAULT 'pending_email', role userrole NOT NULL DEFAULT 'guest', role_id UUID, -- For dynamic RBAC -- Newsletter Preferences newsletter_subscribed BOOLEAN DEFAULT TRUE, newsletter_publish_name BOOLEAN DEFAULT FALSE NOT NULL, newsletter_publish_photo BOOLEAN DEFAULT FALSE NOT NULL, newsletter_publish_birthday BOOLEAN DEFAULT FALSE NOT NULL, newsletter_publish_none BOOLEAN DEFAULT FALSE NOT NULL, -- Volunteer Interests volunteer_interests JSONB DEFAULT '[]'::jsonb, -- Scholarship Request scholarship_requested BOOLEAN DEFAULT FALSE NOT NULL, scholarship_reason TEXT, -- Directory Settings show_in_directory BOOLEAN DEFAULT FALSE NOT NULL, directory_email VARCHAR, directory_bio TEXT, directory_address VARCHAR, directory_phone VARCHAR, directory_dob DATE, directory_partner_name VARCHAR, -- Password Reset password_reset_token VARCHAR, password_reset_expires TIMESTAMP WITH TIME ZONE, force_password_change BOOLEAN DEFAULT FALSE NOT NULL, -- Terms of Service accepts_tos BOOLEAN DEFAULT FALSE NOT NULL, tos_accepted_at TIMESTAMP WITH TIME ZONE, -- Membership member_since DATE, -- Reminder Tracking email_verification_reminders_sent INTEGER DEFAULT 0 NOT NULL, last_email_verification_reminder_at TIMESTAMP WITH TIME ZONE, event_attendance_reminders_sent INTEGER DEFAULT 0 NOT NULL, last_event_attendance_reminder_at TIMESTAMP WITH TIME ZONE, payment_reminders_sent INTEGER DEFAULT 0 NOT NULL, last_payment_reminder_at TIMESTAMP WITH TIME ZONE, renewal_reminders_sent INTEGER DEFAULT 0 NOT NULL, last_renewal_reminder_at TIMESTAMP WITH TIME ZONE, -- Rejection Tracking rejection_reason TEXT, rejected_at TIMESTAMP WITH TIME ZONE, rejected_by UUID REFERENCES users(id), -- WordPress Import Tracking import_source VARCHAR(50), import_job_id UUID REFERENCES import_jobs(id), wordpress_user_id BIGINT, wordpress_registered_date TIMESTAMP WITH TIME ZONE, -- Role Change Audit Trail role_changed_at TIMESTAMP WITH TIME ZONE, role_changed_by UUID REFERENCES users(id) ON DELETE SET NULL, -- Timestamps created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Events table CREATE TABLE IF NOT EXISTS events ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Event Details title VARCHAR NOT NULL, description TEXT, location VARCHAR, cover_image_url VARCHAR, -- Schedule start_at TIMESTAMP WITH TIME ZONE NOT NULL, end_at TIMESTAMP WITH TIME ZONE, -- Capacity capacity INTEGER, published BOOLEAN NOT NULL DEFAULT FALSE, -- Calendar Integration calendar_uid VARCHAR UNIQUE, microsoft_calendar_id VARCHAR, microsoft_calendar_sync_enabled BOOLEAN DEFAULT FALSE, -- Metadata created_by UUID REFERENCES users(id), created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Event RSVPs table CREATE TABLE IF NOT EXISTS event_rsvps ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- RSVP Details rsvp_status rsvpstatus NOT NULL DEFAULT 'maybe', attended BOOLEAN DEFAULT FALSE, attended_at TIMESTAMP WITH TIME ZONE, -- Timestamps created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, -- Unique constraint: one RSVP per user per event UNIQUE(event_id, user_id) ); -- Event Gallery table CREATE TABLE IF NOT EXISTS event_galleries ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), event_id UUID NOT NULL REFERENCES events(id) ON DELETE CASCADE, -- Image Details image_url VARCHAR NOT NULL, caption TEXT, order_index INTEGER DEFAULT 0, -- Metadata uploaded_by UUID REFERENCES users(id), uploaded_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); COMMIT; -- Display progress SELECT 'Step 2/8 completed: Core tables (users, events, rsvps, gallery) created' AS progress; BEGIN; -- ============================================================================ -- SECTION 3: Create Subscription & Payment Tables -- ============================================================================ -- Subscription Plans table CREATE TABLE IF NOT EXISTS subscription_plans ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Plan Details name VARCHAR NOT NULL, description TEXT, price_cents INTEGER NOT NULL, billing_cycle VARCHAR NOT NULL DEFAULT 'yearly', stripe_price_id VARCHAR, -- Legacy, deprecated -- Configuration active BOOLEAN NOT NULL DEFAULT TRUE, -- Custom billing cycle fields (for recurring date ranges like Jan 1 - Dec 31) custom_cycle_enabled BOOLEAN DEFAULT FALSE NOT NULL, custom_cycle_start_month INTEGER, custom_cycle_start_day INTEGER, custom_cycle_end_month INTEGER, custom_cycle_end_day INTEGER, -- Dynamic pricing fields minimum_price_cents INTEGER DEFAULT 3000 NOT NULL, suggested_price_cents INTEGER, allow_donation BOOLEAN DEFAULT TRUE NOT NULL, -- Timestamps created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Subscriptions table CREATE TABLE IF NOT EXISTS subscriptions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, plan_id UUID NOT NULL REFERENCES subscription_plans(id), -- Stripe Integration stripe_subscription_id VARCHAR, stripe_customer_id VARCHAR, -- Status & Dates status subscriptionstatus DEFAULT 'active', start_date TIMESTAMP WITH TIME ZONE NOT NULL, end_date TIMESTAMP WITH TIME ZONE, -- Payment Details amount_paid_cents INTEGER, base_subscription_cents INTEGER NOT NULL, donation_cents INTEGER DEFAULT 0 NOT NULL, -- Stripe transaction metadata (for validation and audit) stripe_payment_intent_id VARCHAR, stripe_charge_id VARCHAR, stripe_invoice_id VARCHAR, payment_completed_at TIMESTAMP WITH TIME ZONE, card_last4 VARCHAR(4), card_brand VARCHAR(20), stripe_receipt_url VARCHAR, -- Manual Payment Support manual_payment BOOLEAN DEFAULT FALSE NOT NULL, manual_payment_notes TEXT, manual_payment_admin_id UUID REFERENCES users(id), manual_payment_date TIMESTAMP WITH TIME ZONE, payment_method VARCHAR, -- Timestamps created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Donations table CREATE TABLE IF NOT EXISTS donations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- Donation Details amount_cents INTEGER NOT NULL, donation_type donationtype NOT NULL DEFAULT 'public', status donationstatus NOT NULL DEFAULT 'pending', -- Donor Information user_id UUID REFERENCES users(id), -- NULL for public donations donor_email VARCHAR, donor_name VARCHAR, -- Payment Details stripe_checkout_session_id VARCHAR, stripe_payment_intent_id VARCHAR, payment_method VARCHAR, -- Stripe transaction metadata (for validation and audit) stripe_charge_id VARCHAR, stripe_customer_id VARCHAR, payment_completed_at TIMESTAMP WITH TIME ZONE, card_last4 VARCHAR(4), card_brand VARCHAR(20), stripe_receipt_url VARCHAR, -- Metadata notes TEXT, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE ); COMMIT; -- Display progress SELECT 'Step 3/8 completed: Subscription and donation tables created' AS progress; BEGIN; -- ============================================================================ -- SECTION 4: Create RBAC Tables -- ============================================================================ -- Permissions table CREATE TABLE IF NOT EXISTS permissions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code VARCHAR NOT NULL UNIQUE, name VARCHAR NOT NULL, description TEXT, module VARCHAR NOT NULL, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Roles table (for dynamic RBAC) CREATE TABLE IF NOT EXISTS roles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code VARCHAR NOT NULL UNIQUE, name VARCHAR NOT NULL, description TEXT, is_system_role BOOLEAN NOT NULL DEFAULT FALSE, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES users(id) ON DELETE SET NULL ); -- Role Permissions junction table CREATE TABLE IF NOT EXISTS role_permissions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), role userrole, -- Legacy enum-based role (for backward compatibility) role_id UUID REFERENCES roles(id) ON DELETE CASCADE, -- Dynamic role permission_id UUID NOT NULL REFERENCES permissions(id) ON DELETE CASCADE, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, created_by UUID REFERENCES users(id) ON DELETE SET NULL ); COMMIT; -- Display progress SELECT 'Step 4/8 completed: RBAC tables created' AS progress; BEGIN; -- ============================================================================ -- SECTION 5: Create Document Management Tables -- ============================================================================ -- Newsletter Archive table CREATE TABLE IF NOT EXISTS newsletter_archives ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title VARCHAR NOT NULL, file_url VARCHAR NOT NULL, file_size_bytes INTEGER, issue_date DATE NOT NULL, description TEXT, uploaded_by UUID REFERENCES users(id), uploaded_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Financial Reports table CREATE TABLE IF NOT EXISTS financial_reports ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title VARCHAR NOT NULL, file_url VARCHAR NOT NULL, file_size_bytes INTEGER, fiscal_period VARCHAR NOT NULL, report_type VARCHAR, uploaded_by UUID REFERENCES users(id), uploaded_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Bylaws Documents table CREATE TABLE IF NOT EXISTS bylaws_documents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title VARCHAR NOT NULL, file_url VARCHAR NOT NULL, file_size_bytes INTEGER, version VARCHAR NOT NULL, effective_date DATE NOT NULL, description TEXT, is_current BOOLEAN DEFAULT TRUE, uploaded_by UUID REFERENCES users(id), uploaded_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); COMMIT; -- Display progress SELECT 'Step 5/8 completed: Document management tables created' AS progress; BEGIN; -- ============================================================================ -- SECTION 6: Create System Tables -- ============================================================================ -- Storage Usage table CREATE TABLE IF NOT EXISTS storage_usage ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), total_bytes_used BIGINT NOT NULL DEFAULT 0, max_bytes_allowed BIGINT NOT NULL DEFAULT 1073741824, -- 1GB last_updated TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- User Invitations table CREATE TABLE IF NOT EXISTS user_invitations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), email VARCHAR NOT NULL, token VARCHAR NOT NULL UNIQUE, role userrole NOT NULL, status invitationstatus NOT NULL DEFAULT 'pending', invited_by UUID REFERENCES users(id) ON DELETE SET NULL, expires_at TIMESTAMP WITH TIME ZONE NOT NULL, accepted_at TIMESTAMP WITH TIME ZONE, revoked_at TIMESTAMP WITH TIME ZONE, created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP ); -- Add FK constraints to import_jobs (now that users table exists) ALTER TABLE import_jobs ADD CONSTRAINT fk_import_jobs_rollback_by FOREIGN KEY (rollback_by) REFERENCES users(id), ADD CONSTRAINT fk_import_jobs_started_by FOREIGN KEY (started_by) REFERENCES users(id); -- Import Rollback Audit table (for tracking rollback operations) CREATE TABLE IF NOT EXISTS import_rollback_audit ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), import_job_id UUID NOT NULL REFERENCES import_jobs(id), rolled_back_by UUID NOT NULL REFERENCES users(id), rolled_back_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(), deleted_user_count INTEGER NOT NULL, deleted_user_ids JSONB NOT NULL, reason TEXT, created_at TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW() ); COMMIT; -- Display progress SELECT 'Step 6/8 completed: System tables created' AS progress; BEGIN; -- ============================================================================ -- SECTION 7: Create Indexes -- ============================================================================ -- Users table indexes CREATE INDEX IF NOT EXISTS idx_users_email ON users(email); CREATE INDEX IF NOT EXISTS idx_users_status ON users(status); CREATE INDEX IF NOT EXISTS idx_users_role ON users(role); CREATE INDEX IF NOT EXISTS idx_users_role_id ON users(role_id); CREATE INDEX IF NOT EXISTS idx_users_email_verified ON users(email_verified); CREATE INDEX IF NOT EXISTS idx_users_rejected_at ON users(rejected_at) WHERE rejected_at IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_users_created_at ON users(created_at); CREATE INDEX IF NOT EXISTS idx_users_import_job ON users(import_job_id) WHERE import_job_id IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_users_import_source ON users(import_source) WHERE import_source IS NOT NULL; -- Events table indexes CREATE INDEX IF NOT EXISTS idx_events_created_by ON events(created_by); CREATE INDEX IF NOT EXISTS idx_events_start_at ON events(start_at); CREATE INDEX IF NOT EXISTS idx_events_published ON events(published); -- Event RSVPs indexes CREATE INDEX IF NOT EXISTS idx_event_rsvps_event_id ON event_rsvps(event_id); CREATE INDEX IF NOT EXISTS idx_event_rsvps_user_id ON event_rsvps(user_id); CREATE INDEX IF NOT EXISTS idx_event_rsvps_rsvp_status ON event_rsvps(rsvp_status); -- Event Gallery indexes CREATE INDEX IF NOT EXISTS idx_event_galleries_event_id ON event_galleries(event_id); -- Subscriptions indexes CREATE INDEX IF NOT EXISTS idx_subscriptions_user_id ON subscriptions(user_id); CREATE INDEX IF NOT EXISTS idx_subscriptions_plan_id ON subscriptions(plan_id); CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON subscriptions(status); CREATE INDEX IF NOT EXISTS idx_subscriptions_stripe_subscription_id ON subscriptions(stripe_subscription_id); CREATE INDEX IF NOT EXISTS idx_subscriptions_payment_intent ON subscriptions(stripe_payment_intent_id); CREATE INDEX IF NOT EXISTS idx_subscriptions_charge_id ON subscriptions(stripe_charge_id); CREATE INDEX IF NOT EXISTS idx_subscriptions_invoice_id ON subscriptions(stripe_invoice_id); -- Donations indexes CREATE INDEX IF NOT EXISTS idx_donation_user ON donations(user_id); CREATE INDEX IF NOT EXISTS idx_donation_type ON donations(donation_type); CREATE INDEX IF NOT EXISTS idx_donation_status ON donations(status); CREATE INDEX IF NOT EXISTS idx_donation_created ON donations(created_at); CREATE INDEX IF NOT EXISTS idx_donation_payment_intent ON donations(stripe_payment_intent_id); CREATE INDEX IF NOT EXISTS idx_donation_charge_id ON donations(stripe_charge_id); CREATE INDEX IF NOT EXISTS idx_donation_customer_id ON donations(stripe_customer_id); -- Import Jobs indexes CREATE INDEX IF NOT EXISTS idx_import_jobs_status ON import_jobs(status); CREATE INDEX IF NOT EXISTS idx_import_jobs_started_by ON import_jobs(started_by); -- Import Rollback Audit indexes CREATE INDEX IF NOT EXISTS idx_rollback_audit_import_job ON import_rollback_audit(import_job_id); CREATE INDEX IF NOT EXISTS idx_rollback_audit_rolled_back_at ON import_rollback_audit(rolled_back_at DESC); -- Permissions indexes CREATE INDEX IF NOT EXISTS idx_permissions_code ON permissions(code); CREATE INDEX IF NOT EXISTS idx_permissions_module ON permissions(module); -- Roles indexes CREATE INDEX IF NOT EXISTS idx_roles_code ON roles(code); CREATE INDEX IF NOT EXISTS idx_roles_is_system_role ON roles(is_system_role); -- Role Permissions indexes CREATE INDEX IF NOT EXISTS idx_role_permissions_role ON role_permissions(role); CREATE INDEX IF NOT EXISTS idx_role_permissions_role_id ON role_permissions(role_id); CREATE UNIQUE INDEX IF NOT EXISTS idx_role_permission ON role_permissions(role, permission_id) WHERE role IS NOT NULL; CREATE UNIQUE INDEX IF NOT EXISTS idx_dynamic_role_permission ON role_permissions(role_id, permission_id) WHERE role_id IS NOT NULL; COMMIT; -- Display progress SELECT 'Step 7/8 completed: Indexes created' AS progress; BEGIN; -- ============================================================================ -- SECTION 8: Initialize Default Data -- ============================================================================ -- Insert initial storage usage record INSERT INTO storage_usage (id, total_bytes_used, max_bytes_allowed, last_updated) SELECT gen_random_uuid(), 0, 1073741824, -- 1GB CURRENT_TIMESTAMP WHERE NOT EXISTS (SELECT 1 FROM storage_usage); COMMIT; -- Display progress SELECT 'Step 8/8 completed: Default data initialized' AS progress; -- ============================================================================ -- Migration Complete -- ============================================================================ SELECT ' ================================================================================ ✅ Migration 000 completed successfully! ================================================================================ Database schema initialized with: - 10 ENUM types - 17 tables (users, events, subscriptions, donations, RBAC, documents, system) - 30+ indexes for performance - 1 storage usage record Next steps: 1. Run: python seed_permissions_rbac.py (to populate permissions and roles) 2. Run: python create_admin.py (to create superadmin user) 3. Run remaining migrations in sequence (001-010) Database is ready for LOAF membership platform! 🎉 ================================================================================ ' AS migration_complete;