# Migration Plan — Nxtgauge Database Redesign This document outlines the step-by-step migration strategy to transform the current schema to the target schema. --- ## Migration Principles 1. **Additive First** — Always create new tables before modifying existing ones 2. **No Data Loss** — Preserve all existing data during migration 3. **Reversible** — Each step can be rolled back if needed 4. **Service-by-Service** — Migrate one domain at a time 5. **Backward Compatible** — Keep old tables until services are updated --- ## Migration Phases ### Phase 1: Core Infrastructure (Week 1) ### Phase 2: Profile Domain (Week 2) ### Phase 3: Portfolio Domain (Week 2-3) ### Phase 4: Verification & Approval (Week 3) ### Phase 5: Marketplace (Week 3-4) ### Phase 6: Finance (Week 4) ### Phase 7: Audit & Cleanup (Week 5) --- ## Phase 1: Core Infrastructure ### Step 1.1: Create New Tables **Files:** `20260415000001_create_user_sessions.up.sql` ```sql CREATE TABLE IF NOT EXISTS user_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, session_token TEXT UNIQUE NOT NULL, ip_address TEXT, user_agent TEXT, expires_at TIMESTAMPTZ NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_user_sessions_user_id ON user_sessions(user_id); CREATE INDEX IF NOT EXISTS idx_user_sessions_token ON user_sessions(session_token); ``` ### Step 1.2: Add Missing Columns to users **Files:** `20260415000002_add_users_missing_columns.up.sql` ```sql ALTER TABLE users ADD COLUMN IF NOT EXISTS account_type TEXT DEFAULT 'INDIVIDUAL'; ALTER TABLE users ADD COLUMN IF NOT EXISTS last_login_at TIMESTAMPTZ; ALTER TABLE users ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); UPDATE users SET updated_at = COALESCE(updated_at, created_at, NOW()); ``` ### Step 1.3: Update departments **Files:** `20260415000003_update_departments.up.sql` ```sql ALTER TABLE departments ADD COLUMN IF NOT EXISTS code VARCHAR(64); ALTER TABLE departments ADD COLUMN IF NOT EXISTS description TEXT; ALTER TABLE departments ADD COLUMN IF NOT EXISTS department_head VARCHAR(255); ALTER TABLE departments ADD COLUMN IF NOT EXISTS department_email VARCHAR(255); ALTER TABLE departments ADD COLUMN IF NOT EXISTS visibility VARCHAR(20) DEFAULT 'INTERNAL'; ALTER TABLE departments ADD COLUMN IF NOT EXISTS transfers_enabled BOOLEAN DEFAULT false; ALTER TABLE departments ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); CREATE UNIQUE INDEX IF NOT EXISTS idx_departments_code ON departments(LOWER(code)) WHERE code IS NOT NULL; ``` ### Step 1.4: Update designations **Files:** `20260415000004_update_designations.up.sql` ```sql ALTER TABLE designations ADD COLUMN IF NOT EXISTS code VARCHAR(64); ALTER TABLE designations ADD COLUMN IF NOT EXISTS department_id UUID REFERENCES departments(id); ALTER TABLE designations ADD COLUMN IF NOT EXISTS description TEXT; ALTER TABLE designations ADD COLUMN IF NOT EXISTS level VARCHAR(100); ALTER TABLE designations ADD COLUMN IF NOT EXISTS can_manage_team BOOLEAN DEFAULT false; ALTER TABLE designations ADD COLUMN IF NOT EXISTS can_approve BOOLEAN DEFAULT false; ALTER TABLE designations ADD COLUMN IF NOT EXISTS is_active BOOLEAN DEFAULT true; ALTER TABLE designations ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); CREATE UNIQUE INDEX IF NOT EXISTS idx_designations_code ON designations(LOWER(code)) WHERE code IS NOT NULL; ``` ### Step 1.5: Update employees **Files:** `20260415000005_update_employees.up.sql` ```sql ALTER TABLE employees ADD COLUMN IF NOT EXISTS joining_date DATE; ALTER TABLE employees ADD COLUMN IF NOT EXISTS employment_status VARCHAR(50) DEFAULT 'ACTIVE'; ALTER TABLE employees ADD COLUMN IF NOT EXISTS manager_employee_id UUID REFERENCES employees(id); ALTER TABLE employees ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); ``` ### Step 1.6: Update roles **Files:** `20260415000006_update_roles.up.sql` ```sql ALTER TABLE roles ADD COLUMN IF NOT EXISTS description TEXT; ALTER TABLE roles ADD COLUMN IF NOT EXISTS can_approve_requests BOOLEAN DEFAULT false; ALTER TABLE roles ADD COLUMN IF NOT EXISTS can_manage_system_settings BOOLEAN DEFAULT false; ``` --- ## Phase 2: Profile Domain (CRITICAL) ### Step 2.1: Create user_role_profiles Root Table **Files:** `20260415010001_create_user_role_profiles.up.sql` ```sql CREATE TABLE IF NOT EXISTS user_role_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, role_key VARCHAR(50) NOT NULL, display_name TEXT, bio TEXT, location TEXT, avatar_url TEXT, phone TEXT, email TEXT, status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE', verification_status VARCHAR(50) DEFAULT 'PENDING', approval_status VARCHAR(50) DEFAULT 'PENDING', rejection_reason TEXT, approved_at TIMESTAMPTZ, verified_at TIMESTAMPTZ, is_profile_public BOOLEAN DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(user_id, role_key) ); CREATE INDEX IF NOT EXISTS idx_user_role_profiles_user_id ON user_role_profiles(user_id); CREATE INDEX IF NOT EXISTS idx_user_role_profiles_role_key ON user_role_profiles(role_key); CREATE INDEX IF NOT EXISTS idx_user_role_profiles_status ON user_role_profiles(status); ``` ### Step 2.2: Backfill user_role_profiles from Existing Data **Files:** `20260415010002_backfill_user_role_profiles.up.sql` ```sql -- Backfill from photographer_profiles INSERT INTO user_role_profiles (id, user_id, role_key, display_name, bio, location, status, approved_at, created_at, updated_at) SELECT gen_random_uuid(), p.user_id, 'photographer', COALESCE(p.display_name, ''), p.bio, p.location, COALESCE(p.status, 'ACTIVE'), p.approved_at, p.created_at, COALESCE(p.updated_at, NOW()) FROM photographer_profiles p ON CONFLICT (user_id, 'photographer') DO NOTHING; -- Repeat for all other profession tables... -- Backfill from company_profiles INSERT INTO user_role_profiles (id, user_id, role_key, display_name, bio, location, status, created_at, updated_at) SELECT gen_random_uuid(), cp.user_id, 'company', cp.company_name, cp.bio, NULL, COALESCE(cp.status, 'ACTIVE'), cp.created_at, COALESCE(cp.updated_at, NOW()) FROM company_profiles cp ON CONFLICT (user_id, 'company') DO NOTHING; -- Backfill from customer_profiles INSERT INTO user_role_profiles (id, user_id, role_key, display_name, location, status, created_at, updated_at) SELECT gen_random_uuid(), cp.user_id, 'customer', COALESCE(cp.full_name, ''), cp.city, COALESCE(cp.status, 'ACTIVE'), cp.created_at, COALESCE(cp.updated_at, NOW()) FROM customer_profiles cp ON CONFLICT (user_id, 'customer') DO NOTHING; ``` ### Step 2.3: Add user_role_profile_id to Extension Tables **Files:** `20260415010003_add_user_role_profile_id.up.sql` ```sql -- Add temporary column for mapping ALTER TABLE photographer_profiles ADD COLUMN IF NOT EXISTS user_role_profile_id UUID; -- Update with backfilled data UPDATE photographer_profiles p SET user_role_profile_id = urp.id FROM user_role_profiles urp WHERE p.user_id = urp.user_id AND urp.role_key = 'photographer'; -- Add FK constraint ALTER TABLE photographer_profiles ADD CONSTRAINT fk_photographer_profiles_user_role_profile FOREIGN KEY (user_role_profile_id) REFERENCES user_role_profiles(id); -- Repeat for all extension tables... ``` ### Step 2.4: Update Extension Tables Schema **Files:** `20260415010004_update_extension_tables.up.sql` ```sql -- Remove forbidden external links ALTER TABLE developer_profiles DROP COLUMN IF EXISTS github_url; ALTER TABLE developer_profiles DROP COLUMN IF EXISTS portfolio_url; ALTER TABLE video_editor_profiles DROP COLUMN IF EXISTS reel_url; ALTER TABLE graphic_designer_profiles DROP COLUMN IF EXISTS portfolio_url; ALTER TABLE photographer_profiles DROP COLUMN IF EXISTS portfolio_url; -- Remove custom_data (preserve if needed) ALTER TABLE photographer_profiles DROP COLUMN IF EXISTS custom_data; ALTER TABLE tutor_profiles DROP COLUMN IF EXISTS custom_data; -- ... repeat for all tables -- Rename columns for consistency ALTER TABLE tutor_profiles RENAME COLUMN subjects_taught TO subjects; ``` --- ## Phase 3: Portfolio Domain ### Step 3.1: Update portfolio_items **Files:** `20260415020001_update_portfolio_items.up.sql` ```sql -- Add user_role_profile_id ALTER TABLE portfolio_items ADD COLUMN IF NOT EXISTS user_role_profile_id UUID; ALTER TABLE portfolio_items ADD COLUMN IF NOT EXISTS display_order INTEGER DEFAULT 0; -- Backfill from professionals UPDATE portfolio_items pi SET user_role_profile_id = urp.id FROM user_role_profiles urp WHERE pi.professional_id = urp.user_id; -- Remove old columns ALTER TABLE portfolio_items DROP COLUMN IF EXISTS professional_id; ALTER TABLE portfolio_items DROP COLUMN IF EXISTS user_id; ALTER TABLE portfolio_items DROP COLUMN IF EXISTS profession_key; ``` ### Step 3.2: Update services **Files:** `20260415020002_update_services.up.sql` ```sql ALTER TABLE services ADD COLUMN IF NOT EXISTS user_role_profile_id UUID; UPDATE services s SET user_role_profile_id = urp.id FROM user_role_profiles urp WHERE s.professional_id = urp.user_id; ALTER TABLE services DROP COLUMN IF EXISTS professional_id; ALTER TABLE services DROP COLUMN IF EXISTS user_id; ALTER TABLE services DROP COLUMN IF EXISTS profession_key; ``` --- ## Phase 4: Verification & Approval ### Step 4.1: Create Verification Tables **Files:** `20260415030001_create_verification_tables.up.sql` ```sql CREATE TABLE IF NOT EXISTS verification_requests ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_role_profile_id UUID NOT NULL REFERENCES user_role_profiles(id), verification_type VARCHAR(50) NOT NULL, status VARCHAR(50) NOT NULL DEFAULT 'PENDING', submitted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), reviewed_at TIMESTAMPTZ, reviewed_by_user_id UUID REFERENCES users(id), remarks TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS verification_documents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), verification_request_id UUID NOT NULL REFERENCES verification_requests(id) ON DELETE CASCADE, document_type VARCHAR(100) NOT NULL, file_url TEXT NOT NULL, file_name TEXT, mime_type TEXT, status VARCHAR(50) DEFAULT 'PENDING', uploaded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), reviewed_at TIMESTAMPTZ, reviewed_by_user_id UUID REFERENCES users(id), remarks TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS verification_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), verification_request_id UUID NOT NULL REFERENCES verification_requests(id), action VARCHAR(50) NOT NULL, old_status VARCHAR(50), new_status VARCHAR(50), acted_by_user_id UUID REFERENCES users(id), remarks TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); ``` ### Step 4.2: Create Approval Tables **Files:** `20260415030002_create_approval_tables.up.sql` ```sql CREATE TABLE IF NOT EXISTS approval_requests ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), entity_type VARCHAR(50) NOT NULL, entity_id UUID NOT NULL, approval_type VARCHAR(50), status VARCHAR(50) NOT NULL DEFAULT 'PENDING', submitted_by_user_id UUID REFERENCES users(id), reviewed_by_user_id UUID REFERENCES users(id), submitted_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), reviewed_at TIMESTAMPTZ, remarks TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS approval_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), approval_request_id UUID NOT NULL REFERENCES approval_requests(id), action VARCHAR(50) NOT NULL, old_status VARCHAR(50), new_status VARCHAR(50), acted_by_user_id UUID REFERENCES users(id), remarks TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); ``` --- ## Phase 5: Marketplace ### Step 5.1: Rename and Update jobs-related Tables **Files:** `20260415040001_update_jobs_tables.up.sql` ```sql -- Rename applications to job_applications ALTER TABLE applications RENAME TO job_applications; -- Add new columns ALTER TABLE job_applications ADD COLUMN IF NOT EXISTS applicant_user_id UUID; -- Backfill from job_seeker_profiles UPDATE job_applications ja SET applicant_user_id = ( SELECT user_id FROM job_seeker_profiles jsp WHERE jsp.id = ja.job_seeker_id ); -- Add FK ALTER TABLE job_applications ADD CONSTRAINT fk_job_applications_applicant FOREIGN KEY (applicant_user_id) REFERENCES users(id); -- Remove old columns ALTER TABLE job_applications DROP COLUMN IF EXISTS job_seeker_id; ALTER TABLE job_applications DROP COLUMN IF EXISTS cover_letter; ALTER TABLE job_applications DROP COLUMN IF EXISTS resume_url; ALTER TABLE job_applications DROP COLUMN IF EXISTS contact_viewed; -- Rename cover_note if needed ALTER TABLE job_applications RENAME COLUMN cover_letter TO cover_note; ``` ### Step 5.2: Update jobs Table **Files:** `20260415040002_update_jobs.up.sql` ```sql -- Add new columns ALTER TABLE jobs ADD COLUMN IF NOT EXISTS posted_by_user_id UUID; ALTER TABLE jobs ADD COLUMN IF NOT EXISTS mode_of_work VARCHAR(50); ALTER TABLE jobs ADD COLUMN IF NOT EXISTS budget_inr INTEGER; ALTER TABLE jobs ADD COLUMN IF NOT EXISTS salary_range_json JSONB; -- Add FK ALTER TABLE jobs ADD CONSTRAINT fk_jobs_posted_by FOREIGN KEY (posted_by_user_id) REFERENCES users(id); -- Add updated_at ALTER TABLE jobs ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); UPDATE jobs SET updated_at = COALESCE(updated_at, created_at, NOW()); ``` ### Step 5.3: Rename requirements to leads **Files:** `20260415040003_rename_requirements_to_leads.up.sql` ```sql -- Rename table ALTER TABLE requirements RENAME TO leads; -- Rename columns ALTER TABLE leads RENAME COLUMN customer_id TO created_by_user_id; ALTER TABLE leads RENAME COLUMN preferred_date TO required_date; -- Add FK ALTER TABLE leads ADD CONSTRAINT fk_leads_created_by FOREIGN KEY (created_by_user_id) REFERENCES users(id); -- Add updated_at ALTER TABLE leads ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); ``` ### Step 5.4: Update lead_requests **Files:** `20260415040004_update_lead_requests.up.sql` ```sql -- Add user_role_profile_id ALTER TABLE lead_requests ADD COLUMN IF NOT EXISTS user_role_profile_id UUID; -- Backfill from professionals UPDATE lead_requests lr SET user_role_profile_id = ( SELECT id FROM user_role_profiles urp WHERE urp.user_id = ( SELECT user_id FROM professionals p WHERE p.id = lr.professional_id ) ); -- Add FK ALTER TABLE lead_requests ADD CONSTRAINT fk_lead_requests_profile FOREIGN KEY (user_role_profile_id) REFERENCES user_role_profiles(id); -- Rename columns ALTER TABLE lead_requests ADD COLUMN IF NOT EXISTS remarks TEXT; ALTER TABLE lead_requests ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); ``` --- ## Phase 6: Finance ### Step 6.1: Create Order Tables **Files:** `20260415050001_create_order_tables.up.sql` ```sql CREATE TABLE IF NOT EXISTS orders ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id), order_type VARCHAR(50) NOT NULL, subtotal_inr INTEGER NOT NULL DEFAULT 0, discount_inr INTEGER NOT NULL DEFAULT 0, tax_inr INTEGER NOT NULL DEFAULT 0, total_inr INTEGER NOT NULL DEFAULT 0, status VARCHAR(50) NOT NULL DEFAULT 'PENDING', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS order_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), order_id UUID NOT NULL REFERENCES orders(id) ON DELETE CASCADE, item_type VARCHAR(50) NOT NULL, item_id UUID, item_name TEXT NOT NULL, quantity INTEGER NOT NULL DEFAULT 1, unit_price_inr INTEGER NOT NULL, total_price_inr INTEGER NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_orders_user_id ON orders(user_id); CREATE INDEX IF NOT EXISTS idx_order_items_order_id ON order_items(order_id); ``` ### Step 6.2: Update Existing Finance Tables **Files:** `20260415050002_update_finance_tables.up.sql` ```sql -- Update tracecoin_wallets ALTER TABLE tracecoin_wallets RENAME COLUMN balance TO current_balance; ALTER TABLE tracecoin_wallets ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); -- Update tracecoin_ledger ALTER TABLE tracecoin_ledger ADD COLUMN IF NOT EXISTS balance_after INTEGER; ALTER TABLE tracecoin_ledger ADD COLUMN IF NOT EXISTS remarks TEXT; ALTER TABLE tracecoin_ledger RENAME COLUMN type TO transaction_type; ALTER TABLE tracecoin_ledger RENAME COLUMN reason TO reference_type; -- Update coupons ALTER TABLE coupons ADD COLUMN IF NOT EXISTS max_discount_inr INTEGER; ALTER TABLE coupons ADD COLUMN IF NOT EXISTS min_order_value_inr INTEGER DEFAULT 0; ALTER TABLE coupons ADD COLUMN IF NOT EXISTS valid_from TIMESTAMPTZ DEFAULT NOW(); ALTER TABLE coupons ADD COLUMN IF NOT EXISTS valid_to TIMESTAMPTZ; -- Rename coupon_uses to coupon_redemptions ALTER TABLE coupon_uses RENAME TO coupon_redemptions; ALTER TABLE coupon_redemptions ADD COLUMN IF NOT EXISTS order_id UUID; ALTER TABLE coupon_redemptions ADD COLUMN IF NOT EXISTS discount_amount_inr INTEGER; ALTER TABLE coupon_redemptions RENAME COLUMN used_at TO redeemed_at; ``` ### Step 6.3: Create Payment Infrastructure **Files:** `20260415050003_create_payment_tables.up.sql` ```sql CREATE TABLE IF NOT EXISTS payment_gateway_configs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), gateway_key VARCHAR(50) NOT NULL, display_name VARCHAR(255), config_json JSONB, is_active BOOLEAN DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS payment_transactions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), payment_id UUID NOT NULL REFERENCES payments(id), transaction_type VARCHAR(50) NOT NULL, provider_reference TEXT, request_payload_json JSONB, response_payload_json JSONB, status VARCHAR(50) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS tax_rules ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, tax_type VARCHAR(50) NOT NULL, tax_rate DECIMAL(5,2) NOT NULL, applies_to VARCHAR(50), is_active BOOLEAN DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); ``` --- ## Phase 7: Audit & Cleanup ### Step 7.1: Create Audit Tables **Files:** `20260415060001_create_audit_tables.up.sql` ```sql CREATE TABLE IF NOT EXISTS audit_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), actor_user_id UUID REFERENCES users(id), actor_employee_id UUID REFERENCES employees(id), actor_type VARCHAR(50), action VARCHAR(100) NOT NULL, entity_type VARCHAR(100), entity_id UUID, entity_label TEXT, module_key VARCHAR(100), source_type VARCHAR(50), source_id UUID, request_id UUID, correlation_id UUID, ip_address TEXT, user_agent TEXT, status VARCHAR(50) NOT NULL DEFAULT 'SUCCESS', summary TEXT, metadata_json JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS audit_log_changes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), audit_log_id UUID NOT NULL REFERENCES audit_logs(id) ON DELETE CASCADE, field_name TEXT NOT NULL, old_value_text TEXT, new_value_text TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_audit_logs_actor ON audit_logs(actor_user_id); CREATE INDEX IF NOT EXISTS idx_audit_logs_entity ON audit_logs(entity_type, entity_id); CREATE INDEX IF NOT EXISTS idx_audit_logs_module ON audit_logs(module_key); CREATE INDEX IF NOT EXISTS idx_audit_logs_created ON audit_logs(created_at); ``` ### Step 7.2: Create Missing KB Tables **Files:** `20260415060002_create_kb_tables.up.sql` ```sql CREATE TABLE IF NOT EXISTS kb_sections ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), category_id UUID NOT NULL REFERENCES kb_categories(id) ON DELETE CASCADE, name VARCHAR(255) NOT NULL, slug VARCHAR(255) NOT NULL, description TEXT, display_order INTEGER DEFAULT 0, is_active BOOLEAN DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS kb_article_feedback ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), article_id UUID NOT NULL REFERENCES kb_articles(id) ON DELETE CASCADE, user_id UUID REFERENCES users(id), is_helpful BOOLEAN, feedback_text TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Update kb_articles ALTER TABLE kb_articles ADD COLUMN IF NOT EXISTS section_id UUID REFERENCES kb_sections(id); ALTER TABLE kb_articles ADD COLUMN IF NOT EXISTS article_type VARCHAR(50) DEFAULT 'HOW_TO'; ALTER TABLE kb_articles ADD COLUMN IF NOT EXISTS audience_type VARCHAR(50) DEFAULT 'ALL'; ALTER TABLE kb_articles RENAME COLUMN body TO content_markdown; ALTER TABLE kb_articles RENAME COLUMN created_by TO author_user_id; ALTER TABLE kb_articles RENAME COLUMN is_published TO status; ``` ### Step 7.3: Create Notification Infrastructure **Files:** `20260415060003_create_notification_tables.up.sql` ```sql CREATE TABLE IF NOT EXISTS notification_templates ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), template_key VARCHAR(100) NOT NULL UNIQUE, channel VARCHAR(50) NOT NULL, title_template TEXT, body_template TEXT, is_active BOOLEAN DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE TABLE IF NOT EXISTS smtp_configs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), provider_name VARCHAR(100), host VARCHAR(255), port INTEGER, username TEXT, encryption_mode VARCHAR(20), from_name VARCHAR(255), from_email VARCHAR(255), is_default BOOLEAN DEFAULT false, is_active BOOLEAN DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Update notifications ALTER TABLE notifications ADD COLUMN IF NOT EXISTS channel VARCHAR(50) DEFAULT 'IN_APP'; ALTER TABLE notifications ADD COLUMN IF NOT EXISTS related_entity_type VARCHAR(50); ALTER TABLE notifications RENAME COLUMN reference_id TO related_entity_id; ALTER TABLE notifications RENAME COLUMN is_read TO status; ``` ### Step 7.4: Create Dashboard Widgets **Files:** `20260415060004_create_dashboard_tables.up.sql` ```sql CREATE TABLE IF NOT EXISTS dashboard_widgets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), dashboard_config_id UUID NOT NULL REFERENCES dashboard_configs(id) ON DELETE CASCADE, widget_key VARCHAR(100) NOT NULL, widget_title VARCHAR(255), config_json JSONB, display_order INTEGER DEFAULT 0, width_units INTEGER DEFAULT 1, height_units INTEGER DEFAULT 1, is_visible BOOLEAN DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_dashboard_widgets_config ON dashboard_widgets(dashboard_config_id); ``` --- ## Rollback Procedures ### Rollback Phase 1 ```sql -- Drop new tables DROP TABLE IF EXISTS user_sessions; -- Revert column changes (use down migrations) ``` ### Rollback Phase 2 ```sql -- DO NOT rollback user_role_profiles if data exists -- Instead, mark as deprecated and keep parallel structure ``` --- ## Testing Strategy 1. **Unit Tests** — Test each migration script independently 2. **Integration Tests** — Test application functionality after each phase 3. **Data Validation** — Verify all data is correctly migrated 4. **Performance Tests** — Ensure indexes perform well 5. **Rollback Tests** — Test rollback procedures in staging --- ## Pre-Migration Checklist - [ ] Backup production database - [ ] Test migrations on staging environment - [ ] Verify all foreign key relationships - [ ] Check for circular dependencies - [ ] Plan maintenance window for critical migrations - [ ] Notify stakeholders of potential downtime --- ## Post-Migration Checklist - [ ] Verify all data integrity - [ ] Check application logs for errors - [ ] Update documentation - [ ] Remove deprecated tables (after full validation) - [ ] Archive old migration files