-- 1. ROLES CREATE TABLE IF NOT EXISTS roles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), key VARCHAR(255) UNIQUE NOT NULL, name VARCHAR(255) NOT NULL, audience VARCHAR(50) NOT NULL, -- INTERNAL or EXTERNAL is_active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 2. ONBOARDING CONFIGS CREATE TABLE IF NOT EXISTS onboarding_configs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE, schema_json JSONB NOT NULL, version INTEGER NOT NULL DEFAULT 1, is_active BOOLEAN NOT NULL DEFAULT true, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Only one active onboarding config per role at a time CREATE UNIQUE INDEX IF NOT EXISTS idx_active_onboarding_per_role ON onboarding_configs(role_id) WHERE is_active = true; -- 3. DASHBOARD CONFIGS CREATE TABLE IF NOT EXISTS dashboard_configs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE, audience VARCHAR(50) NOT NULL, -- INTERNAL or EXTERNAL config_json JSONB NOT NULL, version INTEGER NOT NULL DEFAULT 1, is_active BOOLEAN NOT NULL DEFAULT true, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Only one active dashboard config per role+audience combination CREATE UNIQUE INDEX IF NOT EXISTS idx_active_dashboard_per_role_audience ON dashboard_configs(role_id, audience) WHERE is_active = true; -- 4. RUNTIME CONFIGS CREATE TABLE IF NOT EXISTS runtime_configs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE, config_json JSONB NOT NULL, version INTEGER NOT NULL DEFAULT 1, is_active BOOLEAN NOT NULL DEFAULT true, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Only one active runtime config per role at a time CREATE UNIQUE INDEX IF NOT EXISTS idx_active_runtime_per_role ON runtime_configs(role_id) WHERE is_active = true; -- 1. USERS 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, status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE', -- ACTIVE, PENDING, SUSPENDED role_id UUID REFERENCES roles(id) ON DELETE SET NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 2. REFRESH TOKENS CREATE TABLE IF NOT EXISTS refresh_tokens ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, token_hash VARCHAR(255) UNIQUE NOT NULL, expires_at TIMESTAMPTZ NOT NULL, revoked BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Index for fast token lookups CREATE INDEX IF NOT EXISTS idx_refresh_tokens_hash ON refresh_tokens(token_hash); CREATE TABLE IF NOT EXISTS photographer_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- Photographer Specific Fields portfolio_url VARCHAR(255), equipment_list TEXT, years_of_experience INT, hourly_rate INTEGER, -- in paise (INR × 100) specialties TEXT[], -- e.g., ["wedding", "portrait", "commercial"] created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- Ensure a user can only have one photographer profile UNIQUE(user_id) ); CREATE TABLE IF NOT EXISTS tutor_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- Tutor Specific Fields subjects_taught TEXT[], -- e.g., ["math", "physics", "computer science"] education_level VARCHAR(255), certifications TEXT, years_of_experience INT, hourly_rate INTEGER, -- in paise (INR × 100) created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- Ensure a user can only have one tutor profile UNIQUE(user_id) ); CREATE TABLE IF NOT EXISTS company_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- Company Specific Fields company_name VARCHAR(255) NOT NULL, registration_number VARCHAR(100), industry VARCHAR(150), website_url VARCHAR(255), employee_count INT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- Ensure a user can only have one company profile UNIQUE(user_id) ); CREATE TABLE IF NOT EXISTS job_seeker_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- Generic Fields for Job Seeker bio TEXT, experience_years INT, custom_data JSONB DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(user_id) ); CREATE TABLE IF NOT EXISTS customer_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- Generic Fields for Customer bio TEXT, experience_years INT, custom_data JSONB DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(user_id) ); CREATE TABLE IF NOT EXISTS makeup_artist_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- Generic Fields for Makeup Artist bio TEXT, experience_years INT, custom_data JSONB DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(user_id) ); CREATE TABLE IF NOT EXISTS developer_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- Generic Fields for Developer bio TEXT, experience_years INT, custom_data JSONB DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(user_id) ); CREATE TABLE IF NOT EXISTS video_editor_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- Generic Fields for Video Editor bio TEXT, experience_years INT, custom_data JSONB DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(user_id) ); CREATE TABLE IF NOT EXISTS graphic_designer_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- Generic Fields for Graphic Designer bio TEXT, experience_years INT, custom_data JSONB DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(user_id) ); CREATE TABLE IF NOT EXISTS social_media_manager_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- Generic Fields for Social Media Manager bio TEXT, experience_years INT, custom_data JSONB DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(user_id) ); CREATE TABLE IF NOT EXISTS fitness_trainer_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- Generic Fields for Fitness Trainer bio TEXT, experience_years INT, custom_data JSONB DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(user_id) ); CREATE TABLE IF NOT EXISTS catering_service_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, -- Generic Fields for Catering Service bio TEXT, experience_years INT, custom_data JSONB DEFAULT '{}'::jsonb, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(user_id) ); -- Add missing columns to users table ALTER TABLE users ADD COLUMN IF NOT EXISTS full_name VARCHAR(255), ADD COLUMN IF NOT EXISTS phone VARCHAR(20) UNIQUE, ADD COLUMN IF NOT EXISTS email_verified BOOLEAN NOT NULL DEFAULT false, ADD COLUMN IF NOT EXISTS phone_verified BOOLEAN NOT NULL DEFAULT false, ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMPTZ; -- user_roles: many-to-many, a user can hold multiple external roles CREATE TABLE IF NOT EXISTS user_roles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE, status VARCHAR(50) NOT NULL DEFAULT 'PENDING', -- PENDING, APPROVED, REJECTED, SUSPENDED approved_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(user_id, role_id) ); -- role_permissions CREATE TABLE IF NOT EXISTS role_permissions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE, permission_key VARCHAR(100) NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(role_id, permission_key) ); -- departments for internal staff CREATE TABLE IF NOT EXISTS departments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(100) NOT NULL UNIQUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- designations for internal staff CREATE TABLE IF NOT EXISTS designations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(100) NOT NULL UNIQUE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- employees (internal staff records) CREATE TABLE IF NOT EXISTS employees ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, role_id UUID NOT NULL REFERENCES roles(id), department_id UUID REFERENCES departments(id), designation_id UUID REFERENCES designations(id), employee_code VARCHAR(50), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- onboarding_submissions: tracks verification submissions CREATE TABLE IF NOT EXISTS onboarding_submissions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, role_id UUID NOT NULL REFERENCES roles(id), config_id UUID REFERENCES onboarding_configs(id), data_json JSONB, status VARCHAR(50) NOT NULL DEFAULT 'DRAFT', submitted_at TIMESTAMPTZ, reviewed_at TIMESTAMPTZ, reviewed_by UUID REFERENCES users(id), rejection_reason TEXT, document_request TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- submission_documents: uploaded files for onboarding CREATE TABLE IF NOT EXISTS submission_documents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), submission_id UUID NOT NULL REFERENCES onboarding_submissions(id) ON DELETE CASCADE, document_type VARCHAR(100) NOT NULL, file_url VARCHAR(500) NOT NULL, file_name VARCHAR(255), uploaded_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_user_roles_user_id ON user_roles(user_id); CREATE INDEX IF NOT EXISTS idx_user_roles_status ON user_roles(status); CREATE INDEX IF NOT EXISTS idx_onboarding_submissions_user_id ON onboarding_submissions(user_id); CREATE INDEX IF NOT EXISTS idx_onboarding_submissions_status ON onboarding_submissions(status); -- Complete company profile (replacing the minimal stub) ALTER TABLE company_profiles ADD COLUMN IF NOT EXISTS business_type VARCHAR(100), ADD COLUMN IF NOT EXISTS gst_number VARCHAR(50), ADD COLUMN IF NOT EXISTS contact_name VARCHAR(255), ADD COLUMN IF NOT EXISTS contact_email VARCHAR(255), ADD COLUMN IF NOT EXISTS contact_phone VARCHAR(20), ADD COLUMN IF NOT EXISTS address_line1 VARCHAR(500), ADD COLUMN IF NOT EXISTS city VARCHAR(100), ADD COLUMN IF NOT EXISTS state VARCHAR(100), ADD COLUMN IF NOT EXISTS country VARCHAR(100) NOT NULL DEFAULT 'India', ADD COLUMN IF NOT EXISTS postal_code VARCHAR(20), ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE', ADD COLUMN IF NOT EXISTS free_job_slots INTEGER NOT NULL DEFAULT 1, ADD COLUMN IF NOT EXISTS purchased_job_slots INTEGER NOT NULL DEFAULT 0, ADD COLUMN IF NOT EXISTS free_contact_views INTEGER NOT NULL DEFAULT 30, ADD COLUMN IF NOT EXISTS purchased_contact_views INTEGER NOT NULL DEFAULT 0; -- Jobs CREATE TABLE IF NOT EXISTS jobs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), company_id UUID NOT NULL REFERENCES company_profiles(id) ON DELETE CASCADE, title VARCHAR(200) NOT NULL, category VARCHAR(100), description TEXT NOT NULL, location VARCHAR(255) NOT NULL, job_type VARCHAR(50) NOT NULL DEFAULT 'FULL_TIME', -- FULL_TIME, PART_TIME, CONTRACT salary_min INTEGER, -- in paise salary_max INTEGER, -- in paise experience_years INTEGER, skills TEXT[] DEFAULT '{}', status VARCHAR(50) NOT NULL DEFAULT 'DRAFT', -- DRAFT, PENDING_APPROVAL, LIVE, EXPIRED, CLOSED, REJECTED rejection_reason TEXT, expires_at TIMESTAMPTZ, approved_at TIMESTAMPTZ, approved_by UUID REFERENCES users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Applications (Job Seeker → Job) CREATE TABLE IF NOT EXISTS applications ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), job_id UUID NOT NULL REFERENCES jobs(id) ON DELETE CASCADE, job_seeker_id UUID NOT NULL REFERENCES job_seeker_profiles(id) ON DELETE CASCADE, cover_letter TEXT, resume_url VARCHAR(500), status VARCHAR(50) NOT NULL DEFAULT 'APPLIED', -- APPLIED, SHORTLISTED, INTERVIEW, OFFERED, HIRED, REJECTED, WITHDRAWN applied_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), contact_viewed BOOLEAN NOT NULL DEFAULT false, UNIQUE(job_id, job_seeker_id) ); CREATE INDEX IF NOT EXISTS idx_jobs_company_id ON jobs(company_id); CREATE INDEX IF NOT EXISTS idx_jobs_status ON jobs(status); CREATE INDEX IF NOT EXISTS idx_applications_job_id ON applications(job_id); CREATE INDEX IF NOT EXISTS idx_applications_job_seeker_id ON applications(job_seeker_id); CREATE INDEX IF NOT EXISTS idx_applications_status ON applications(status); -- Add missing fields to job_seeker_profiles ALTER TABLE job_seeker_profiles ADD COLUMN IF NOT EXISTS full_name VARCHAR(255), ADD COLUMN IF NOT EXISTS location VARCHAR(255), ADD COLUMN IF NOT EXISTS summary TEXT, ADD COLUMN IF NOT EXISTS experience_years INTEGER DEFAULT 0, ADD COLUMN IF NOT EXISTS skills TEXT[] DEFAULT '{}', ADD COLUMN IF NOT EXISTS resume_url VARCHAR(500), ADD COLUMN IF NOT EXISTS active_application_count INTEGER NOT NULL DEFAULT 0, ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE'; -- Requirements (customer leads) CREATE TABLE IF NOT EXISTS requirements ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), customer_id UUID NOT NULL REFERENCES customer_profiles(id) ON DELETE CASCADE, profession_key VARCHAR(50) NOT NULL, title VARCHAR(200) NOT NULL, description TEXT NOT NULL, location VARCHAR(255) NOT NULL, budget INTEGER, -- in paise preferred_date DATE, extra_data_json JSONB, status VARCHAR(50) NOT NULL DEFAULT 'DRAFT', -- DRAFT, PENDING_APPROVAL, OPEN, CLOSED, EXPIRED, REJECTED rejection_reason TEXT, request_count INTEGER NOT NULL DEFAULT 0, accepted_count INTEGER NOT NULL DEFAULT 0, expires_at TIMESTAMPTZ, approved_at TIMESTAMPTZ, approved_by UUID REFERENCES users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- professionals unified table (parent for all 9 profession subtypes) CREATE TABLE IF NOT EXISTS professionals ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, profession_key VARCHAR(50) NOT NULL, display_name VARCHAR(255) NOT NULL, location VARCHAR(255), bio TEXT, extra_data_json JSONB, status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Lead requests (professional → requirement) CREATE TABLE IF NOT EXISTS lead_requests ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), requirement_id UUID NOT NULL REFERENCES requirements(id) ON DELETE CASCADE, professional_id UUID NOT NULL REFERENCES professionals(id) ON DELETE CASCADE, status VARCHAR(50) NOT NULL DEFAULT 'PENDING', -- PENDING, ACCEPTED, REJECTED, EXPIRED, CANCELLED tracecoins_reserved INTEGER NOT NULL DEFAULT 25, expires_at TIMESTAMPTZ NOT NULL, requested_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), resolved_at TIMESTAMPTZ, UNIQUE(requirement_id, professional_id) ); -- Add missing fields to customer_profiles ALTER TABLE customer_profiles ADD COLUMN IF NOT EXISTS full_name VARCHAR(255), ADD COLUMN IF NOT EXISTS phone VARCHAR(20), ADD COLUMN IF NOT EXISTS city VARCHAR(100), ADD COLUMN IF NOT EXISTS area VARCHAR(100), ADD COLUMN IF NOT EXISTS preferred_professions TEXT[] DEFAULT '{}', ADD COLUMN IF NOT EXISTS active_requirement_count INTEGER NOT NULL DEFAULT 0, ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE'; CREATE INDEX IF NOT EXISTS idx_requirements_customer_id ON requirements(customer_id); CREATE INDEX IF NOT EXISTS idx_requirements_status ON requirements(status); CREATE INDEX IF NOT EXISTS idx_requirements_profession_key ON requirements(profession_key); CREATE INDEX IF NOT EXISTS idx_lead_requests_requirement_id ON lead_requests(requirement_id); CREATE INDEX IF NOT EXISTS idx_lead_requests_professional_id ON lead_requests(professional_id); CREATE INDEX IF NOT EXISTS idx_lead_requests_status ON lead_requests(status); CREATE INDEX IF NOT EXISTS idx_professionals_profession_key ON professionals(profession_key); -- Portfolio items (for professionals) CREATE TABLE IF NOT EXISTS portfolio_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), professional_id UUID NOT NULL REFERENCES professionals(id) ON DELETE CASCADE, title VARCHAR(255) NOT NULL, description TEXT, tags TEXT[] DEFAULT '{}', created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Portfolio images (multiple images per portfolio item) CREATE TABLE IF NOT EXISTS portfolio_images ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), portfolio_item_id UUID NOT NULL REFERENCES portfolio_items(id) ON DELETE CASCADE, file_url VARCHAR(500) NOT NULL, display_order INTEGER NOT NULL DEFAULT 0 ); -- Services (offered by professionals) CREATE TABLE IF NOT EXISTS services ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), professional_id UUID NOT NULL REFERENCES professionals(id) ON DELETE CASCADE, name VARCHAR(255) NOT NULL, description TEXT, price INTEGER NOT NULL DEFAULT 0, -- in paise duration_minutes INTEGER, is_active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Tracecoin wallets (one per user) CREATE TABLE IF NOT EXISTS tracecoin_wallets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, balance INTEGER NOT NULL DEFAULT 0, reserved INTEGER NOT NULL DEFAULT 0, updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Tracecoin ledger (IMMUTABLE — never update or delete) CREATE TABLE IF NOT EXISTS tracecoin_ledger ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), wallet_id UUID NOT NULL REFERENCES tracecoin_wallets(id), type VARCHAR(20) NOT NULL, -- CREDIT, DEBIT, RESERVE, RELEASE amount INTEGER NOT NULL, reason VARCHAR(100) NOT NULL, -- LEAD_REQUEST, LEAD_ACCEPTED, PURCHASE, ADMIN_CREDIT, LEAD_EXPIRED reference_id UUID, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Pricing packages (Tracecoin bundles, job slots, contact views) CREATE TABLE IF NOT EXISTS pricing_packages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, role_key VARCHAR(50) NOT NULL, package_type VARCHAR(50) NOT NULL, -- JOB_POSTING, CONTACT_VIEWS, TRACECOIN_BUNDLE tracecoins_amount INTEGER NOT NULL DEFAULT 0, price_inr INTEGER NOT NULL, -- in paise description TEXT, is_active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Payments (Razorpay transactions) CREATE TABLE IF NOT EXISTS payments ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id), package_id UUID NOT NULL REFERENCES pricing_packages(id), razorpay_order_id VARCHAR(100), razorpay_payment_id VARCHAR(100), amount_inr INTEGER NOT NULL, tracecoins_credited INTEGER NOT NULL DEFAULT 0, status VARCHAR(20) NOT NULL DEFAULT 'PENDING', -- PENDING, SUCCESS, FAILED verified_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Invoices (generated for every successful payment) CREATE TABLE IF NOT EXISTS invoices ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), payment_id UUID NOT NULL REFERENCES payments(id), user_id UUID NOT NULL REFERENCES users(id), invoice_number VARCHAR(50) NOT NULL UNIQUE, subtotal INTEGER NOT NULL, -- in paise gst_amount INTEGER NOT NULL, -- in paise total INTEGER NOT NULL, -- in paise status VARCHAR(20) NOT NULL DEFAULT 'ISSUED', -- ISSUED, PAID issued_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), file_url VARCHAR(500) ); CREATE INDEX IF NOT EXISTS idx_portfolio_items_professional_id ON portfolio_items(professional_id); CREATE INDEX IF NOT EXISTS idx_services_professional_id ON services(professional_id); CREATE INDEX IF NOT EXISTS idx_tracecoin_ledger_wallet_id ON tracecoin_ledger(wallet_id); CREATE INDEX IF NOT EXISTS idx_payments_user_id ON payments(user_id); CREATE INDEX IF NOT EXISTS idx_invoices_user_id ON invoices(user_id); -- Notifications (in-app) CREATE TABLE IF NOT EXISTS notifications ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, title VARCHAR(255) NOT NULL, body TEXT, type VARCHAR(50), -- APPROVAL, LEAD, JOB, PAYMENT reference_id UUID, is_read BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Email logs (audit trail) CREATE TABLE IF NOT EXISTS email_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES users(id), trigger VARCHAR(100) NOT NULL, -- PROFILE_APPROVED, JOB_APPROVED, etc. to_email VARCHAR(255) NOT NULL, subject VARCHAR(500), status VARCHAR(20) NOT NULL DEFAULT 'PENDING', -- PENDING, SENT, FAILED sent_at TIMESTAMPTZ ); CREATE INDEX IF NOT EXISTS idx_notifications_user_id ON notifications(user_id); CREATE INDEX IF NOT EXISTS idx_notifications_is_read ON notifications(is_read); CREATE INDEX IF NOT EXISTS idx_email_logs_user_id ON email_logs(user_id); -- Drop the generic professionals table approach; use per-profession profile tables -- Portfolio and services stay shared (referenced by user_id + profession_key) -- 1. PHOTOGRAPHER PROFILES CREATE TABLE IF NOT EXISTS photographer_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, display_name VARCHAR(255) NOT NULL, bio TEXT, location VARCHAR(255), -- Profession-specific specialties TEXT[] DEFAULT '{}', -- e.g. ['Wedding', 'Portrait', 'Commercial'] camera_brands TEXT[] DEFAULT '{}', -- e.g. ['Sony', 'Canon'] studio_available BOOLEAN NOT NULL DEFAULT false, outdoor_shoots BOOLEAN NOT NULL DEFAULT true, travel_radius_km INTEGER DEFAULT 50, starting_price_inr INTEGER DEFAULT 0, -- in paise -- Verification & status status VARCHAR(50) NOT NULL DEFAULT 'PENDING', -- PENDING, APPROVED, REJECTED, SUSPENDED rejection_reason TEXT, approved_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 2. TUTOR PROFILES CREATE TABLE IF NOT EXISTS tutor_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, display_name VARCHAR(255) NOT NULL, bio TEXT, location VARCHAR(255), -- Profession-specific subjects TEXT[] DEFAULT '{}', -- e.g. ['Math', 'Physics', 'Hindi'] board_types TEXT[] DEFAULT '{}', -- e.g. ['CBSE', 'ICSE', 'IB'] qualification VARCHAR(255), -- e.g. 'B.Tech IIT Delhi' teaches_online BOOLEAN NOT NULL DEFAULT true, teaches_offline BOOLEAN NOT NULL DEFAULT true, experience_years INTEGER DEFAULT 0, hourly_rate_inr INTEGER DEFAULT 0, -- in paise status VARCHAR(50) NOT NULL DEFAULT 'PENDING', rejection_reason TEXT, approved_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 3. MAKEUP ARTIST PROFILES CREATE TABLE IF NOT EXISTS makeup_artist_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, display_name VARCHAR(255) NOT NULL, bio TEXT, location VARCHAR(255), -- Profession-specific specializations TEXT[] DEFAULT '{}', -- e.g. ['Bridal', 'Editorial', 'SFX'] kit_brands TEXT[] DEFAULT '{}', -- e.g. ['MAC', 'NARS', 'NYX'] home_service BOOLEAN NOT NULL DEFAULT true, studio_available BOOLEAN NOT NULL DEFAULT false, starting_price_inr INTEGER DEFAULT 0, status VARCHAR(50) NOT NULL DEFAULT 'PENDING', rejection_reason TEXT, approved_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 4. DEVELOPER PROFILES CREATE TABLE IF NOT EXISTS developer_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, display_name VARCHAR(255) NOT NULL, bio TEXT, location VARCHAR(255), -- Profession-specific tech_stack TEXT[] DEFAULT '{}', -- e.g. ['Rust', 'React', 'PostgreSQL'] github_url VARCHAR(500), portfolio_url VARCHAR(500), experience_years INTEGER DEFAULT 0, availability VARCHAR(50) DEFAULT 'FULL_TIME', -- FULL_TIME, PART_TIME, FREELANCE hourly_rate_inr INTEGER DEFAULT 0, remote_ok BOOLEAN NOT NULL DEFAULT true, status VARCHAR(50) NOT NULL DEFAULT 'PENDING', rejection_reason TEXT, approved_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 5. VIDEO EDITOR PROFILES CREATE TABLE IF NOT EXISTS video_editor_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, display_name VARCHAR(255) NOT NULL, bio TEXT, location VARCHAR(255), -- Profession-specific software_skills TEXT[] DEFAULT '{}', -- e.g. ['Premiere Pro', 'DaVinci Resolve'] style_tags TEXT[] DEFAULT '{}', -- e.g. ['Cinematic', 'Corporate', 'Reels'] turnaround_days INTEGER DEFAULT 7, reel_url VARCHAR(500), starting_price_inr INTEGER DEFAULT 0, status VARCHAR(50) NOT NULL DEFAULT 'PENDING', rejection_reason TEXT, approved_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 6. GRAPHIC DESIGNER PROFILES CREATE TABLE IF NOT EXISTS graphic_designer_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, display_name VARCHAR(255) NOT NULL, bio TEXT, location VARCHAR(255), -- Profession-specific design_tools TEXT[] DEFAULT '{}', -- e.g. ['Figma', 'Illustrator', 'Photoshop'] style_tags TEXT[] DEFAULT '{}', -- e.g. ['Minimalist', 'Bold', 'Corporate'] brand_experience BOOLEAN NOT NULL DEFAULT false, portfolio_url VARCHAR(500), starting_price_inr INTEGER DEFAULT 0, status VARCHAR(50) NOT NULL DEFAULT 'PENDING', rejection_reason TEXT, approved_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 7. SOCIAL MEDIA MANAGER PROFILES CREATE TABLE IF NOT EXISTS social_media_manager_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, display_name VARCHAR(255) NOT NULL, bio TEXT, location VARCHAR(255), -- Profession-specific platforms TEXT[] DEFAULT '{}', -- e.g. ['Instagram', 'LinkedIn', 'YouTube'] industries TEXT[] DEFAULT '{}', -- e.g. ['F&B', 'Fashion', 'Real Estate'] content_types TEXT[] DEFAULT '{}', -- e.g. ['Reels', 'Carousels', 'Stories'] avg_follower_growth_pct INTEGER DEFAULT 0, starting_price_inr INTEGER DEFAULT 0, status VARCHAR(50) NOT NULL DEFAULT 'PENDING', rejection_reason TEXT, approved_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 8. FITNESS TRAINER PROFILES CREATE TABLE IF NOT EXISTS fitness_trainer_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, display_name VARCHAR(255) NOT NULL, bio TEXT, location VARCHAR(255), -- Profession-specific disciplines TEXT[] DEFAULT '{}', -- e.g. ['Yoga', 'HIIT', 'Zumba', 'CrossFit'] certifications TEXT[] DEFAULT '{}', -- e.g. ['ACE', 'NASM', 'Yoga Alliance RYT'] online_sessions BOOLEAN NOT NULL DEFAULT true, home_visits BOOLEAN NOT NULL DEFAULT false, gym_based BOOLEAN NOT NULL DEFAULT false, per_session_rate_inr INTEGER DEFAULT 0, status VARCHAR(50) NOT NULL DEFAULT 'PENDING', rejection_reason TEXT, approved_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 9. CATERING SERVICES PROFILES CREATE TABLE IF NOT EXISTS catering_service_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, business_name VARCHAR(255) NOT NULL, bio TEXT, location VARCHAR(255), -- Profession-specific cuisine_types TEXT[] DEFAULT '{}', -- e.g. ['North Indian', 'Continental', 'Vegan'] event_types TEXT[] DEFAULT '{}', -- e.g. ['Wedding', 'Corporate', 'Birthday'] min_guests INTEGER DEFAULT 10, max_guests INTEGER DEFAULT 500, has_setup_team BOOLEAN NOT NULL DEFAULT true, has_serving_staff BOOLEAN NOT NULL DEFAULT true, price_per_head_inr INTEGER DEFAULT 0, -- in paise status VARCHAR(50) NOT NULL DEFAULT 'PENDING', rejection_reason TEXT, approved_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Shared: portfolio_items now uses user_id + profession_key (no foreign key to professionals) -- Drop the professionals-table FK if it was added before ALTER TABLE portfolio_items ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES users(id) ON DELETE CASCADE, ADD COLUMN IF NOT EXISTS profession_key VARCHAR(50); ALTER TABLE services ADD COLUMN IF NOT EXISTS user_id UUID REFERENCES users(id) ON DELETE CASCADE, ADD COLUMN IF NOT EXISTS profession_key VARCHAR(50); -- Lead requests: use user_id instead of professional_id foreign key ALTER TABLE lead_requests ADD COLUMN IF NOT EXISTS professional_user_id UUID REFERENCES users(id) ON DELETE CASCADE; -- Backfill columns when legacy minimal profile tables already exist. -- This keeps migrations idempotent while upgrading old schemas to the new profile shape. ALTER TABLE photographer_profiles ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '', ADD COLUMN IF NOT EXISTS bio TEXT, ADD COLUMN IF NOT EXISTS location VARCHAR(255), ADD COLUMN IF NOT EXISTS specialties TEXT[] DEFAULT '{}', ADD COLUMN IF NOT EXISTS camera_brands TEXT[] DEFAULT '{}', ADD COLUMN IF NOT EXISTS studio_available BOOLEAN NOT NULL DEFAULT false, ADD COLUMN IF NOT EXISTS outdoor_shoots BOOLEAN NOT NULL DEFAULT true, ADD COLUMN IF NOT EXISTS travel_radius_km INTEGER DEFAULT 50, ADD COLUMN IF NOT EXISTS starting_price_inr INTEGER DEFAULT 0, ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', ADD COLUMN IF NOT EXISTS rejection_reason TEXT, ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; ALTER TABLE tutor_profiles ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '', ADD COLUMN IF NOT EXISTS bio TEXT, ADD COLUMN IF NOT EXISTS location VARCHAR(255), ADD COLUMN IF NOT EXISTS subjects TEXT[] DEFAULT '{}', ADD COLUMN IF NOT EXISTS board_types TEXT[] DEFAULT '{}', ADD COLUMN IF NOT EXISTS qualification VARCHAR(255), ADD COLUMN IF NOT EXISTS teaches_online BOOLEAN NOT NULL DEFAULT true, ADD COLUMN IF NOT EXISTS teaches_offline BOOLEAN NOT NULL DEFAULT true, ADD COLUMN IF NOT EXISTS hourly_rate_inr INTEGER DEFAULT 0, ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', ADD COLUMN IF NOT EXISTS rejection_reason TEXT, ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; ALTER TABLE makeup_artist_profiles ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '', ADD COLUMN IF NOT EXISTS location VARCHAR(255), ADD COLUMN IF NOT EXISTS specializations TEXT[] DEFAULT '{}', ADD COLUMN IF NOT EXISTS kit_brands TEXT[] DEFAULT '{}', ADD COLUMN IF NOT EXISTS home_service BOOLEAN NOT NULL DEFAULT true, ADD COLUMN IF NOT EXISTS studio_available BOOLEAN NOT NULL DEFAULT false, ADD COLUMN IF NOT EXISTS starting_price_inr INTEGER DEFAULT 0, ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', ADD COLUMN IF NOT EXISTS rejection_reason TEXT, ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; ALTER TABLE developer_profiles ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '', ADD COLUMN IF NOT EXISTS location VARCHAR(255), ADD COLUMN IF NOT EXISTS tech_stack TEXT[] DEFAULT '{}', ADD COLUMN IF NOT EXISTS github_url VARCHAR(500), ADD COLUMN IF NOT EXISTS portfolio_url VARCHAR(500), ADD COLUMN IF NOT EXISTS availability VARCHAR(50) DEFAULT 'FULL_TIME', ADD COLUMN IF NOT EXISTS hourly_rate_inr INTEGER DEFAULT 0, ADD COLUMN IF NOT EXISTS remote_ok BOOLEAN NOT NULL DEFAULT true, ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', ADD COLUMN IF NOT EXISTS rejection_reason TEXT, ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; ALTER TABLE video_editor_profiles ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '', ADD COLUMN IF NOT EXISTS location VARCHAR(255), ADD COLUMN IF NOT EXISTS software_skills TEXT[] DEFAULT '{}', ADD COLUMN IF NOT EXISTS style_tags TEXT[] DEFAULT '{}', ADD COLUMN IF NOT EXISTS turnaround_days INTEGER DEFAULT 7, ADD COLUMN IF NOT EXISTS reel_url VARCHAR(500), ADD COLUMN IF NOT EXISTS starting_price_inr INTEGER DEFAULT 0, ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', ADD COLUMN IF NOT EXISTS rejection_reason TEXT, ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; ALTER TABLE graphic_designer_profiles ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '', ADD COLUMN IF NOT EXISTS location VARCHAR(255), ADD COLUMN IF NOT EXISTS design_tools TEXT[] DEFAULT '{}', ADD COLUMN IF NOT EXISTS style_tags TEXT[] DEFAULT '{}', ADD COLUMN IF NOT EXISTS brand_experience BOOLEAN NOT NULL DEFAULT false, ADD COLUMN IF NOT EXISTS portfolio_url VARCHAR(500), ADD COLUMN IF NOT EXISTS starting_price_inr INTEGER DEFAULT 0, ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', ADD COLUMN IF NOT EXISTS rejection_reason TEXT, ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; ALTER TABLE social_media_manager_profiles ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '', ADD COLUMN IF NOT EXISTS location VARCHAR(255), ADD COLUMN IF NOT EXISTS platforms TEXT[] DEFAULT '{}', ADD COLUMN IF NOT EXISTS industries TEXT[] DEFAULT '{}', ADD COLUMN IF NOT EXISTS content_types TEXT[] DEFAULT '{}', ADD COLUMN IF NOT EXISTS avg_follower_growth_pct INTEGER DEFAULT 0, ADD COLUMN IF NOT EXISTS starting_price_inr INTEGER DEFAULT 0, ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', ADD COLUMN IF NOT EXISTS rejection_reason TEXT, ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; ALTER TABLE fitness_trainer_profiles ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '', ADD COLUMN IF NOT EXISTS location VARCHAR(255), ADD COLUMN IF NOT EXISTS disciplines TEXT[] DEFAULT '{}', ADD COLUMN IF NOT EXISTS certifications TEXT[] DEFAULT '{}', ADD COLUMN IF NOT EXISTS online_sessions BOOLEAN NOT NULL DEFAULT true, ADD COLUMN IF NOT EXISTS home_visits BOOLEAN NOT NULL DEFAULT false, ADD COLUMN IF NOT EXISTS gym_based BOOLEAN NOT NULL DEFAULT false, ADD COLUMN IF NOT EXISTS per_session_rate_inr INTEGER DEFAULT 0, ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', ADD COLUMN IF NOT EXISTS rejection_reason TEXT, ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; ALTER TABLE catering_service_profiles ADD COLUMN IF NOT EXISTS business_name VARCHAR(255) NOT NULL DEFAULT '', ADD COLUMN IF NOT EXISTS location VARCHAR(255), ADD COLUMN IF NOT EXISTS cuisine_types TEXT[] DEFAULT '{}', ADD COLUMN IF NOT EXISTS event_types TEXT[] DEFAULT '{}', ADD COLUMN IF NOT EXISTS min_guests INTEGER DEFAULT 10, ADD COLUMN IF NOT EXISTS max_guests INTEGER DEFAULT 500, ADD COLUMN IF NOT EXISTS has_setup_team BOOLEAN NOT NULL DEFAULT true, ADD COLUMN IF NOT EXISTS has_serving_staff BOOLEAN NOT NULL DEFAULT true, ADD COLUMN IF NOT EXISTS price_per_head_inr INTEGER DEFAULT 0, ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', ADD COLUMN IF NOT EXISTS rejection_reason TEXT, ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; ALTER TABLE lead_requests ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); -- Indexes CREATE INDEX IF NOT EXISTS idx_photographer_profiles_status ON photographer_profiles(status); CREATE INDEX IF NOT EXISTS idx_tutor_profiles_status ON tutor_profiles(status); CREATE INDEX IF NOT EXISTS idx_makeup_artist_profiles_status ON makeup_artist_profiles(status); CREATE INDEX IF NOT EXISTS idx_developer_profiles_status ON developer_profiles(status); CREATE INDEX IF NOT EXISTS idx_video_editor_profiles_status ON video_editor_profiles(status); CREATE INDEX IF NOT EXISTS idx_graphic_designer_profiles_status ON graphic_designer_profiles(status); CREATE INDEX IF NOT EXISTS idx_social_media_manager_profiles_status ON social_media_manager_profiles(status); CREATE INDEX IF NOT EXISTS idx_fitness_trainer_profiles_status ON fitness_trainer_profiles(status); CREATE INDEX IF NOT EXISTS idx_catering_service_profiles_status ON catering_service_profiles(status); CREATE INDEX IF NOT EXISTS idx_portfolio_items_user_id ON portfolio_items(user_id); CREATE INDEX IF NOT EXISTS idx_services_user_id ON services(user_id); -- Add email verification and password reset columns to users table ALTER TABLE users ADD COLUMN IF NOT EXISTS email_verification_token VARCHAR(255), ADD COLUMN IF NOT EXISTS email_verification_expires_at TIMESTAMPTZ, ADD COLUMN IF NOT EXISTS reset_password_token VARCHAR(255), ADD COLUMN IF NOT EXISTS reset_password_expires_at TIMESTAMPTZ; -- Add index for token lookups CREATE INDEX IF NOT EXISTS idx_users_email_verification_token ON users(email_verification_token); CREATE INDEX IF NOT EXISTS idx_users_reset_password_token ON users(reset_password_token); -- Reviews: customers leave reviews on professionals after an accepted lead CREATE TABLE IF NOT EXISTS reviews ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), lead_request_id UUID NOT NULL REFERENCES lead_requests(id) ON DELETE CASCADE UNIQUE, customer_id UUID NOT NULL REFERENCES customer_profiles(id) ON DELETE CASCADE, professional_id UUID NOT NULL REFERENCES professionals(id) ON DELETE CASCADE, rating SMALLINT NOT NULL CHECK (rating >= 1 AND rating <= 5), comment TEXT, is_published BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_reviews_professional_id ON reviews(professional_id); CREATE INDEX IF NOT EXISTS idx_reviews_customer_id ON reviews(customer_id); -- Knowledge Base categories CREATE TABLE IF NOT EXISTS kb_categories ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name VARCHAR(255) NOT NULL, slug VARCHAR(255) NOT NULL UNIQUE, description TEXT, display_order INTEGER NOT NULL DEFAULT 0, is_active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Knowledge Base articles CREATE TABLE IF NOT EXISTS kb_articles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), category_id UUID NOT NULL REFERENCES kb_categories(id) ON DELETE CASCADE, title VARCHAR(500) NOT NULL, slug VARCHAR(500) NOT NULL UNIQUE, body TEXT NOT NULL, target_roles TEXT[] DEFAULT '{}', -- empty = visible to all is_published BOOLEAN NOT NULL DEFAULT false, views INTEGER NOT NULL DEFAULT 0, created_by UUID REFERENCES users(id), created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_kb_articles_category_id ON kb_articles(category_id); CREATE INDEX IF NOT EXISTS idx_kb_articles_slug ON kb_articles(slug); -- Support tickets CREATE TABLE IF NOT EXISTS support_tickets ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, subject VARCHAR(500) NOT NULL, category VARCHAR(50) NOT NULL DEFAULT 'GENERAL', -- GENERAL, BILLING, ACCOUNT, LEAD, JOB status VARCHAR(20) NOT NULL DEFAULT 'OPEN', -- OPEN, IN_PROGRESS, RESOLVED, CLOSED priority VARCHAR(10) NOT NULL DEFAULT 'NORMAL', -- LOW, NORMAL, HIGH, URGENT assigned_to UUID REFERENCES users(id), resolved_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Support ticket messages CREATE TABLE IF NOT EXISTS support_ticket_messages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), ticket_id UUID NOT NULL REFERENCES support_tickets(id) ON DELETE CASCADE, sender_id UUID NOT NULL REFERENCES users(id), body TEXT NOT NULL, is_internal BOOLEAN NOT NULL DEFAULT false, -- true = staff-only note created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_support_tickets_user_id ON support_tickets(user_id); CREATE INDEX IF NOT EXISTS idx_support_tickets_status ON support_tickets(status); CREATE INDEX IF NOT EXISTS idx_support_ticket_messages_ticket_id ON support_ticket_messages(ticket_id); -- Discount coupons for Tracecoin and package purchases CREATE TABLE IF NOT EXISTS coupons ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), code VARCHAR(50) NOT NULL UNIQUE, description TEXT, discount_type VARCHAR(20) NOT NULL, -- PERCENT, FLAT discount_value INTEGER NOT NULL, -- percent (0-100) or paise applies_to VARCHAR(50) NOT NULL DEFAULT 'ALL', -- ALL, TRACECOIN_BUNDLE, JOB_POSTING, CONTACT_VIEWS min_order_amount INTEGER NOT NULL DEFAULT 0, -- paise max_uses INTEGER, -- NULL = unlimited uses_count INTEGER NOT NULL DEFAULT 0, per_user_limit INTEGER NOT NULL DEFAULT 1, valid_from TIMESTAMPTZ NOT NULL DEFAULT NOW(), valid_until TIMESTAMPTZ, is_active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Track which users used which coupons CREATE TABLE IF NOT EXISTS coupon_uses ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), coupon_id UUID NOT NULL REFERENCES coupons(id) ON DELETE CASCADE, user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, payment_id UUID REFERENCES payments(id), used_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE (coupon_id, user_id) ); CREATE INDEX IF NOT EXISTS idx_coupons_code ON coupons(code); CREATE INDEX IF NOT EXISTS idx_coupon_uses_user_id ON coupon_uses(user_id); -- Onboarding state per user per role -- Tracks progress through the schema-driven onboarding form CREATE TABLE IF NOT EXISTS onboarding_states ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, role_id UUID NOT NULL REFERENCES roles(id) ON DELETE CASCADE, status VARCHAR(20) NOT NULL DEFAULT 'NOT_STARTED', -- NOT_STARTED | IN_PROGRESS | COMPLETED progress_json JSONB NOT NULL DEFAULT '{}', completed_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- One onboarding state record per user per role CREATE UNIQUE INDEX IF NOT EXISTS idx_onboarding_state_user_role ON onboarding_states(user_id, role_id); -- Make display_name / business_name nullable so upserts can work -- without forcing the name on every call. -- Add custom_data JSONB to every profession table so all onboarding -- form fields are preserved even if they don't have a dedicated column. ALTER TABLE photographer_profiles ALTER COLUMN display_name DROP NOT NULL; ALTER TABLE tutor_profiles ALTER COLUMN display_name DROP NOT NULL; ALTER TABLE makeup_artist_profiles ALTER COLUMN display_name DROP NOT NULL; ALTER TABLE developer_profiles ALTER COLUMN display_name DROP NOT NULL; ALTER TABLE video_editor_profiles ALTER COLUMN display_name DROP NOT NULL; ALTER TABLE graphic_designer_profiles ALTER COLUMN display_name DROP NOT NULL; ALTER TABLE social_media_manager_profiles ALTER COLUMN display_name DROP NOT NULL; ALTER TABLE fitness_trainer_profiles ALTER COLUMN display_name DROP NOT NULL; ALTER TABLE catering_service_profiles ALTER COLUMN business_name DROP NOT NULL; ALTER TABLE photographer_profiles ADD COLUMN IF NOT EXISTS custom_data JSONB; ALTER TABLE tutor_profiles ADD COLUMN IF NOT EXISTS custom_data JSONB; ALTER TABLE makeup_artist_profiles ADD COLUMN IF NOT EXISTS custom_data JSONB; ALTER TABLE developer_profiles ADD COLUMN IF NOT EXISTS custom_data JSONB; ALTER TABLE video_editor_profiles ADD COLUMN IF NOT EXISTS custom_data JSONB; ALTER TABLE graphic_designer_profiles ADD COLUMN IF NOT EXISTS custom_data JSONB; ALTER TABLE social_media_manager_profiles ADD COLUMN IF NOT EXISTS custom_data JSONB; ALTER TABLE fitness_trainer_profiles ADD COLUMN IF NOT EXISTS custom_data JSONB; ALTER TABLE catering_service_profiles ADD COLUMN IF NOT EXISTS custom_data JSONB; -- Enforce immutable tracecoin ledger: no UPDATE/DELETE allowed. CREATE OR REPLACE FUNCTION prevent_tracecoin_ledger_mutation() RETURNS trigger AS $$ BEGIN RAISE EXCEPTION 'tracecoin_ledger is immutable; % is not allowed', TG_OP; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS trg_prevent_tracecoin_ledger_update ON tracecoin_ledger; CREATE TRIGGER trg_prevent_tracecoin_ledger_update BEFORE UPDATE ON tracecoin_ledger FOR EACH ROW EXECUTE FUNCTION prevent_tracecoin_ledger_mutation(); DROP TRIGGER IF EXISTS trg_prevent_tracecoin_ledger_delete ON tracecoin_ledger; CREATE TRIGGER trg_prevent_tracecoin_ledger_delete BEFORE DELETE ON tracecoin_ledger FOR EACH ROW EXECUTE FUNCTION prevent_tracecoin_ledger_mutation(); UPDATE company_profiles SET status = 'APPROVED' WHERE status = 'ACTIVE'; UPDATE customer_profiles SET status = 'APPROVED' WHERE status = 'ACTIVE'; -- Extend roles table for internal role management ALTER TABLE roles ADD COLUMN IF NOT EXISTS description TEXT, ADD COLUMN IF NOT EXISTS department_id UUID REFERENCES departments(id) ON DELETE SET NULL, ADD COLUMN IF NOT EXISTS can_approve_requests BOOLEAN NOT NULL DEFAULT false, ADD COLUMN IF NOT EXISTS can_manage_system_settings BOOLEAN NOT NULL DEFAULT false; ALTER TABLE departments ADD COLUMN IF NOT EXISTS code VARCHAR(64), ADD COLUMN IF NOT EXISTS description TEXT, ADD COLUMN IF NOT EXISTS department_head VARCHAR(255), ADD COLUMN IF NOT EXISTS department_email VARCHAR(255), ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT true, ADD COLUMN IF NOT EXISTS visibility VARCHAR(20) NOT NULL DEFAULT 'INTERNAL', ADD COLUMN IF NOT EXISTS transfers_enabled BOOLEAN NOT NULL DEFAULT false, ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); UPDATE departments SET updated_at = COALESCE(updated_at, created_at, NOW()); CREATE UNIQUE INDEX IF NOT EXISTS idx_departments_code_unique ON departments (LOWER(code)) WHERE code IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_departments_is_active ON departments (is_active); ALTER TABLE designations ADD COLUMN IF NOT EXISTS code VARCHAR(64), ADD COLUMN IF NOT EXISTS department_id UUID REFERENCES departments(id) ON DELETE SET NULL, ADD COLUMN IF NOT EXISTS description TEXT, ADD COLUMN IF NOT EXISTS level VARCHAR(100), ADD COLUMN IF NOT EXISTS can_manage_team BOOLEAN NOT NULL DEFAULT false, ADD COLUMN IF NOT EXISTS can_approve BOOLEAN NOT NULL DEFAULT false, ADD COLUMN IF NOT EXISTS is_active BOOLEAN NOT NULL DEFAULT true, ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); UPDATE designations SET updated_at = COALESCE(updated_at, created_at, NOW()); CREATE UNIQUE INDEX IF NOT EXISTS idx_designations_code_unique ON designations (LOWER(code)) WHERE code IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_designations_is_active ON designations (is_active); CREATE INDEX IF NOT EXISTS idx_designations_department_id ON designations (department_id); -- UP: 20260402030000_strict_employee_separation.up.sql -- Drop old employees table (was linked to users — replacing with standalone auth) DROP TABLE IF EXISTS employees CASCADE; -- 1. EMPLOYEES (Standalone Table - Not Linked to 'users') CREATE TABLE employees ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), first_name VARCHAR(100) NOT NULL, last_name VARCHAR(100) NOT NULL, email VARCHAR(255) UNIQUE NOT NULL, password_hash VARCHAR(255) NOT NULL, employee_code VARCHAR(50) UNIQUE, department_id UUID REFERENCES departments(id) ON DELETE SET NULL, designation_id UUID REFERENCES designations(id) ON DELETE SET NULL, role_code VARCHAR(50) NOT NULL DEFAULT 'STAFF', status VARCHAR(50) NOT NULL DEFAULT 'ACTIVE', joined_at DATE NOT NULL DEFAULT CURRENT_DATE, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 2. EMPLOYEE SESSIONS (Standalone Auth) CREATE TABLE IF NOT EXISTS employee_sessions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), employee_id UUID NOT NULL REFERENCES employees(id) ON DELETE CASCADE, token_hash VARCHAR(255) UNIQUE NOT NULL, expires_at TIMESTAMPTZ NOT NULL, revoked BOOLEAN NOT NULL DEFAULT false, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Indexes CREATE INDEX IF NOT EXISTS idx_employees_email ON employees(email); CREATE INDEX IF NOT EXISTS idx_employees_status ON employees(status); CREATE INDEX IF NOT EXISTS idx_employee_sessions_token ON employee_sessions(token_hash); -- Up migration: Create activity_logs table CREATE TABLE IF NOT EXISTS activity_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), actor_id UUID NOT NULL, -- User or Employee who performed the action actor_type VARCHAR(20) NOT NULL, -- 'USER' or 'EMPLOYEE' entity_id UUID NOT NULL, -- Target of the action (User ID, Job ID, etc.) entity_type VARCHAR(50) NOT NULL, -- 'USER', 'JOB', 'REQUIREMENT', 'EMPLOYEE', etc. action VARCHAR(100) NOT NULL, -- 'APPROVE', 'REJECT', 'STATUS_CHANGE', 'DELETE', etc. metadata JSONB, -- Optional extra context: { "old_status": "PENDING", "new_status": "APPROVED", "reason": "..." } ip_address VARCHAR(45), user_agent TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_activity_logs_entity ON activity_logs (entity_type, entity_id); CREATE INDEX IF NOT EXISTS idx_activity_logs_actor ON activity_logs (actor_type, actor_id); CREATE INDEX IF NOT EXISTS idx_activity_logs_created_at ON activity_logs (created_at DESC); ALTER TABLE kb_articles ADD COLUMN IF NOT EXISTS summary TEXT, ADD COLUMN IF NOT EXISTS tags TEXT[] NOT NULL DEFAULT '{}'; -- Allow admin-created tickets with no linked user ALTER TABLE support_tickets ALTER COLUMN user_id DROP NOT NULL; -- Add description body and requester info for admin-created cases ALTER TABLE support_tickets ADD COLUMN IF NOT EXISTS description TEXT, ADD COLUMN IF NOT EXISTS requester_name VARCHAR(255), ADD COLUMN IF NOT EXISTS requester_email VARCHAR(255); -- Extend reviews table to support admin-created reviews and admin moderation ALTER TABLE reviews ALTER COLUMN lead_request_id DROP NOT NULL, ALTER COLUMN customer_id DROP NOT NULL, ALTER COLUMN professional_id DROP NOT NULL, ADD COLUMN IF NOT EXISTS title VARCHAR(255), ADD COLUMN IF NOT EXISTS subject_type VARCHAR(50) NOT NULL DEFAULT 'PLATFORM', ADD COLUMN IF NOT EXISTS subject_id VARCHAR(255), ADD COLUMN IF NOT EXISTS reviewer_name VARCHAR(255), ADD COLUMN IF NOT EXISTS status VARCHAR(20) NOT NULL DEFAULT 'PUBLISHED'; -- Sync status with is_published for existing rows UPDATE reviews SET status = CASE WHEN is_published THEN 'PUBLISHED' ELSE 'HIDDEN' END; -- Add title and role_keys to coupons for admin UI ALTER TABLE coupons ADD COLUMN IF NOT EXISTS title VARCHAR(255), ADD COLUMN IF NOT EXISTS role_keys TEXT[] NOT NULL DEFAULT '{}'; -- Backfill title from description UPDATE coupons SET title = description WHERE title IS NULL AND description IS NOT NULL; -- Admin-managed automatic discounts (applied before coupon codes) CREATE TABLE IF NOT EXISTS discounts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title VARCHAR(255) NOT NULL, scope VARCHAR(20) NOT NULL DEFAULT 'ROLE', -- ROLE, PACKAGE role_key VARCHAR(50), package_id UUID REFERENCES pricing_packages(id) ON DELETE SET NULL, discount_type VARCHAR(20) NOT NULL, -- PERCENT, FIXED discount_value INTEGER NOT NULL, is_active BOOLEAN NOT NULL DEFAULT true, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 10. UGC CONTENT CREATOR PROFILES CREATE TABLE IF NOT EXISTS ugc_content_creator_profiles ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE UNIQUE, display_name VARCHAR(255) NOT NULL DEFAULT '', bio TEXT, location VARCHAR(255), -- Profession-specific platforms TEXT[] DEFAULT '{}', -- e.g. ['Instagram', 'YouTube', 'TikTok'] content_niches TEXT[] DEFAULT '{}', -- e.g. ['Beauty', 'Tech', 'Food', 'Lifestyle'] content_formats TEXT[] DEFAULT '{}', -- e.g. ['Reels', 'Unboxing', 'Reviews', 'GRWM'] follower_count INTEGER DEFAULT 0, avg_views_per_post INTEGER DEFAULT 0, has_media_kit BOOLEAN NOT NULL DEFAULT false, instagram_handle VARCHAR(100), youtube_channel_url VARCHAR(500), portfolio_url VARCHAR(500), starting_price_inr INTEGER DEFAULT 0, -- in paise custom_data JSONB, -- Verification & status status VARCHAR(50) NOT NULL DEFAULT 'PENDING', -- PENDING, APPROVED, REJECTED, SUSPENDED rejection_reason TEXT, approved_at TIMESTAMPTZ, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_ugc_content_creator_profiles_status ON ugc_content_creator_profiles(status); CREATE INDEX IF NOT EXISTS idx_ugc_content_creator_profiles_user_id ON ugc_content_creator_profiles(user_id); -- 1. VERIFICATIONS TABLE CREATE TABLE IF NOT EXISTS verifications ( 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, status VARCHAR(50) NOT NULL DEFAULT 'PENDING', -- PENDING, UNDER_REVIEW, DOCUMENTS_REQUESTED, REVISION_REQUESTED, APPROVED, REJECTED priority VARCHAR(10) NOT NULL DEFAULT 'LOW', -- HIGH, MEDIUM, LOW case_type VARCHAR(50) NOT NULL, -- PROFILE, PORTFOLIO, JOB, REQUIREMENT payload JSONB NOT NULL DEFAULT '{}', -- full submission data documents JSONB NOT NULL DEFAULT '[]', -- list of documents [{id, title, url, status}] notes TEXT, rejection_reason TEXT, assigned_to UUID REFERENCES users(id) ON DELETE SET NULL, -- Admin/Employee ID created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 2. VERIFICATION LOGS (History of actions) CREATE TABLE IF NOT EXISTS verification_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), verification_id UUID NOT NULL REFERENCES verifications(id) ON DELETE CASCADE, action VARCHAR(50) NOT NULL, -- STATUS_CHANGE, NOTE_ADDED, DOCS_REQUESTED, REASSIGNED actor_id UUID REFERENCES users(id) ON DELETE SET NULL, old_status VARCHAR(50), new_status VARCHAR(50), message TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- 3. INDEXES CREATE INDEX IF NOT EXISTS idx_verifications_user_id ON verifications(user_id); CREATE INDEX IF NOT EXISTS idx_verifications_status ON verifications(status); CREATE INDEX IF NOT EXISTS idx_verifications_case_type ON verifications(case_type); CREATE INDEX IF NOT EXISTS idx_verification_logs_ver_id ON verification_logs(verification_id);