Prod Deployment Preparation

This commit is contained in:
Koncept Kit
2026-01-04 19:08:54 +07:00
parent 85199958bc
commit 6ec0745966
16 changed files with 2188 additions and 5 deletions

View File

@@ -146,14 +146,19 @@ CREATE TABLE IF NOT EXISTS users (
-- Membership
member_since DATE,
tos_accepted BOOLEAN DEFAULT FALSE,
accepts_tos BOOLEAN DEFAULT FALSE,
tos_accepted_at TIMESTAMP WITH TIME ZONE,
newsletter_subscribed BOOLEAN DEFAULT TRUE,
-- Reminder Tracking
reminder_30_days_sent BOOLEAN DEFAULT FALSE,
reminder_60_days_sent BOOLEAN DEFAULT FALSE,
reminder_85_days_sent BOOLEAN DEFAULT FALSE,
-- Reminder Tracking (from migration 004)
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,
-- WordPress Import Tracking
import_source VARCHAR(50),

View File

@@ -0,0 +1,394 @@
-- ============================================================================
-- Create Tables Only (ENUMs already exist)
-- Run this when ENUMs exist but tables don't
-- ============================================================================
BEGIN;
-- ============================================================================
-- STEP 1: Core Tables
-- ============================================================================
-- Users table
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
first_name VARCHAR(100) NOT NULL,
last_name VARCHAR(100) NOT NULL,
phone VARCHAR(20),
address TEXT,
city VARCHAR(100),
state VARCHAR(2),
zipcode VARCHAR(10),
date_of_birth DATE,
-- Profile
profile_image_url TEXT,
bio TEXT,
interests TEXT,
-- Partner Information
partner_first_name VARCHAR(100),
partner_last_name VARCHAR(100),
partner_is_member BOOLEAN DEFAULT FALSE,
partner_plan_to_become_member BOOLEAN DEFAULT FALSE,
-- Referral
referred_by_member_name VARCHAR(200),
-- Newsletter Preferences
newsletter_subscribed BOOLEAN DEFAULT TRUE,
newsletter_publish_name BOOLEAN DEFAULT FALSE,
newsletter_publish_photo BOOLEAN DEFAULT FALSE,
newsletter_publish_birthday BOOLEAN DEFAULT FALSE,
newsletter_publish_none BOOLEAN DEFAULT FALSE,
-- Volunteer & Scholarship
volunteer_interests TEXT,
scholarship_requested BOOLEAN DEFAULT FALSE,
-- Directory
show_in_directory BOOLEAN DEFAULT TRUE,
-- Lead Sources (JSON array)
lead_sources JSONB DEFAULT '[]'::jsonb,
-- Status & Role
status userstatus DEFAULT 'pending_email' NOT NULL,
role userrole DEFAULT 'guest' NOT NULL,
role_id UUID,
-- Rejection Tracking
rejection_reason TEXT,
rejected_at TIMESTAMP WITH TIME ZONE,
rejected_by UUID REFERENCES users(id),
-- Membership
member_since DATE,
accepts_tos BOOLEAN DEFAULT FALSE,
tos_accepted_at TIMESTAMP WITH TIME ZONE,
-- Reminder Tracking (from migration 004)
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,
-- WordPress Import Tracking
import_source VARCHAR(50),
import_job_id UUID,
wordpress_user_id BIGINT,
wordpress_registered_date TIMESTAMP WITH TIME ZONE,
-- Authentication
email_verified BOOLEAN DEFAULT FALSE,
email_verification_token VARCHAR(255),
email_verification_expires TIMESTAMP WITH TIME ZONE,
password_reset_token VARCHAR(255),
password_reset_expires TIMESTAMP WITH TIME ZONE,
force_password_change BOOLEAN DEFAULT FALSE,
-- Timestamps
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Events table
CREATE TABLE IF NOT EXISTS events (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(255) NOT NULL,
description TEXT,
location VARCHAR(255),
start_at TIMESTAMP WITH TIME ZONE NOT NULL,
end_at TIMESTAMP WITH TIME ZONE NOT NULL,
capacity INTEGER,
published BOOLEAN DEFAULT FALSE,
calendar_uid VARCHAR(255) UNIQUE,
created_by UUID REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Event RSVPs
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_status rsvpstatus NOT NULL,
attended BOOLEAN DEFAULT FALSE,
attended_at TIMESTAMP WITH TIME ZONE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
UNIQUE(event_id, user_id)
);
-- Event Gallery
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_url TEXT NOT NULL,
caption TEXT,
uploaded_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Import Jobs
CREATE TABLE IF NOT EXISTS import_jobs (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
filename VARCHAR(255) NOT NULL,
file_key VARCHAR(255),
total_rows INTEGER NOT NULL,
processed_rows INTEGER DEFAULT 0,
successful_rows INTEGER DEFAULT 0,
failed_rows INTEGER DEFAULT 0,
status importjobstatus DEFAULT 'processing' NOT NULL,
errors 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 REFERENCES users(id),
imported_by UUID NOT NULL REFERENCES users(id),
started_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
completed_at TIMESTAMP WITH TIME ZONE
);
COMMIT;
-- ============================================================================
-- STEP 2: Subscription & Payment Tables
-- ============================================================================
BEGIN;
CREATE TABLE IF NOT EXISTS subscription_plans (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL,
description TEXT,
price_cents INTEGER NOT NULL,
billing_cycle VARCHAR(20) NOT NULL,
stripe_price_id VARCHAR(255),
custom_cycle_enabled BOOLEAN DEFAULT FALSE,
minimum_price_cents INTEGER DEFAULT 0,
allow_donation BOOLEAN DEFAULT FALSE,
is_active BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
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_subscription_id VARCHAR(255),
stripe_customer_id VARCHAR(255),
base_subscription_cents INTEGER NOT NULL,
donation_cents INTEGER DEFAULT 0,
status subscriptionstatus DEFAULT 'active' NOT NULL,
current_period_start TIMESTAMP WITH TIME ZONE,
current_period_end TIMESTAMP WITH TIME ZONE,
cancel_at_period_end BOOLEAN DEFAULT FALSE,
canceled_at TIMESTAMP WITH TIME ZONE,
manual_payment BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS donations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id),
amount_cents INTEGER NOT NULL,
donation_type donationtype NOT NULL,
status donationstatus DEFAULT 'pending' NOT NULL,
stripe_payment_intent_id VARCHAR(255),
donor_name VARCHAR(200),
donor_email VARCHAR(255),
message TEXT,
is_anonymous BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
completed_at TIMESTAMP WITH TIME ZONE
);
COMMIT;
-- ============================================================================
-- STEP 3: RBAC Tables
-- ============================================================================
BEGIN;
CREATE TABLE IF NOT EXISTS permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(100) UNIQUE NOT NULL,
name VARCHAR(200) NOT NULL,
description TEXT,
module VARCHAR(50),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS roles (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
code VARCHAR(50) UNIQUE NOT NULL,
name VARCHAR(100) NOT NULL,
description TEXT,
is_system_role BOOLEAN DEFAULT FALSE,
created_by UUID REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS role_permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
role VARCHAR(50),
role_id UUID REFERENCES roles(id) ON DELETE CASCADE,
permission_id UUID NOT NULL REFERENCES permissions(id) ON DELETE CASCADE,
created_by UUID REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS user_invitations (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email VARCHAR(255) NOT NULL,
role userrole NOT NULL,
token VARCHAR(255) UNIQUE NOT NULL,
invited_by UUID NOT NULL REFERENCES users(id),
invited_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
accepted_by UUID REFERENCES users(id),
accepted_at TIMESTAMP WITH TIME ZONE,
status invitationstatus DEFAULT 'pending' NOT NULL
);
COMMIT;
-- ============================================================================
-- STEP 4: Document Management Tables
-- ============================================================================
BEGIN;
CREATE TABLE IF NOT EXISTS newsletter_archives (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(255) NOT NULL,
description TEXT,
published_date TIMESTAMP WITH TIME ZONE NOT NULL,
document_url TEXT NOT NULL,
document_type VARCHAR(50) NOT NULL,
created_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS financial_reports (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
year INTEGER NOT NULL,
title VARCHAR(255) NOT NULL,
document_url TEXT NOT NULL,
document_type VARCHAR(50) NOT NULL,
created_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS bylaws_documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
title VARCHAR(255) NOT NULL,
version VARCHAR(50) NOT NULL,
effective_date TIMESTAMP WITH TIME ZONE NOT NULL,
document_url TEXT NOT NULL,
document_type VARCHAR(50) NOT NULL,
is_current BOOLEAN DEFAULT FALSE,
created_by UUID NOT NULL REFERENCES users(id),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
COMMIT;
-- ============================================================================
-- STEP 5: System Tables
-- ============================================================================
BEGIN;
CREATE TABLE IF NOT EXISTS storage_usage (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
total_bytes_used BIGINT DEFAULT 0,
max_bytes_allowed BIGINT,
last_calculated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
updated_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
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 DEFAULT NOW(),
deleted_user_count INTEGER NOT NULL,
deleted_user_ids JSONB NOT NULL,
reason TEXT,
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);
-- Initialize storage_usage with default row
INSERT INTO storage_usage (id, total_bytes_used, max_bytes_allowed)
VALUES (gen_random_uuid(), 0, 107374182400) -- 100GB limit
ON CONFLICT DO NOTHING;
COMMIT;
-- ============================================================================
-- STEP 6: Create Indexes
-- ============================================================================
BEGIN;
-- Users 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_created_at ON users(created_at);
-- Events indexes
CREATE INDEX IF NOT EXISTS idx_events_start_at ON events(start_at);
CREATE INDEX IF NOT EXISTS idx_events_published ON events(published);
CREATE INDEX IF NOT EXISTS idx_events_created_by ON events(created_by);
-- 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_attended ON event_rsvps(attended);
-- Subscriptions indexes
CREATE INDEX IF NOT EXISTS idx_subscriptions_user_id ON subscriptions(user_id);
CREATE INDEX IF NOT EXISTS idx_subscriptions_status ON subscriptions(status);
CREATE INDEX IF NOT EXISTS idx_subscriptions_stripe_customer_id ON subscriptions(stripe_customer_id);
-- Permissions indexes
CREATE INDEX IF NOT EXISTS ix_permissions_code ON permissions(code);
CREATE INDEX IF NOT EXISTS ix_permissions_module ON permissions(module);
-- Roles indexes
CREATE INDEX IF NOT EXISTS ix_roles_code ON roles(code);
-- Role permissions indexes
CREATE INDEX IF NOT EXISTS ix_role_permissions_role ON role_permissions(role);
CREATE INDEX IF NOT EXISTS ix_role_permissions_role_id ON role_permissions(role_id);
-- User invitations indexes
CREATE INDEX IF NOT EXISTS ix_user_invitations_email ON user_invitations(email);
CREATE INDEX IF NOT EXISTS ix_user_invitations_token ON user_invitations(token);
COMMIT;
\echo '✅ All tables created successfully!'
\echo 'Run: psql ... -c "\dt" to verify'

View File

@@ -0,0 +1,80 @@
-- ============================================================================
-- Database Diagnostic Script
-- Run this to check what exists in your database
-- ============================================================================
\echo '=== CHECKING ENUMS ==='
SELECT
t.typname as enum_name,
string_agg(e.enumlabel, ', ' ORDER BY e.enumsortorder) as values
FROM pg_type t
JOIN pg_enum e ON t.oid = e.enumtypid
WHERE t.typname IN (
'userstatus', 'userrole', 'rsvpstatus', 'subscriptionstatus',
'donationtype', 'donationstatus', 'invitationstatus', 'importjobstatus'
)
GROUP BY t.typname
ORDER BY t.typname;
\echo ''
\echo '=== CHECKING TABLES ==='
SELECT
schemaname,
tablename
FROM pg_tables
WHERE schemaname = 'public'
ORDER BY tablename;
\echo ''
\echo '=== CHECKING USERS TABLE STRUCTURE ==='
SELECT
column_name,
data_type,
is_nullable,
column_default
FROM information_schema.columns
WHERE table_name = 'users'
ORDER BY ordinal_position;
\echo ''
\echo '=== CHECKING FOR CRITICAL FIELDS ==='
\echo 'Checking if reminder tracking fields exist...'
SELECT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'users'
AND column_name = 'email_verification_reminders_sent'
) as has_reminder_fields;
\echo ''
\echo 'Checking if accepts_tos field exists (should be accepts_tos, not tos_accepted)...'
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'users'
AND column_name IN ('accepts_tos', 'tos_accepted');
\echo ''
\echo 'Checking if WordPress import fields exist...'
SELECT EXISTS (
SELECT 1
FROM information_schema.columns
WHERE table_name = 'users'
AND column_name = 'import_source'
) as has_import_fields;
\echo ''
\echo '=== CHECKING IMPORT_JOBS TABLE ==='
SELECT column_name
FROM information_schema.columns
WHERE table_name = 'import_jobs'
ORDER BY ordinal_position;
\echo ''
\echo '=== SUMMARY ==='
SELECT
(SELECT COUNT(*) FROM pg_type WHERE typname IN (
'userstatus', 'userrole', 'rsvpstatus', 'subscriptionstatus',
'donationtype', 'donationstatus', 'invitationstatus', 'importjobstatus'
)) as enum_count,
(SELECT COUNT(*) FROM pg_tables WHERE schemaname = 'public') as table_count,
(SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'users') as users_column_count;

View File

@@ -0,0 +1,169 @@
-- ============================================================================
-- Fix Missing Fields Script
-- Safely adds missing fields without recreating existing structures
-- ============================================================================
BEGIN;
\echo '=== FIXING USERS TABLE ==='
-- Fix TOS field name if needed (tos_accepted -> accepts_tos)
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'tos_accepted'
) AND NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'accepts_tos'
) THEN
ALTER TABLE users RENAME COLUMN tos_accepted TO accepts_tos;
RAISE NOTICE 'Renamed tos_accepted to accepts_tos';
END IF;
END $$;
-- Add reminder tracking fields if missing
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'email_verification_reminders_sent'
) THEN
ALTER TABLE users ADD COLUMN email_verification_reminders_sent INTEGER DEFAULT 0 NOT NULL;
ALTER TABLE users ADD COLUMN last_email_verification_reminder_at TIMESTAMP WITH TIME ZONE;
ALTER TABLE users ADD COLUMN event_attendance_reminders_sent INTEGER DEFAULT 0 NOT NULL;
ALTER TABLE users ADD COLUMN last_event_attendance_reminder_at TIMESTAMP WITH TIME ZONE;
ALTER TABLE users ADD COLUMN payment_reminders_sent INTEGER DEFAULT 0 NOT NULL;
ALTER TABLE users ADD COLUMN last_payment_reminder_at TIMESTAMP WITH TIME ZONE;
ALTER TABLE users ADD COLUMN renewal_reminders_sent INTEGER DEFAULT 0 NOT NULL;
ALTER TABLE users ADD COLUMN last_renewal_reminder_at TIMESTAMP WITH TIME ZONE;
RAISE NOTICE 'Added reminder tracking fields';
END IF;
END $$;
-- Add WordPress import fields if missing
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'users' AND column_name = 'import_source'
) THEN
ALTER TABLE users ADD COLUMN import_source VARCHAR(50);
ALTER TABLE users ADD COLUMN import_job_id UUID REFERENCES import_jobs(id);
ALTER TABLE users ADD COLUMN wordpress_user_id BIGINT;
ALTER TABLE users ADD COLUMN wordpress_registered_date TIMESTAMP WITH TIME ZONE;
RAISE NOTICE 'Added WordPress import tracking fields';
END IF;
END $$;
\echo '=== FIXING IMPORT_JOBS TABLE ==='
-- Add WordPress import enhancement fields if missing
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_name = 'import_jobs' AND column_name = 'field_mapping'
) THEN
ALTER TABLE import_jobs ADD COLUMN field_mapping JSONB DEFAULT '{}'::jsonb;
ALTER TABLE import_jobs ADD COLUMN wordpress_metadata JSONB DEFAULT '{}'::jsonb;
ALTER TABLE import_jobs ADD COLUMN imported_user_ids JSONB DEFAULT '[]'::jsonb;
ALTER TABLE import_jobs ADD COLUMN rollback_at TIMESTAMP WITH TIME ZONE;
ALTER TABLE import_jobs ADD COLUMN rollback_by UUID REFERENCES users(id);
RAISE NOTICE 'Added WordPress import enhancement fields to import_jobs';
END IF;
END $$;
-- Add validating, preview_ready, rolled_back to ImportJobStatus enum if missing
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_enum e
JOIN pg_type t ON e.enumtypid = t.oid
WHERE t.typname = 'importjobstatus' AND e.enumlabel = 'validating'
) THEN
ALTER TYPE importjobstatus ADD VALUE IF NOT EXISTS 'validating';
RAISE NOTICE 'Added validating to importjobstatus enum';
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_enum e
JOIN pg_type t ON e.enumtypid = t.oid
WHERE t.typname = 'importjobstatus' AND e.enumlabel = 'preview_ready'
) THEN
ALTER TYPE importjobstatus ADD VALUE IF NOT EXISTS 'preview_ready';
RAISE NOTICE 'Added preview_ready to importjobstatus enum';
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_enum e
JOIN pg_type t ON e.enumtypid = t.oid
WHERE t.typname = 'importjobstatus' AND e.enumlabel = 'rolled_back'
) THEN
ALTER TYPE importjobstatus ADD VALUE IF NOT EXISTS 'rolled_back';
RAISE NOTICE 'Added rolled_back to importjobstatus enum';
END IF;
END $$;
-- Add pending_validation, pre_validated, canceled, expired, abandoned to UserStatus enum if missing
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_enum e
JOIN pg_type t ON e.enumtypid = t.oid
WHERE t.typname = 'userstatus' AND e.enumlabel = 'pending_validation'
) THEN
ALTER TYPE userstatus ADD VALUE IF NOT EXISTS 'pending_validation';
RAISE NOTICE 'Added pending_validation to userstatus enum';
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_enum e
JOIN pg_type t ON e.enumtypid = t.oid
WHERE t.typname = 'userstatus' AND e.enumlabel = 'pre_validated'
) THEN
ALTER TYPE userstatus ADD VALUE IF NOT EXISTS 'pre_validated';
RAISE NOTICE 'Added pre_validated to userstatus enum';
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_enum e
JOIN pg_type t ON e.enumtypid = t.oid
WHERE t.typname = 'userstatus' AND e.enumlabel = 'canceled'
) THEN
ALTER TYPE userstatus ADD VALUE IF NOT EXISTS 'canceled';
RAISE NOTICE 'Added canceled to userstatus enum';
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_enum e
JOIN pg_type t ON e.enumtypid = t.oid
WHERE t.typname = 'userstatus' AND e.enumlabel = 'expired'
) THEN
ALTER TYPE userstatus ADD VALUE IF NOT EXISTS 'expired';
RAISE NOTICE 'Added expired to userstatus enum';
END IF;
IF NOT EXISTS (
SELECT 1 FROM pg_enum e
JOIN pg_type t ON e.enumtypid = t.oid
WHERE t.typname = 'userstatus' AND e.enumlabel = 'abandoned'
) THEN
ALTER TYPE userstatus ADD VALUE IF NOT EXISTS 'abandoned';
RAISE NOTICE 'Added abandoned to userstatus enum';
END IF;
END $$;
COMMIT;
\echo ''
\echo '=== VERIFICATION ==='
SELECT
(SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'users') as users_columns,
(SELECT COUNT(*) FROM information_schema.columns WHERE table_name = 'import_jobs') as import_jobs_columns,
(SELECT COUNT(*) FROM pg_enum e JOIN pg_type t ON e.enumtypid = t.oid WHERE t.typname = 'userstatus') as userstatus_values,
(SELECT COUNT(*) FROM pg_enum e JOIN pg_type t ON e.enumtypid = t.oid WHERE t.typname = 'importjobstatus') as importjobstatus_values;
\echo ''
\echo '✅ Missing fields have been added!'
\echo 'You can now run: alembic stamp head'

238
migrations/seed_data.sql Normal file
View File

@@ -0,0 +1,238 @@
-- ============================================================================
-- Seed Data for LOAF Membership Platform
-- Run this after creating the database schema
-- ============================================================================
BEGIN;
-- ============================================================================
-- STEP 1: Create Default Roles
-- ============================================================================
INSERT INTO roles (id, code, name, description, is_system_role, created_at, updated_at)
VALUES
(gen_random_uuid(), 'guest', 'Guest', 'Default role for new registrations', true, NOW(), NOW()),
(gen_random_uuid(), 'member', 'Member', 'Active paying members with full access', true, NOW(), NOW()),
(gen_random_uuid(), 'admin', 'Admin', 'Board members with management access', true, NOW(), NOW()),
(gen_random_uuid(), 'finance', 'Finance', 'Treasurer role with financial access', true, NOW(), NOW()),
(gen_random_uuid(), 'superadmin', 'Super Admin', 'Full system access', true, NOW(), NOW())
ON CONFLICT (code) DO NOTHING;
-- ============================================================================
-- STEP 2: Create Permissions
-- ============================================================================
INSERT INTO permissions (id, code, name, description, module, created_at)
VALUES
-- User Management Permissions
(gen_random_uuid(), 'users.view', 'View Users', 'View user list and profiles', 'users', NOW()),
(gen_random_uuid(), 'users.create', 'Create Users', 'Create new users', 'users', NOW()),
(gen_random_uuid(), 'users.edit', 'Edit Users', 'Edit user information', 'users', NOW()),
(gen_random_uuid(), 'users.delete', 'Delete Users', 'Delete users', 'users', NOW()),
(gen_random_uuid(), 'users.approve', 'Approve Users', 'Approve pending memberships', 'users', NOW()),
(gen_random_uuid(), 'users.import', 'Import Users', 'Import users from CSV/external sources', 'users', NOW()),
-- Event Management Permissions
(gen_random_uuid(), 'events.view', 'View Events', 'View event list and details', 'events', NOW()),
(gen_random_uuid(), 'events.create', 'Create Events', 'Create new events', 'events', NOW()),
(gen_random_uuid(), 'events.edit', 'Edit Events', 'Edit event information', 'events', NOW()),
(gen_random_uuid(), 'events.delete', 'Delete Events', 'Delete events', 'events', NOW()),
(gen_random_uuid(), 'events.publish', 'Publish Events', 'Publish/unpublish events', 'events', NOW()),
(gen_random_uuid(), 'events.manage_attendance', 'Manage Attendance', 'Mark event attendance', 'events', NOW()),
-- Financial Permissions
(gen_random_uuid(), 'finance.view', 'View Financial Data', 'View subscriptions and payments', 'finance', NOW()),
(gen_random_uuid(), 'finance.manage_plans', 'Manage Subscription Plans', 'Create/edit subscription plans', 'finance', NOW()),
(gen_random_uuid(), 'finance.manage_subscriptions', 'Manage Subscriptions', 'Manage user subscriptions', 'finance', NOW()),
(gen_random_uuid(), 'finance.view_reports', 'View Financial Reports', 'Access financial reports', 'finance', NOW()),
(gen_random_uuid(), 'finance.export', 'Export Financial Data', 'Export financial data', 'finance', NOW()),
-- Content Management Permissions
(gen_random_uuid(), 'content.newsletters', 'Manage Newsletters', 'Manage newsletter archives', 'content', NOW()),
(gen_random_uuid(), 'content.documents', 'Manage Documents', 'Manage bylaws and documents', 'content', NOW()),
(gen_random_uuid(), 'content.gallery', 'Manage Gallery', 'Manage event galleries', 'content', NOW()),
-- System Permissions
(gen_random_uuid(), 'system.settings', 'System Settings', 'Manage system settings', 'system', NOW()),
(gen_random_uuid(), 'system.roles', 'Manage Roles', 'Create/edit roles and permissions', 'system', NOW()),
(gen_random_uuid(), 'system.invitations', 'Manage Invitations', 'Send admin invitations', 'system', NOW()),
(gen_random_uuid(), 'system.storage', 'Manage Storage', 'View storage usage', 'system', NOW()),
(gen_random_uuid(), 'system.audit', 'View Audit Logs', 'View system audit logs', 'system', NOW())
ON CONFLICT (code) DO NOTHING;
-- ============================================================================
-- STEP 3: Assign Permissions to Roles
-- ============================================================================
-- Guest Role: No permissions (view-only through public pages)
-- No entries needed
-- Member Role: Limited permissions
INSERT INTO role_permissions (id, role, role_id, permission_id, created_at)
SELECT
gen_random_uuid(),
'member',
(SELECT id FROM roles WHERE code = 'member'),
p.id,
NOW()
FROM permissions p
WHERE p.code IN (
'events.view'
)
ON CONFLICT DO NOTHING;
-- Admin Role: Most permissions except financial
INSERT INTO role_permissions (id, role, role_id, permission_id, created_at)
SELECT
gen_random_uuid(),
'admin',
(SELECT id FROM roles WHERE code = 'admin'),
p.id,
NOW()
FROM permissions p
WHERE p.code IN (
-- User Management
'users.view', 'users.create', 'users.edit', 'users.approve', 'users.import',
-- Event Management
'events.view', 'events.create', 'events.edit', 'events.delete', 'events.publish', 'events.manage_attendance',
-- Content Management
'content.newsletters', 'content.documents', 'content.gallery',
-- System (limited)
'system.invitations', 'system.storage'
)
ON CONFLICT DO NOTHING;
-- Finance Role: Financial permissions + basic access
INSERT INTO role_permissions (id, role, role_id, permission_id, created_at)
SELECT
gen_random_uuid(),
'finance',
(SELECT id FROM roles WHERE code = 'finance'),
p.id,
NOW()
FROM permissions p
WHERE p.code IN (
-- Financial
'finance.view', 'finance.manage_plans', 'finance.manage_subscriptions', 'finance.view_reports', 'finance.export',
-- Basic Access
'users.view', 'events.view'
)
ON CONFLICT DO NOTHING;
-- Superadmin Role: All permissions
INSERT INTO role_permissions (id, role, role_id, permission_id, created_at)
SELECT
gen_random_uuid(),
'superadmin',
(SELECT id FROM roles WHERE code = 'superadmin'),
p.id,
NOW()
FROM permissions p
ON CONFLICT DO NOTHING;
-- ============================================================================
-- STEP 4: Create Subscription Plans
-- ============================================================================
INSERT INTO subscription_plans (id, name, description, price_cents, billing_cycle, custom_cycle_enabled, minimum_price_cents, allow_donation, is_active, created_at, updated_at)
VALUES
-- Annual Individual Membership
(
gen_random_uuid(),
'Annual Individual Membership',
'Standard annual membership for one person. Includes access to all LOAF events, member directory, and exclusive content.',
6000, -- $60.00
'annual',
false,
6000,
false,
true,
NOW(),
NOW()
),
-- Annual Group Membership
(
gen_random_uuid(),
'Annual Group Membership',
'Annual membership for two people living at the same address. Both members receive full access to all LOAF benefits.',
10000, -- $100.00
'annual',
false,
10000,
false,
true,
NOW(),
NOW()
),
-- Pay What You Want (with minimum)
(
gen_random_uuid(),
'Pay What You Want Membership',
'Choose your own annual membership amount. Minimum $30. Additional contributions help support our scholarship fund.',
3000, -- $30.00 minimum
'annual',
true, -- Allow custom amount
3000, -- Minimum $30
true, -- Additional amount is treated as donation
true,
NOW(),
NOW()
)
ON CONFLICT DO NOTHING;
-- ============================================================================
-- STEP 5: Initialize Storage Usage (if not already done)
-- ============================================================================
INSERT INTO storage_usage (id, total_bytes_used, max_bytes_allowed, last_calculated_at, created_at, updated_at)
VALUES (gen_random_uuid(), 0, 107374182400, NOW(), NOW(), NOW()) -- 100GB limit
ON CONFLICT DO NOTHING;
COMMIT;
-- ============================================================================
-- Success Message
-- ============================================================================
\echo '✅ Seed data created successfully!'
\echo ''
\echo 'Created:'
\echo ' - 5 default roles (guest, member, admin, finance, superadmin)'
\echo ' - 25 permissions across 5 modules'
\echo ' - Role-permission mappings'
\echo ' - 3 subscription plans'
\echo ' - Storage usage initialization'
\echo ''
\echo 'Next steps:'
\echo ' 1. Create superadmin user (see instructions below)'
\echo ' 2. Configure Stripe price IDs in subscription_plans'
\echo ' 3. Start the application'
\echo ''
\echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'
\echo 'CREATE SUPERADMIN USER:'
\echo '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'
\echo ''
\echo 'Generate password hash in Python:'
\echo ' python3 -c "import bcrypt; print(bcrypt.hashpw(b\"your-password\", bcrypt.gensalt()).decode())"'
\echo ''
\echo 'Then run:'
\echo ' psql -U postgres -d loaf_new'
\echo ''
\echo 'INSERT INTO users ('
\echo ' id, email, password_hash, first_name, last_name,'
\echo ' status, role, email_verified, created_at, updated_at'
\echo ') VALUES ('
\echo ' gen_random_uuid(),'
\echo ' '\''admin@loafmembers.org'\'','
\echo ' '\''$2b$12$YOUR_BCRYPT_HASH_HERE'\'','
\echo ' '\''Admin'\'','
\echo ' '\''User'\'','
\echo ' '\''active'\'','
\echo ' '\''superadmin'\'','
\echo ' true,'
\echo ' NOW(),'
\echo ' NOW()'
\echo ');'
\echo ''