diff --git a/crates/db/migrations/20260415000001_create_user_sessions.down.sql b/crates/db/migrations/20260415000001_create_user_sessions.down.sql new file mode 100644 index 0000000..141c136 --- /dev/null +++ b/crates/db/migrations/20260415000001_create_user_sessions.down.sql @@ -0,0 +1,2 @@ +-- Rollback: Drop user_sessions table +DROP TABLE IF EXISTS user_sessions; diff --git a/crates/db/migrations/20260415000001_create_user_sessions.up.sql b/crates/db/migrations/20260415000001_create_user_sessions.up.sql new file mode 100644 index 0000000..150980b --- /dev/null +++ b/crates/db/migrations/20260415000001_create_user_sessions.up.sql @@ -0,0 +1,16 @@ +-- Phase 1.1: Create user_sessions table +-- Migration: 20260415000001 + +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); +CREATE INDEX IF NOT EXISTS idx_user_sessions_expires ON user_sessions(expires_at); diff --git a/crates/db/migrations/20260415000002_add_users_missing_columns.down.sql b/crates/db/migrations/20260415000002_add_users_missing_columns.down.sql new file mode 100644 index 0000000..a4db613 --- /dev/null +++ b/crates/db/migrations/20260415000002_add_users_missing_columns.down.sql @@ -0,0 +1,4 @@ +-- Rollback: Remove added columns from users +ALTER TABLE users DROP COLUMN IF EXISTS account_type; +ALTER TABLE users DROP COLUMN IF EXISTS last_login_at; +ALTER TABLE users DROP COLUMN IF EXISTS updated_at; diff --git a/crates/db/migrations/20260415000002_add_users_missing_columns.up.sql b/crates/db/migrations/20260415000002_add_users_missing_columns.up.sql new file mode 100644 index 0000000..180fd88 --- /dev/null +++ b/crates/db/migrations/20260415000002_add_users_missing_columns.up.sql @@ -0,0 +1,8 @@ +-- Phase 1.2: Add missing columns to users table +-- Migration: 20260415000002 + +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()) WHERE updated_at IS NULL; diff --git a/crates/db/migrations/20260415000003_update_departments.down.sql b/crates/db/migrations/20260415000003_update_departments.down.sql new file mode 100644 index 0000000..723c472 --- /dev/null +++ b/crates/db/migrations/20260415000003_update_departments.down.sql @@ -0,0 +1,11 @@ +-- Rollback: Remove added columns from departments +ALTER TABLE departments DROP COLUMN IF EXISTS code; +ALTER TABLE departments DROP COLUMN IF EXISTS description; +ALTER TABLE departments DROP COLUMN IF EXISTS department_head; +ALTER TABLE departments DROP COLUMN IF EXISTS department_email; +ALTER TABLE departments DROP COLUMN IF EXISTS visibility; +ALTER TABLE departments DROP COLUMN IF EXISTS transfers_enabled; +ALTER TABLE departments DROP COLUMN IF EXISTS updated_at; + +DROP INDEX IF EXISTS idx_departments_code; +DROP INDEX IF EXISTS idx_departments_is_active; diff --git a/crates/db/migrations/20260415000003_update_departments.up.sql b/crates/db/migrations/20260415000003_update_departments.up.sql new file mode 100644 index 0000000..542c50f --- /dev/null +++ b/crates/db/migrations/20260415000003_update_departments.up.sql @@ -0,0 +1,15 @@ +-- Phase 1.3: Update departments table with new fields +-- Migration: 20260415000003 + +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(); + +UPDATE departments SET updated_at = COALESCE(updated_at, created_at, NOW()) WHERE updated_at IS NULL; + +CREATE UNIQUE INDEX IF NOT EXISTS idx_departments_code ON departments(LOWER(code)) WHERE code IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_departments_is_active ON departments(is_active); diff --git a/crates/db/migrations/20260415000004_update_designations.down.sql b/crates/db/migrations/20260415000004_update_designations.down.sql new file mode 100644 index 0000000..a3412ad --- /dev/null +++ b/crates/db/migrations/20260415000004_update_designations.down.sql @@ -0,0 +1,12 @@ +-- Rollback: Remove added columns from designations +ALTER TABLE designations DROP COLUMN IF EXISTS code; +ALTER TABLE designations DROP COLUMN IF EXISTS department_id; +ALTER TABLE designations DROP COLUMN IF EXISTS description; +ALTER TABLE designations DROP COLUMN IF EXISTS level; +ALTER TABLE designations DROP COLUMN IF EXISTS can_manage_team; +ALTER TABLE designations DROP COLUMN IF EXISTS can_approve; +ALTER TABLE designations DROP COLUMN IF EXISTS is_active; +ALTER TABLE designations DROP COLUMN IF EXISTS updated_at; + +DROP INDEX IF EXISTS idx_designations_code; +DROP INDEX IF EXISTS idx_designations_is_active; diff --git a/crates/db/migrations/20260415000004_update_designations.up.sql b/crates/db/migrations/20260415000004_update_designations.up.sql new file mode 100644 index 0000000..6f2d561 --- /dev/null +++ b/crates/db/migrations/20260415000004_update_designations.up.sql @@ -0,0 +1,16 @@ +-- Phase 1.4: Update designations table with new fields +-- Migration: 20260415000004 + +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) ON DELETE SET NULL; +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(); + +UPDATE designations SET updated_at = COALESCE(updated_at, created_at, NOW()) WHERE updated_at IS NULL; + +CREATE UNIQUE INDEX IF NOT EXISTS idx_designations_code ON designations(LOWER(code)) WHERE code IS NOT NULL; +CREATE INDEX IF NOT EXISTS idx_designations_is_active ON designations(is_active); diff --git a/crates/db/migrations/20260415000005_update_employees.down.sql b/crates/db/migrations/20260415000005_update_employees.down.sql new file mode 100644 index 0000000..c34e788 --- /dev/null +++ b/crates/db/migrations/20260415000005_update_employees.down.sql @@ -0,0 +1,8 @@ +-- Rollback: Remove added columns from employees +ALTER TABLE employees DROP COLUMN IF EXISTS joining_date; +ALTER TABLE employees DROP COLUMN IF EXISTS employment_status; +ALTER TABLE employees DROP COLUMN IF EXISTS manager_employee_id; +ALTER TABLE employees DROP COLUMN IF EXISTS updated_at; + +DROP INDEX IF EXISTS idx_employees_manager; +DROP INDEX IF EXISTS idx_employees_status; diff --git a/crates/db/migrations/20260415000005_update_employees.up.sql b/crates/db/migrations/20260415000005_update_employees.up.sql new file mode 100644 index 0000000..9b6723e --- /dev/null +++ b/crates/db/migrations/20260415000005_update_employees.up.sql @@ -0,0 +1,12 @@ +-- Phase 1.5: Update employees table with new fields +-- Migration: 20260415000005 + +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) ON DELETE SET NULL; +ALTER TABLE employees ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); + +UPDATE employees SET updated_at = COALESCE(updated_at, created_at, NOW()) WHERE updated_at IS NULL; + +CREATE INDEX IF NOT EXISTS idx_employees_manager ON employees(manager_employee_id); +CREATE INDEX IF NOT EXISTS idx_employees_status ON employees(employment_status); diff --git a/crates/db/migrations/20260415010001_create_user_role_profiles.down.sql b/crates/db/migrations/20260415010001_create_user_role_profiles.down.sql new file mode 100644 index 0000000..532bb47 --- /dev/null +++ b/crates/db/migrations/20260415010001_create_user_role_profiles.down.sql @@ -0,0 +1,3 @@ +-- Rollback: Drop user_role_profiles table +-- WARNING: This will fail if data exists and FK constraints are in place +DROP TABLE IF EXISTS user_role_profiles CASCADE; diff --git a/crates/db/migrations/20260415010001_create_user_role_profiles.up.sql b/crates/db/migrations/20260415010001_create_user_role_profiles.up.sql new file mode 100644 index 0000000..58ef62d --- /dev/null +++ b/crates/db/migrations/20260415010001_create_user_role_profiles.up.sql @@ -0,0 +1,31 @@ +-- Phase 2.1: Create user_role_profiles root table (CRITICAL) +-- Migration: 20260415010001 +-- This is the ROOT table for all user role profiles + +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); +CREATE INDEX IF NOT EXISTS idx_user_role_profiles_verification ON user_role_profiles(verification_status); +CREATE INDEX IF NOT EXISTS idx_user_role_profiles_approval ON user_role_profiles(approval_status); diff --git a/crates/db/migrations/20260415010002_backfill_user_role_profiles.down.sql b/crates/db/migrations/20260415010002_backfill_user_role_profiles.down.sql new file mode 100644 index 0000000..bb0ee89 --- /dev/null +++ b/crates/db/migrations/20260415010002_backfill_user_role_profiles.down.sql @@ -0,0 +1,2 @@ +-- Rollback: Clear backfilled data (run before dropping user_role_profiles) +DELETE FROM user_role_profiles WHERE created_at > '2024-04-15'; diff --git a/crates/db/migrations/20260415010002_backfill_user_role_profiles.up.sql b/crates/db/migrations/20260415010002_backfill_user_role_profiles.up.sql new file mode 100644 index 0000000..46397d5 --- /dev/null +++ b/crates/db/migrations/20260415010002_backfill_user_role_profiles.up.sql @@ -0,0 +1,262 @@ +-- Phase 2.2: Backfill user_role_profiles from existing profile tables +-- Migration: 20260415010002 + +-- Backfill from photographer_profiles +INSERT INTO user_role_profiles (id, user_id, role_key, display_name, bio, location, status, verification_status, approval_status, rejection_reason, 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'), + CASE WHEN p.status = 'VERIFIED' THEN 'VERIFIED' WHEN p.status = 'REJECTED' THEN 'REJECTED' ELSE 'PENDING' END, + CASE WHEN p.status = 'APPROVED' THEN 'APPROVED' WHEN p.status = 'REJECTED' THEN 'REJECTED' ELSE 'PENDING' END, + p.rejection_reason, + p.approved_at, + p.created_at, + COALESCE(p.updated_at, NOW()) +FROM photographer_profiles p +WHERE NOT EXISTS ( + SELECT 1 FROM user_role_profiles urp WHERE urp.user_id = p.user_id AND urp.role_key = 'photographer' +); + +-- Backfill from tutor_profiles +INSERT INTO user_role_profiles (id, user_id, role_key, display_name, bio, location, status, verification_status, approval_status, rejection_reason, approved_at, created_at, updated_at) +SELECT + gen_random_uuid(), + p.user_id, + 'tutor', + COALESCE(p.display_name, ''), + p.bio, + p.location, + COALESCE(p.status, 'ACTIVE'), + CASE WHEN p.status = 'VERIFIED' THEN 'VERIFIED' WHEN p.status = 'REJECTED' THEN 'REJECTED' ELSE 'PENDING' END, + CASE WHEN p.status = 'APPROVED' THEN 'APPROVED' WHEN p.status = 'REJECTED' THEN 'REJECTED' ELSE 'PENDING' END, + p.rejection_reason, + p.approved_at, + p.created_at, + COALESCE(p.updated_at, NOW()) +FROM tutor_profiles p +WHERE NOT EXISTS ( + SELECT 1 FROM user_role_profiles urp WHERE urp.user_id = p.user_id AND urp.role_key = 'tutor' +); + +-- Backfill from makeup_artist_profiles +INSERT INTO user_role_profiles (id, user_id, role_key, display_name, bio, location, status, verification_status, approval_status, rejection_reason, approved_at, created_at, updated_at) +SELECT + gen_random_uuid(), + p.user_id, + 'makeup_artist', + COALESCE(p.display_name, ''), + p.bio, + p.location, + COALESCE(p.status, 'ACTIVE'), + CASE WHEN p.status = 'VERIFIED' THEN 'VERIFIED' WHEN p.status = 'REJECTED' THEN 'REJECTED' ELSE 'PENDING' END, + CASE WHEN p.status = 'APPROVED' THEN 'APPROVED' WHEN p.status = 'REJECTED' THEN 'REJECTED' ELSE 'PENDING' END, + p.rejection_reason, + p.approved_at, + p.created_at, + COALESCE(p.updated_at, NOW()) +FROM makeup_artist_profiles p +WHERE NOT EXISTS ( + SELECT 1 FROM user_role_profiles urp WHERE urp.user_id = p.user_id AND urp.role_key = 'makeup_artist' +); + +-- Backfill from developer_profiles +INSERT INTO user_role_profiles (id, user_id, role_key, display_name, bio, location, status, verification_status, approval_status, rejection_reason, approved_at, created_at, updated_at) +SELECT + gen_random_uuid(), + p.user_id, + 'developer', + COALESCE(p.display_name, ''), + p.bio, + p.location, + COALESCE(p.status, 'ACTIVE'), + CASE WHEN p.status = 'VERIFIED' THEN 'VERIFIED' WHEN p.status = 'REJECTED' THEN 'REJECTED' ELSE 'PENDING' END, + CASE WHEN p.status = 'APPROVED' THEN 'APPROVED' WHEN p.status = 'REJECTED' THEN 'REJECTED' ELSE 'PENDING' END, + p.rejection_reason, + p.approved_at, + p.created_at, + COALESCE(p.updated_at, NOW()) +FROM developer_profiles p +WHERE NOT EXISTS ( + SELECT 1 FROM user_role_profiles urp WHERE urp.user_id = p.user_id AND urp.role_key = 'developer' +); + +-- Backfill from video_editor_profiles +INSERT INTO user_role_profiles (id, user_id, role_key, display_name, bio, location, status, verification_status, approval_status, rejection_reason, approved_at, created_at, updated_at) +SELECT + gen_random_uuid(), + p.user_id, + 'video_editor', + COALESCE(p.display_name, ''), + p.bio, + p.location, + COALESCE(p.status, 'ACTIVE'), + CASE WHEN p.status = 'VERIFIED' THEN 'VERIFIED' WHEN p.status = 'REJECTED' THEN 'REJECTED' ELSE 'PENDING' END, + CASE WHEN p.status = 'APPROVED' THEN 'APPROVED' WHEN p.status = 'REJECTED' THEN 'REJECTED' ELSE 'PENDING' END, + p.rejection_reason, + p.approved_at, + p.created_at, + COALESCE(p.updated_at, NOW()) +FROM video_editor_profiles p +WHERE NOT EXISTS ( + SELECT 1 FROM user_role_profiles urp WHERE urp.user_id = p.user_id AND urp.role_key = 'video_editor' +); + +-- Backfill from graphic_designer_profiles +INSERT INTO user_role_profiles (id, user_id, role_key, display_name, bio, location, status, verification_status, approval_status, rejection_reason, approved_at, created_at, updated_at) +SELECT + gen_random_uuid(), + p.user_id, + 'graphic_designer', + COALESCE(p.display_name, ''), + p.bio, + p.location, + COALESCE(p.status, 'ACTIVE'), + CASE WHEN p.status = 'VERIFIED' THEN 'VERIFIED' WHEN p.status = 'REJECTED' THEN 'REJECTED' ELSE 'PENDING' END, + CASE WHEN p.status = 'APPROVED' THEN 'APPROVED' WHEN p.status = 'REJECTED' THEN 'REJECTED' ELSE 'PENDING' END, + p.rejection_reason, + p.approved_at, + p.created_at, + COALESCE(p.updated_at, NOW()) +FROM graphic_designer_profiles p +WHERE NOT EXISTS ( + SELECT 1 FROM user_role_profiles urp WHERE urp.user_id = p.user_id AND urp.role_key = 'graphic_designer' +); + +-- Backfill from social_media_manager_profiles +INSERT INTO user_role_profiles (id, user_id, role_key, display_name, bio, location, status, verification_status, approval_status, rejection_reason, approved_at, created_at, updated_at) +SELECT + gen_random_uuid(), + p.user_id, + 'social_media_manager', + COALESCE(p.display_name, ''), + p.bio, + p.location, + COALESCE(p.status, 'ACTIVE'), + CASE WHEN p.status = 'VERIFIED' THEN 'VERIFIED' WHEN p.status = 'REJECTED' THEN 'REJECTED' ELSE 'PENDING' END, + CASE WHEN p.status = 'APPROVED' THEN 'APPROVED' WHEN p.status = 'REJECTED' THEN 'REJECTED' ELSE 'PENDING' END, + p.rejection_reason, + p.approved_at, + p.created_at, + COALESCE(p.updated_at, NOW()) +FROM social_media_manager_profiles p +WHERE NOT EXISTS ( + SELECT 1 FROM user_role_profiles urp WHERE urp.user_id = p.user_id AND urp.role_key = 'social_media_manager' +); + +-- Backfill from fitness_trainer_profiles +INSERT INTO user_role_profiles (id, user_id, role_key, display_name, bio, location, status, verification_status, approval_status, rejection_reason, approved_at, created_at, updated_at) +SELECT + gen_random_uuid(), + p.user_id, + 'fitness_trainer', + COALESCE(p.display_name, ''), + p.bio, + p.location, + COALESCE(p.status, 'ACTIVE'), + CASE WHEN p.status = 'VERIFIED' THEN 'VERIFIED' WHEN p.status = 'REJECTED' THEN 'REJECTED' ELSE 'PENDING' END, + CASE WHEN p.status = 'APPROVED' THEN 'APPROVED' WHEN p.status = 'REJECTED' THEN 'REJECTED' ELSE 'PENDING' END, + p.rejection_reason, + p.approved_at, + p.created_at, + COALESCE(p.updated_at, NOW()) +FROM fitness_trainer_profiles p +WHERE NOT EXISTS ( + SELECT 1 FROM user_role_profiles urp WHERE urp.user_id = p.user_id AND urp.role_key = 'fitness_trainer' +); + +-- Backfill from catering_service_profiles +INSERT INTO user_role_profiles (id, user_id, role_key, display_name, bio, location, status, verification_status, approval_status, rejection_reason, approved_at, created_at, updated_at) +SELECT + gen_random_uuid(), + p.user_id, + 'catering_service', + COALESCE(p.business_name, ''), + p.bio, + p.location, + COALESCE(p.status, 'ACTIVE'), + CASE WHEN p.status = 'VERIFIED' THEN 'VERIFIED' WHEN p.status = 'REJECTED' THEN 'REJECTED' ELSE 'PENDING' END, + CASE WHEN p.status = 'APPROVED' THEN 'APPROVED' WHEN p.status = 'REJECTED' THEN 'REJECTED' ELSE 'PENDING' END, + p.rejection_reason, + p.approved_at, + p.created_at, + COALESCE(p.updated_at, NOW()) +FROM catering_service_profiles p +WHERE NOT EXISTS ( + SELECT 1 FROM user_role_profiles urp WHERE urp.user_id = p.user_id AND urp.role_key = 'catering_service' +); + +-- Backfill from ugc_content_creator_profiles +INSERT INTO user_role_profiles (id, user_id, role_key, display_name, bio, location, status, verification_status, approval_status, rejection_reason, approved_at, created_at, updated_at) +SELECT + gen_random_uuid(), + p.user_id, + 'ugc_content_creator', + COALESCE(p.display_name, ''), + p.bio, + p.location, + COALESCE(p.status, 'ACTIVE'), + CASE WHEN p.status = 'VERIFIED' THEN 'VERIFIED' WHEN p.status = 'REJECTED' THEN 'REJECTED' ELSE 'PENDING' END, + CASE WHEN p.status = 'APPROVED' THEN 'APPROVED' WHEN p.status = 'REJECTED' THEN 'REJECTED' ELSE 'PENDING' END, + p.rejection_reason, + p.approved_at, + p.created_at, + COALESCE(p.updated_at, NOW()) +FROM ugc_content_creator_profiles p +WHERE NOT EXISTS ( + SELECT 1 FROM user_role_profiles urp WHERE urp.user_id = p.user_id AND urp.role_key = 'ugc_content_creator' +); + +-- 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 +WHERE NOT EXISTS ( + SELECT 1 FROM user_role_profiles urp WHERE urp.user_id = cp.user_id AND urp.role_key = 'company' +); + +-- 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 +WHERE NOT EXISTS ( + SELECT 1 FROM user_role_profiles urp WHERE urp.user_id = cp.user_id AND urp.role_key = 'customer' +); + +-- Backfill from job_seeker_profiles +INSERT INTO user_role_profiles (id, user_id, role_key, display_name, bio, location, status, created_at, updated_at) +SELECT + gen_random_uuid(), + jsp.user_id, + 'candidate', + COALESCE(jsp.full_name, ''), + jsp.bio, + jsp.location, + COALESCE(jsp.status, 'ACTIVE'), + jsp.created_at, + COALESCE(jsp.updated_at, NOW()) +FROM job_seeker_profiles jsp +WHERE NOT EXISTS ( + SELECT 1 FROM user_role_profiles urp WHERE urp.user_id = jsp.user_id AND urp.role_key = 'candidate' +); diff --git a/crates/db/migrations/20260415010003_add_user_role_profile_id.down.sql b/crates/db/migrations/20260415010003_add_user_role_profile_id.down.sql new file mode 100644 index 0000000..c5d1c76 --- /dev/null +++ b/crates/db/migrations/20260415010003_add_user_role_profile_id.down.sql @@ -0,0 +1,23 @@ +-- Rollback: Remove user_role_profile_id columns +-- WARNING: This will fail if FK constraints exist +ALTER TABLE photographer_profiles DROP COLUMN IF EXISTS user_role_profile_id; +ALTER TABLE tutor_profiles DROP COLUMN IF EXISTS user_role_profile_id; +ALTER TABLE makeup_artist_profiles DROP COLUMN IF EXISTS user_role_profile_id; +ALTER TABLE developer_profiles DROP COLUMN IF EXISTS user_role_profile_id; +ALTER TABLE video_editor_profiles DROP COLUMN IF EXISTS user_role_profile_id; +ALTER TABLE graphic_designer_profiles DROP COLUMN IF EXISTS user_role_profile_id; +ALTER TABLE social_media_manager_profiles DROP COLUMN IF EXISTS user_role_profile_id; +ALTER TABLE fitness_trainer_profiles DROP COLUMN IF EXISTS user_role_profile_id; +ALTER TABLE catering_service_profiles DROP COLUMN IF EXISTS user_role_profile_id; +ALTER TABLE ugc_content_creator_profiles DROP COLUMN IF EXISTS user_role_profile_id; + +DROP INDEX IF EXISTS idx_photographer_profiles_user_role; +DROP INDEX IF EXISTS idx_tutor_profiles_user_role; +DROP INDEX IF EXISTS idx_makeup_artist_profiles_user_role; +DROP INDEX IF EXISTS idx_developer_profiles_user_role; +DROP INDEX IF EXISTS idx_video_editor_profiles_user_role; +DROP INDEX IF EXISTS idx_graphic_designer_profiles_user_role; +DROP INDEX IF EXISTS idx_social_media_manager_profiles_user_role; +DROP INDEX IF EXISTS idx_fitness_trainer_profiles_user_role; +DROP INDEX IF EXISTS idx_catering_service_profiles_user_role; +DROP INDEX IF EXISTS idx_ugc_content_creator_profiles_user_role; diff --git a/crates/db/migrations/20260415010003_add_user_role_profile_id.up.sql b/crates/db/migrations/20260415010003_add_user_role_profile_id.up.sql new file mode 100644 index 0000000..fec1959 --- /dev/null +++ b/crates/db/migrations/20260415010003_add_user_role_profile_id.up.sql @@ -0,0 +1,85 @@ +-- Phase 2.3: Add user_role_profile_id to extension tables +-- Migration: 20260415010003 +-- This links existing extension tables to the new user_role_profiles root + +-- photographer_profiles +ALTER TABLE photographer_profiles ADD COLUMN IF NOT EXISTS user_role_profile_id UUID; +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'; +ALTER TABLE photographer_profiles ALTER COLUMN user_role_profile_id SET NOT NULL; + +-- tutor_profiles +ALTER TABLE tutor_profiles ADD COLUMN IF NOT EXISTS user_role_profile_id UUID; +UPDATE tutor_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 = 'tutor'; +ALTER TABLE tutor_profiles ALTER COLUMN user_role_profile_id SET NOT NULL; + +-- makeup_artist_profiles +ALTER TABLE makeup_artist_profiles ADD COLUMN IF NOT EXISTS user_role_profile_id UUID; +UPDATE makeup_artist_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 = 'makeup_artist'; +ALTER TABLE makeup_artist_profiles ALTER COLUMN user_role_profile_id SET NOT NULL; + +-- developer_profiles +ALTER TABLE developer_profiles ADD COLUMN IF NOT EXISTS user_role_profile_id UUID; +UPDATE developer_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 = 'developer'; +ALTER TABLE developer_profiles ALTER COLUMN user_role_profile_id SET NOT NULL; + +-- video_editor_profiles +ALTER TABLE video_editor_profiles ADD COLUMN IF NOT EXISTS user_role_profile_id UUID; +UPDATE video_editor_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 = 'video_editor'; +ALTER TABLE video_editor_profiles ALTER COLUMN user_role_profile_id SET NOT NULL; + +-- graphic_designer_profiles +ALTER TABLE graphic_designer_profiles ADD COLUMN IF NOT EXISTS user_role_profile_id UUID; +UPDATE graphic_designer_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 = 'graphic_designer'; +ALTER TABLE graphic_designer_profiles ALTER COLUMN user_role_profile_id SET NOT NULL; + +-- social_media_manager_profiles +ALTER TABLE social_media_manager_profiles ADD COLUMN IF NOT EXISTS user_role_profile_id UUID; +UPDATE social_media_manager_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 = 'social_media_manager'; +ALTER TABLE social_media_manager_profiles ALTER COLUMN user_role_profile_id SET NOT NULL; + +-- fitness_trainer_profiles +ALTER TABLE fitness_trainer_profiles ADD COLUMN IF NOT EXISTS user_role_profile_id UUID; +UPDATE fitness_trainer_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 = 'fitness_trainer'; +ALTER TABLE fitness_trainer_profiles ALTER COLUMN user_role_profile_id SET NOT NULL; + +-- catering_service_profiles +ALTER TABLE catering_service_profiles ADD COLUMN IF NOT EXISTS user_role_profile_id UUID; +UPDATE catering_service_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 = 'catering_service'; +ALTER TABLE catering_service_profiles ALTER COLUMN user_role_profile_id SET NOT NULL; + +-- ugc_content_creator_profiles +ALTER TABLE ugc_content_creator_profiles ADD COLUMN IF NOT EXISTS user_role_profile_id UUID; +UPDATE ugc_content_creator_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 = 'ugc_content_creator'; +ALTER TABLE ugc_content_creator_profiles ALTER COLUMN user_role_profile_id SET NOT NULL; + +-- Create indexes +CREATE INDEX IF NOT EXISTS idx_photographer_profiles_user_role ON photographer_profiles(user_role_profile_id); +CREATE INDEX IF NOT EXISTS idx_tutor_profiles_user_role ON tutor_profiles(user_role_profile_id); +CREATE INDEX IF NOT EXISTS idx_makeup_artist_profiles_user_role ON makeup_artist_profiles(user_role_profile_id); +CREATE INDEX IF NOT EXISTS idx_developer_profiles_user_role ON developer_profiles(user_role_profile_id); +CREATE INDEX IF NOT EXISTS idx_video_editor_profiles_user_role ON video_editor_profiles(user_role_profile_id); +CREATE INDEX IF NOT EXISTS idx_graphic_designer_profiles_user_role ON graphic_designer_profiles(user_role_profile_id); +CREATE INDEX IF NOT EXISTS idx_social_media_manager_profiles_user_role ON social_media_manager_profiles(user_role_profile_id); +CREATE INDEX IF NOT EXISTS idx_fitness_trainer_profiles_user_role ON fitness_trainer_profiles(user_role_profile_id); +CREATE INDEX IF NOT EXISTS idx_catering_service_profiles_user_role ON catering_service_profiles(user_role_profile_id); +CREATE INDEX IF NOT EXISTS idx_ugc_content_creator_profiles_user_role ON ugc_content_creator_profiles(user_role_profile_id); diff --git a/crates/db/migrations/20260415010004_remove_external_links.down.sql b/crates/db/migrations/20260415010004_remove_external_links.down.sql new file mode 100644 index 0000000..c6d9593 --- /dev/null +++ b/crates/db/migrations/20260415010004_remove_external_links.down.sql @@ -0,0 +1,3 @@ +-- Rollback: Cannot easily restore removed columns +-- This migration is NOT easily reversible +-- Only run after full backup and testing diff --git a/crates/db/migrations/20260415010004_remove_external_links.up.sql b/crates/db/migrations/20260415010004_remove_external_links.up.sql new file mode 100644 index 0000000..334c090 --- /dev/null +++ b/crates/db/migrations/20260415010004_remove_external_links.up.sql @@ -0,0 +1,31 @@ +-- Phase 2.4: Remove forbidden external portfolio links +-- Migration: 20260415010004 +-- Per source of truth: NO external portfolio links allowed + +-- Remove github_url, portfolio_url from developer_profiles +ALTER TABLE developer_profiles DROP COLUMN IF EXISTS github_url; +ALTER TABLE developer_profiles DROP COLUMN IF EXISTS portfolio_url; + +-- Remove reel_url from video_editor_profiles +ALTER TABLE video_editor_profiles DROP COLUMN IF EXISTS reel_url; + +-- Remove portfolio_url from graphic_designer_profiles +ALTER TABLE graphic_designer_profiles DROP COLUMN IF EXISTS portfolio_url; + +-- Remove portfolio_url from photographer_profiles +ALTER TABLE photographer_profiles DROP COLUMN IF EXISTS portfolio_url; + +-- Remove custom_data from all extension tables (preserve as JSONB if needed) +ALTER TABLE photographer_profiles DROP COLUMN IF EXISTS custom_data; +ALTER TABLE tutor_profiles DROP COLUMN IF EXISTS custom_data; +ALTER TABLE makeup_artist_profiles DROP COLUMN IF EXISTS custom_data; +ALTER TABLE developer_profiles DROP COLUMN IF EXISTS custom_data; +ALTER TABLE video_editor_profiles DROP COLUMN IF EXISTS custom_data; +ALTER TABLE graphic_designer_profiles DROP COLUMN IF EXISTS custom_data; +ALTER TABLE social_media_manager_profiles DROP COLUMN IF EXISTS custom_data; +ALTER TABLE fitness_trainer_profiles DROP COLUMN IF EXISTS custom_data; +ALTER TABLE catering_service_profiles DROP COLUMN IF EXISTS custom_data; +ALTER TABLE ugc_content_creator_profiles DROP COLUMN IF EXISTS custom_data; + +-- Rename inconsistent columns +ALTER TABLE tutor_profiles RENAME COLUMN subjects_taught TO subjects; diff --git a/crates/db/src/models/mod.rs b/crates/db/src/models/mod.rs index 4d10aef..5e40ef3 100644 --- a/crates/db/src/models/mod.rs +++ b/crates/db/src/models/mod.rs @@ -25,4 +25,5 @@ pub mod employee; pub mod department; pub mod designation; pub mod verification; +pub mod user_role_profile; diff --git a/crates/db/src/models/photographer.rs b/crates/db/src/models/photographer.rs index 7a25bcb..229ad7c 100644 --- a/crates/db/src/models/photographer.rs +++ b/crates/db/src/models/photographer.rs @@ -6,58 +6,67 @@ use uuid::Uuid; #[derive(Debug, Serialize, Deserialize, FromRow)] pub struct PhotographerProfile { pub id: Uuid, - pub user_id: Uuid, - pub display_name: Option, - pub bio: Option, - pub location: Option, - pub custom_data: Option, - pub status: String, + pub user_role_profile_id: Uuid, + pub specialties: Vec, + pub camera_brands: Vec, + pub studio_available: bool, + pub outdoor_shoots: bool, + pub travel_radius_km: Option, + pub starting_price_inr: Option, pub created_at: DateTime, pub updated_at: DateTime, } #[derive(Debug, Serialize, Deserialize)] pub struct UpsertPhotographerProfilePayload { - pub display_name: Option, - pub bio: Option, - pub location: Option, - pub custom_data: Option, + pub specialties: Vec, + pub camera_brands: Vec, + pub studio_available: bool, + pub outdoor_shoots: bool, + pub travel_radius_km: Option, + pub starting_price_inr: Option, } pub struct PhotographerRepository; impl PhotographerRepository { - pub async fn get_by_user_id(pool: &PgPool, user_id: Uuid) -> Result, sqlx::Error> { + pub async fn get_by_user_role_id(pool: &PgPool, user_role_profile_id: Uuid) -> Result, sqlx::Error> { sqlx::query_as::<_, PhotographerProfile>( - r#"SELECT id, user_id, display_name, bio, location, - custom_data, - status, created_at, updated_at - FROM photographer_profiles WHERE user_id = $1"#, + r#"SELECT id, user_role_profile_id, specialties, camera_brands, + studio_available, outdoor_shoots, travel_radius_km, + starting_price_inr, + created_at, updated_at + FROM photographer_profiles WHERE user_role_profile_id = $1"#, ) - .bind(user_id) + .bind(user_role_profile_id) .fetch_optional(pool) .await } - pub async fn upsert(pool: &PgPool, user_id: Uuid, p: UpsertPhotographerProfilePayload) -> Result { + pub async fn upsert(pool: &PgPool, user_role_profile_id: Uuid, p: UpsertPhotographerProfilePayload) -> Result { sqlx::query_as::<_, PhotographerProfile>( - r#"INSERT INTO photographer_profiles (user_id, display_name, bio, location, custom_data) - VALUES ($1, $2, $3, $4, $5) - ON CONFLICT (user_id) DO UPDATE SET - display_name = COALESCE(EXCLUDED.display_name, photographer_profiles.display_name), - bio = EXCLUDED.bio, - location = EXCLUDED.location, - custom_data = EXCLUDED.custom_data, - updated_at = NOW() - RETURNING id, user_id, display_name, bio, location, - custom_data, - status, created_at, updated_at"#, + r#"INSERT INTO photographer_profiles (user_role_profile_id, specialties, camera_brands, + studio_available, outdoor_shoots, travel_radius_km, starting_price_inr) + VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT (user_role_profile_id) DO UPDATE SET + specialties = COALESCE(EXCLUDED.specialties, photographer_profiles.specialties), + camera_brands = COALESCE(EXCLUDED.camera_brands, photographer_profiles.camera_brands), + studio_available = EXCLUDED.studio_available, + outdoor_shoots = EXCLUDED.outdoor_shoots, + travel_radius_km = EXCLUDED.travel_radius_km, + starting_price_inr = EXCLUDED.starting_price_inr, + updated_at = NOW() + RETURNING id, user_role_profile_id, specialties, camera_brands, + studio_available, outdoor_shoots, travel_radius_km, + starting_price_inr, created_at, updated_at"#, ) - .bind(user_id) - .bind(p.display_name) - .bind(p.bio) - .bind(p.location) - .bind(p.custom_data) + .bind(user_role_profile_id) + .bind(&p.specialties) + .bind(&p.camera_brands) + .bind(p.studio_available) + .bind(p.outdoor_shoots) + .bind(p.travel_radius_km) + .bind(p.starting_price_inr) .fetch_one(pool) .await } diff --git a/crates/db/src/models/user_role_profile.rs b/crates/db/src/models/user_role_profile.rs new file mode 100644 index 0000000..0735d1f --- /dev/null +++ b/crates/db/src/models/user_role_profile.rs @@ -0,0 +1,237 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use sqlx::{FromRow, PgPool}; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize, FromRow)] +pub struct UserRoleProfile { + pub id: Uuid, + pub user_id: Uuid, + pub role_key: String, + pub display_name: Option, + pub bio: Option, + pub location: Option, + pub avatar_url: Option, + pub phone: Option, + pub email: Option, + pub status: String, + pub verification_status: String, + pub approval_status: String, + pub rejection_reason: Option, + pub approved_at: Option>, + pub verified_at: Option>, + pub is_profile_public: bool, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UserRoleProfileWithExtension { + #[serde(flatten)] + pub profile: UserRoleProfile, + #[serde(flatten)] + pub extension: T, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum RoleKey { + Photographer, + Tutor, + MakeupArtist, + Developer, + VideoEditor, + GraphicDesigner, + SocialMediaManager, + FitnessTrainer, + CateringService, + UgcContentCreator, + Company, + Customer, + Candidate, +} + +impl RoleKey { + pub fn as_str(&self) -> &'static str { + match self { + RoleKey::Photographer => "photographer", + RoleKey::Tutor => "tutor", + RoleKey::MakeupArtist => "makeup_artist", + RoleKey::Developer => "developer", + RoleKey::VideoEditor => "video_editor", + RoleKey::GraphicDesigner => "graphic_designer", + RoleKey::SocialMediaManager => "social_media_manager", + RoleKey::FitnessTrainer => "fitness_trainer", + RoleKey::CateringService => "catering_service", + RoleKey::UgcContentCreator => "ugc_content_creator", + RoleKey::Company => "company", + RoleKey::Customer => "customer", + RoleKey::Candidate => "candidate", + } + } +} + +pub struct UserRoleProfileRepository; + +impl UserRoleProfileRepository { + pub async fn get_by_user_and_role( + pool: &PgPool, + user_id: Uuid, + role_key: &str, + ) -> Result, sqlx::Error> { + sqlx::query_as::<_, UserRoleProfile>( + r#"SELECT id, user_id, role_key, display_name, bio, location, + avatar_url, phone, email, status, + verification_status, approval_status, rejection_reason, + approved_at, verified_at, is_profile_public, + created_at, updated_at + FROM user_role_profiles + WHERE user_id = $1 AND role_key = $2"#, + ) + .bind(user_id) + .bind(role_key) + .fetch_optional(pool) + .await + } + + pub async fn get_by_id( + pool: &PgPool, + id: Uuid, + ) -> Result, sqlx::Error> { + sqlx::query_as::<_, UserRoleProfile>( + r#"SELECT id, user_id, role_key, display_name, bio, location, + avatar_url, phone, email, status, + verification_status, approval_status, rejection_reason, + approved_at, verified_at, is_profile_public, + created_at, updated_at + FROM user_role_profiles + WHERE id = $1"#, + ) + .bind(id) + .fetch_optional(pool) + .await + } + + pub async fn get_all_by_user( + pool: &PgPool, + user_id: Uuid, + ) -> Result, sqlx::Error> { + sqlx::query_as::<_, UserRoleProfile>( + r#"SELECT id, user_id, role_key, display_name, bio, location, + avatar_url, phone, email, status, + verification_status, approval_status, rejection_reason, + approved_at, verified_at, is_profile_public, + created_at, updated_at + FROM user_role_profiles + WHERE user_id = $1"#, + ) + .bind(user_id) + .fetch_all(pool) + .await + } + + pub async fn create( + pool: &PgPool, + user_id: Uuid, + role_key: &str, + ) -> Result { + sqlx::query_as::<_, UserRoleProfile>( + r#"INSERT INTO user_role_profiles (user_id, role_key) + VALUES ($1, $2) + ON CONFLICT (user_id, role_key) DO UPDATE SET user_id = EXCLUDED.user_id + RETURNING id, user_id, role_key, display_name, bio, location, + avatar_url, phone, email, status, + verification_status, approval_status, rejection_reason, + approved_at, verified_at, is_profile_public, + created_at, updated_at"#, + ) + .bind(user_id) + .bind(role_key) + .fetch_one(pool) + .await + } + + pub async fn update( + pool: &PgPool, + id: Uuid, + display_name: Option, + bio: Option, + location: Option, + avatar_url: Option, + phone: Option, + email: Option, + is_profile_public: bool, + ) -> Result { + sqlx::query_as::<_, UserRoleProfile>( + r#"UPDATE user_role_profiles SET + display_name = COALESCE($2, display_name), + bio = COALESCE($3, bio), + location = COALESCE($4, location), + avatar_url = COALESCE($5, avatar_url), + phone = COALESCE($6, phone), + email = COALESCE($7, email), + is_profile_public = $8, + updated_at = NOW() + WHERE id = $1 + RETURNING id, user_id, role_key, display_name, bio, location, + avatar_url, phone, email, status, + verification_status, approval_status, rejection_reason, + approved_at, verified_at, is_profile_public, + created_at, updated_at"#, + ) + .bind(id) + .bind(display_name) + .bind(bio) + .bind(location) + .bind(avatar_url) + .bind(phone) + .bind(email) + .bind(is_profile_public) + .fetch_one(pool) + .await + } + + pub async fn approve( + pool: &PgPool, + id: Uuid, + approved_by: Uuid, + ) -> Result { + sqlx::query_as::<_, UserRoleProfile>( + r#"UPDATE user_role_profiles SET + approval_status = 'APPROVED', + approved_at = NOW(), + updated_at = NOW() + WHERE id = $1 + RETURNING id, user_id, role_key, display_name, bio, location, + avatar_url, phone, email, status, + verification_status, approval_status, rejection_reason, + approved_at, verified_at, is_profile_public, + created_at, updated_at"#, + ) + .bind(id) + .fetch_one(pool) + .await + } + + pub async fn reject( + pool: &PgPool, + id: Uuid, + rejection_reason: &str, + ) -> Result { + sqlx::query_as::<_, UserRoleProfile>( + r#"UPDATE user_role_profiles SET + approval_status = 'REJECTED', + rejection_reason = $2, + updated_at = NOW() + WHERE id = $1 + RETURNING id, user_id, role_key, display_name, bio, location, + avatar_url, phone, email, status, + verification_status, approval_status, rejection_reason, + approved_at, verified_at, is_profile_public, + created_at, updated_at"#, + ) + .bind(id) + .bind(rejection_reason) + .fetch_one(pool) + .await + } +} diff --git a/docs/migration_plan.md b/docs/migration_plan.md new file mode 100644 index 0000000..5f507ea --- /dev/null +++ b/docs/migration_plan.md @@ -0,0 +1,767 @@ +# 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 diff --git a/docs/old_to_new_mapping.md b/docs/old_to_new_mapping.md new file mode 100644 index 0000000..9e74fd2 --- /dev/null +++ b/docs/old_to_new_mapping.md @@ -0,0 +1,508 @@ +# Old to New Mapping — Nxtgauge Database Migration + +This document maps the current schema to the target schema. + +--- + +## 1. Tables to CREATE (New) + +### Core Infrastructure + +| New Table | Purpose | Priority | +| ------------------------- | --------------------------- | -------- | +| `user_sessions` | Session tracking | High | +| `user_role_profiles` | Root profile for all roles | Critical | +| `verification_requests` | Verification workflow | High | +| `verification_documents` | Verification documents | High | +| `approval_requests` | Approval workflow | High | +| `approval_logs` | Approval audit trail | High | +| `audit_logs` | Comprehensive audit logging | High | +| `audit_log_changes` | Field-level audit changes | Medium | +| `dashboard_widgets` | Dashboard widget configs | Medium | +| `kb_sections` | Knowledge base sections | Low | +| `kb_article_feedback` | KB article feedback | Low | +| `notification_templates` | Notification templates | Medium | +| `smtp_configs` | SMTP configurations | Low | +| `orders` | Order management | Medium | +| `order_items` | Order line items | Medium | +| `payment_gateway_configs` | Payment gateway configs | Low | +| `payment_transactions` | Payment transaction log | Low | +| `tax_rules` | Tax rules | Low | + +### Extension Tables (New FK Reference) + +| New Table | References | +| ------------------------------- | ----------------------------------------------- | +| `photographer_profiles` | `user_role_profiles` (via user_role_profile_id) | +| `tutor_profiles` | `user_role_profiles` | +| `makeup_artist_profiles` | `user_role_profiles` | +| `developer_profiles` | `user_role_profiles` | +| `video_editor_profiles` | `user_role_profiles` | +| `graphic_designer_profiles` | `user_role_profiles` | +| `social_media_manager_profiles` | `user_role_profiles` | +| `fitness_trainer_profiles` | `user_role_profiles` | +| `catering_service_profiles` | `user_role_profiles` | +| `ugc_content_creator_profiles` | `user_role_profiles` | + +--- + +## 2. Tables to RENAME + +| Current Name | Target Name | Notes | +| -------------- | -------------------- | --------------------- | +| `applications` | `job_applications` | Job applications | +| `requirements` | `leads` | Customer leads | +| `coupon_uses` | `coupon_redemptions` | Coupon usage tracking | + +--- + +## 3. Tables to UPDATE (Schema Changes) + +### users + +| Action | Changes | +| ------ | --------------------------------------------------- | +| ADD | `account_type` (TEXT) | +| ADD | `last_login_at` (TIMESTAMP) | +| RENAME | `full_name` → Remove (use profiles) | +| RENAME | `email_verified` → Remove (use verification_status) | +| RENAME | `phone_verified` → Remove (use verification_status) | + +### refresh_tokens + +| Action | Changes | +| ------ | ------------------- | +| ADD | `revoked` (BOOLEAN) | + +### roles + +| Action | Changes | +| ------ | -------------------------------------- | +| ADD | `description` (TEXT) | +| ADD | `department_id` (UUID) | +| ADD | `can_approve_requests` (BOOLEAN) | +| ADD | `can_manage_system_settings` (BOOLEAN) | + +### departments + +| Action | Changes | +| ------ | ----------------------------- | +| ADD | `code` (TEXT) | +| ADD | `description` (TEXT) | +| ADD | `department_head` (TEXT) | +| ADD | `department_email` (TEXT) | +| ADD | `visibility` (TEXT) | +| ADD | `transfers_enabled` (BOOLEAN) | +| ADD | `updated_at` (TIMESTAMP) | + +### designations + +| Action | Changes | +| ------ | --------------------------- | +| ADD | `code` (TEXT) | +| ADD | `department_id` (UUID) | +| ADD | `description` (TEXT) | +| ADD | `level` (TEXT) | +| ADD | `can_manage_team` (BOOLEAN) | +| ADD | `can_approve` (BOOLEAN) | +| ADD | `is_active` (BOOLEAN) | +| ADD | `updated_at` (TIMESTAMP) | + +### employees + +| Action | Changes | +| ------ | ---------------------------------- | +| ADD | `joining_date` (DATE) | +| ADD | `employment_status` (TEXT) | +| ADD | `manager_employee_id` (UUID) | +| ADD | `updated_at` (TIMESTAMP) | +| RENAME | `user_id` → Keep (points to users) | + +--- + +## 4. Extension Tables — Update FK Reference + +**Current State:** Extension tables reference `users.id` via `user_id` +**Target State:** Extension tables reference `user_role_profiles.id` via `user_role_profile_id` + +### photographer_profiles + +| Action | Changes | +| ------ | ------------------------------------------- | +| RENAME | `user_id` → `user_role_profile_id` | +| REMOVE | `portfolio_url` | +| REMOVE | `equipment_list` | +| REMOVE | `years_of_experience` (use root profile) | +| REMOVE | `hourly_rate` (use `starting_price_inr`) | +| REMOVE | `custom_data` (preserve to JSONB if needed) | + +### tutor_profiles + +| Action | Changes | +| ------ | --------------------------------------- | +| RENAME | `user_id` → `user_role_profile_id` | +| RENAME | `subjects_taught` → `subjects` | +| REMOVE | `education_level` (use `qualification`) | +| REMOVE | `custom_data` | + +### makeup_artist_profiles + +| Action | Changes | +| ------ | ---------------------------------- | +| RENAME | `user_id` → `user_role_profile_id` | +| REMOVE | `custom_data` | + +### developer_profiles + +| Action | Changes | +| ------ | ---------------------------------- | +| RENAME | `user_id` → `user_role_profile_id` | +| REMOVE | `github_url` (FORBIDDEN) | +| REMOVE | `portfolio_url` (FORBIDDEN) | +| REMOVE | `custom_data` | + +### video_editor_profiles + +| Action | Changes | +| ------ | ---------------------------------- | +| RENAME | `user_id` → `user_role_profile_id` | +| REMOVE | `reel_url` (FORBIDDEN) | +| REMOVE | `custom_data` | + +### graphic_designer_profiles + +| Action | Changes | +| ------ | ---------------------------------- | +| RENAME | `user_id` → `user_role_profile_id` | +| REMOVE | `portfolio_url` (FORBIDDEN) | +| REMOVE | `custom_data` | + +### social_media_manager_profiles + +| Action | Changes | +| ------ | ---------------------------------- | +| RENAME | `user_id` → `user_role_profile_id` | +| REMOVE | `custom_data` | + +### fitness_trainer_profiles + +| Action | Changes | +| ------ | ---------------------------------- | +| RENAME | `user_id` → `user_role_profile_id` | +| REMOVE | `custom_data` | + +### catering_service_profiles + +| Action | Changes | +| ------ | ---------------------------------- | +| RENAME | `user_id` → `user_role_profile_id` | +| REMOVE | `custom_data` | + +### ugc_content_creator_profiles + +| Action | Changes | +| ------ | ---------------------------------- | +| RENAME | `user_id` → `user_role_profile_id` | + +--- + +## 5. Other Profile Tables + +### company_profiles + +| Action | Changes | +| ------ | ----------------------------------------- | +| ADD | `verification_status` (TEXT) | +| ADD | `approval_status` (TEXT) | +| RENAME | `website_url` → Remove | +| RENAME | `registration_number` → Remove | +| RENAME | `employee_count` → Remove | +| REMOVE | Legacy fields merged into JSONB if needed | + +### customer_profiles + +| Action | Changes | +| ------ | ---------------------------------------------------- | +| ADD | `email` (TEXT) | +| RENAME | `experience_years` → Remove (use candidate_profiles) | +| RENAME | `custom_data` → Remove | + +### job_seeker_profiles + +| Action | Changes | +| ------ | ---------------------- | +| RENAME | → `candidate_profiles` | +| REMOVE | `custom_data` | + +--- + +## 6. Portfolio Domain + +### portfolio_items + +| Action | Changes | +| ------ | ------------------------------------------------ | +| RENAME | `professional_id` → `user_role_profile_id` | +| ADD | `display_order` (INTEGER) | +| REMOVE | `profession_key` (derive from user_role_profile) | + +### services + +| Action | Changes | +| ------ | ------------------------------------------------ | +| RENAME | `professional_id` → `user_role_profile_id` | +| REMOVE | `profession_key` (derive from user_role_profile) | + +--- + +## 7. Marketplace Domain + +### jobs + +| Action | Changes | +| ------ | -------------------------------------------------- | +| ADD | `posted_by_user_id` (UUID) | +| ADD | `mode_of_work` (TEXT) | +| ADD | `budget_inr` (INTEGER) | +| ADD | `salary_range_json` (JSONB) | +| RENAME | `company_id` → `company_profile_id` | +| REMOVE | `category` (use tags) | +| REMOVE | `skills` (use tags) | +| REMOVE | `salary_min`, `salary_max` (use salary_range_json) | +| REMOVE | `experience_years` | +| REMOVE | `rejection_reason` (use approval_requests) | + +### job_applications + +| Action | Changes | +| ------ | ----------------------------------------------------- | +| RENAME | `applications` → `job_applications` | +| RENAME | `job_seeker_id` → Remove (use `applicant_user_id`) | +| ADD | `applicant_user_id` (UUID) | +| RENAME | `cover_letter` → `cover_note` | +| REMOVE | `resume_url` (use candidate_profiles.resume_file_url) | +| REMOVE | `contact_viewed` | + +### leads + +| Action | Changes | +| ------ | ------------------------------------------ | +| RENAME | `requirements` → `leads` | +| RENAME | `customer_id` → `created_by_user_id` | +| ADD | `required_date` (DATE) | +| REMOVE | `profession_key` (use leads directly) | +| REMOVE | `extra_data_json` | +| REMOVE | `rejection_reason` (use approval_requests) | +| REMOVE | `request_count`, `accepted_count` | +| REMOVE | `expires_at` | + +### lead_requests + +| Action | Changes | +| ------ | ------------------------------------------ | +| RENAME | `professional_id` → `user_role_profile_id` | +| ADD | `remarks` (TEXT) | +| REMOVE | `tracecoins_reserved` | +| REMOVE | `resolved_at` | + +--- + +## 8. Verification & Approval + +### Current: onboarding_submissions + +| Action | Migration | +| ------ | ---------------------------------------------- | +| Map to | `verification_requests` OR `approval_requests` | + +### Current: verification_requests (exists) + +| Action | Changes | +| ------ | ----------------------------- | +| UPDATE | Align fields to target schema | + +### Current: verification_logs + +| Action | Changes | +| ------ | ----------------------------- | +| UPDATE | Align fields to target schema | + +--- + +## 9. Reviews + +### reviews + +| Action | Changes | +| ------ | -------------------------------------------------- | +| ADD | `entity_type` (TEXT) | +| ADD | `entity_id` (UUID) | +| RENAME | `professional_id` → `entity_id` (with entity_type) | +| RENAME | `customer_id` → `reviewer_user_id` | +| REMOVE | `lead_request_id` (reference through entity_id) | +| ADD | `status` (TEXT) | + +--- + +## 10. Finance Domain + +### tracecoin_wallets + +| Action | Changes | +| ------ | ----------------------------- | +| RENAME | `balance` → `current_balance` | +| ADD | `updated_at` (TIMESTAMP) | + +### tracecoin_ledger + +| Action | Changes | +| ------ | --------------------------- | +| ADD | `balance_after` (INTEGER) | +| RENAME | `type` → `transaction_type` | +| RENAME | `reason` → `reference_type` | +| ADD | `remarks` (TEXT) | + +### payments + +| Action | Changes | +| ------ | ---------------------------------- | +| ADD | `payment_gateway_config_id` (UUID) | +| ADD | `payment_method` (TEXT) | +| ADD | `currency_code` (TEXT) | +| RENAME | `verified_at` → Remove | +| REMOVE | `package_id` (use order_items) | + +### invoices + +| Action | Changes | +| ------ | --------------------------- | +| ADD | `order_id` (UUID) | +| RENAME | `subtotal` → `subtotal_inr` | +| RENAME | `gst_amount` → `tax_inr` | +| RENAME | `total` → `total_inr` | +| ADD | `discount_inr` (INTEGER) | +| ADD | `due_at` (TIMESTAMP) | +| ADD | `paid_at` (TIMESTAMP) | + +### coupons + +| Action | Changes | +| ------ | ----------------------------------------- | +| ADD | `max_discount_inr` (INTEGER) | +| ADD | `min_order_value_inr` (INTEGER) | +| ADD | `valid_from` (TIMESTAMP) | +| ADD | `valid_to` (TIMESTAMP) | +| RENAME | `applies_to` → Keep | +| REMOVE | `max_uses` (use `usage_limit`) | +| REMOVE | `uses_count` (tracked in redemptions) | +| REMOVE | `per_user_limit` (tracked in redemptions) | + +### coupon_redemptions + +| Action | Changes | +| ------ | ------------------------------------ | +| RENAME | `coupon_uses` → `coupon_redemptions` | +| ADD | `order_id` (UUID) | +| RENAME | `used_at` → `redeemed_at` | +| ADD | `discount_amount_inr` (INTEGER) | + +--- + +## 11. Knowledge Base + +### kb_articles + +| Action | Changes | +| ------ | ------------------------------------------- | +| ADD | `section_id` (UUID) | +| RENAME | `body` → `content_markdown` | +| ADD | `article_type` (TEXT) | +| ADD | `audience_type` (TEXT) | +| RENAME | `is_published` → `status` | +| RENAME | `created_by` → `author_user_id` | +| REMOVE | `target_roles` | +| REMOVE | `views` (can add back as separate tracking) | + +--- + +## 12. Support + +### support_tickets + +| Action | Changes | +| ------ | ------------------------------------- | +| RENAME | `user_id` → `created_by_user_id` | +| RENAME | `assigned_to` → `assigned_to_user_id` | +| RENAME | `description` → Keep | +| ADD | `related_entity_type` (TEXT) | +| ADD | `related_entity_id` (UUID) | +| ADD | `closed_at` (TIMESTAMP) | + +### support_ticket_messages + +| Action | Changes | +| ------ | ------------------------------ | +| RENAME | `sender_id` → `sender_user_id` | +| RENAME | `body` → `message_body` | +| ADD | `attachment_url` (TEXT) | + +--- + +## 13. Notifications + +### notifications + +| Action | Changes | +| ------ | ------------------------------------ | +| ADD | `channel` (TEXT) | +| RENAME | `type` → Keep (add channel) | +| RENAME | `reference_id` → `related_entity_id` | +| ADD | `related_entity_type` (TEXT) | +| RENAME | `is_read` → `status` | + +--- + +## 14. Tables to DEPRECATE (Keep for Backward Compatibility) + +| Table | Reason | Action | +| ------------------------ | ------------------------------- | ----------------------------- | +| `onboarding_submissions` | Legacy onboarding flow | Map to verification/approval | +| `onboarding_configs` | Legacy onboarding flow | Map to dashboard_configs | +| `onboarding_states` | Legacy onboarding flow | Remove after migration | +| `submission_documents` | Legacy onboarding flow | Map to verification_documents | +| `professionals` | Replaced by user_role_profiles | Keep until migration complete | +| `user_settings` | Already exists, align structure | Align columns | + +--- + +## 15. Tables to DROP (After Migration) + +| Table | Condition | +| ------------------------ | --------------------------------------------- | +| `professionals` | After all data migrated to user_role_profiles | +| `onboarding_submissions` | After verification_requests populated | +| `onboarding_configs` | After dashboard_configs populated | +| `onboarding_states` | After migration complete | +| `submission_documents` | After verification_documents populated | + +--- + +## 16. Foreign Key Changes Summary + +### From users.id to user_role_profiles.id + +**Tables changing FK:** + +- `photographer_profiles.user_id` → `user_role_profile_id` +- `tutor_profiles.user_id` → `user_role_profile_id` +- `makeup_artist_profiles.user_id` → `user_role_profile_id` +- `developer_profiles.user_id` → `user_role_profile_id` +- `video_editor_profiles.user_id` → `user_role_profile_id` +- `graphic_designer_profiles.user_id` → `user_role_profile_id` +- `social_media_manager_profiles.user_id` → `user_role_profile_id` +- `fitness_trainer_profiles.user_id` → `user_role_profile_id` +- `catering_service_profiles.user_id` → `user_role_profile_id` +- `ugc_content_creator_profiles.user_id` → `user_role_profile_id` +- `portfolio_items.professional_id` → `user_role_profile_id` +- `services.professional_id` → `user_role_profile_id` +- `lead_requests.professional_id` → `user_role_profile_id` diff --git a/docs/schema_audit.md b/docs/schema_audit.md new file mode 100644 index 0000000..3103cf1 --- /dev/null +++ b/docs/schema_audit.md @@ -0,0 +1,216 @@ +# Schema Audit — Nxtgauge Database + +## Current State Overview + +The Nxtgauge database contains **45+ tables** spread across migrations and init-db.sql. Below is the complete audit. + +--- + +## 1. Current Table Inventory + +### Identity & Access Control + +| Table | Status | Notes | +| --------------------------- | -------- | --------------------------------------------------- | +| `users` | ✅ Good | Has email, phone, password_hash, status, timestamps | +| `refresh_tokens` | ✅ Good | Token storage for auth | +| `roles` | ✅ Good | Role definitions | +| `role_permissions` | ✅ Good | Permission assignments | +| `user_roles` | ✅ Good | User-role associations | +| `employees` | ✅ Good | Internal staff records | +| `departments` | ✅ Good | Organization structure | +| `designations` | ✅ Good | Job titles | +| `user_settings` | ✅ Added | User preferences | +| `account_deletion_requests` | ✅ Added | Deletion tracking | + +### User Profiles — DUPLICATED STRUCTURE (PROBLEM) + +| Table | Status | Issue | +| ------------------------------- | ------------- | ---------------------------------------------- | +| `photographer_profiles` | ⚠️ | Has duplicated common fields | +| `tutor_profiles` | ⚠️ | Has duplicated common fields | +| `makeup_artist_profiles` | ⚠️ | Has duplicated common fields | +| `developer_profiles` | ⚠️ | Has external links (github_url, portfolio_url) | +| `video_editor_profiles` | ⚠️ | Has external links (reel_url) | +| `graphic_designer_profiles` | ⚠️ | Has external links (portfolio_url) | +| `social_media_manager_profiles` | ⚠️ | Has duplicated common fields | +| `fitness_trainer_profiles` | ⚠️ | Has duplicated common fields | +| `catering_service_profiles` | ⚠️ | Has duplicated common fields | +| `ugc_content_creator_profiles` | ⚠️ | Has duplicated common fields | +| `company_profiles` | ⚠️ | Has duplicated fields | +| `customer_profiles` | ⚠️ | Has duplicated fields | +| `job_seeker_profiles` | ⚠️ | Has duplicated fields | +| `professionals` | ❌ Deprecated | Old root table, references user_id | + +### Portfolio Domain + +| Table | Status | Issue | +| ------------------ | ------- | ---------------------------------------------- | +| `portfolio_items` | ⚠️ | References `professionals` table, needs update | +| `portfolio_images` | ✅ Good | Simple image storage | +| `services` | ⚠️ | References `professionals` table, needs update | + +### Marketplace + +| Table | Status | Notes | +| --------------- | ------- | --------------------------------------- | +| `jobs` | ✅ Good | Job postings | +| `applications` | ⚠️ | Should be renamed to `job_applications` | +| `requirements` | ⚠️ | Should be renamed to `leads` | +| `lead_requests` | ⚠️ | References `professionals` table | +| `reviews` | ✅ Good | Reviews system | + +### Verification & Approval (Mixed with Onboarding) + +| Table | Status | Issue | +| ------------------------ | --------- | ------------------------ | +| `onboarding_submissions` | ❌ Legacy | Tied to onboarding flow | +| `submission_documents` | ❌ Legacy | Tied to onboarding | +| `onboarding_states` | ❌ Legacy | Tied to onboarding | +| `onboarding_configs` | ❌ Legacy | Tied to onboarding | +| `verifications` | ⚠️ | Basic verification table | +| `verification_logs` | ⚠️ | Basic verification logs | + +### Finance Domain + +| Table | Status | Notes | +| ------------------- | ------- | ------------------------------ | +| `tracecoin_wallets` | ✅ Good | Wallet per user | +| `tracecoin_ledger` | ✅ Good | Immutable ledger | +| `pricing_packages` | ✅ Good | Package definitions | +| `payments` | ✅ Good | Payment records | +| `invoices` | ✅ Good | Invoice records | +| `coupons` | ✅ Good | Coupon system | +| `coupon_uses` | ⚠️ | Should be `coupon_redemptions` | +| `discounts` | ✅ Good | Discount rules | + +### Communication & Support + +| Table | Status | Notes | +| ------------------------- | ------- | -------------------- | +| `notifications` | ✅ Good | In-app notifications | +| `email_logs` | ✅ Good | Email audit trail | +| `support_tickets` | ✅ Good | Support system | +| `support_ticket_messages` | ✅ Good | Ticket messages | + +### Knowledge Base + +| Table | Status | Notes | +| --------------- | ------- | ------------------------------------------- | +| `kb_categories` | ✅ Good | KB categories | +| `kb_articles` | ⚠️ | Missing `section_id`, `kb_article_feedback` | + +### Dashboard & Config + +| Table | Status | Notes | +| ------------------- | ------- | ------------------------ | +| `dashboard_configs` | ✅ Good | Dashboard configurations | +| `runtime_configs` | ✅ Good | Runtime feature flags | + +### Audit & Logging + +| Table | Status | Issue | +| ------------------- | ---------- | ------------------------- | +| `activity_logs` | ⚠️ | Basic logging, incomplete | +| `audit_logs` | ❌ Missing | Not implemented | +| `audit_log_changes` | ❌ Missing | Not implemented | + +--- + +## 2. Problems Identified + +### Problem 1: Duplicated Profile Fields + +Every profile table has these duplicated fields: + +- `display_name`, `bio`, `location`, `status`, `rejection_reason`, `approved_at` + +**Impact**: Data redundancy, inconsistent updates, maintenance burden. + +### Problem 2: Extension Tables Reference `user_id` Instead of Root Profile + +Current: `photographer_profiles.user_id` → `users.id` +Target: `photographer_profiles.user_role_profile_id` → `user_role_profiles.id` + +**Impact**: Cannot support multiple role profiles per user properly. + +### Problem 3: External Portfolio Links + +Tables with forbidden external links: + +- `developer_profiles`: `github_url`, `portfolio_url` +- `video_editor_profiles`: `reel_url` +- `graphic_designer_profiles`: `portfolio_url` + +**Impact**: Violates platform-native portfolio requirement. + +### Problem 4: Table Names Not Aligned with Target + +| Current | Target | +| -------------- | -------------------- | +| `applications` | `job_applications` | +| `requirements` | `leads` | +| `coupon_uses` | `coupon_redemptions` | + +### Problem 5: Missing Root Profile Table + +`user_role_profiles` does not exist. Users cannot have multiple role profiles properly. + +### Problem 6: Missing Audit Infrastructure + +- No `audit_logs` table +- No `audit_log_changes` table +- Current `activity_logs` is incomplete + +### Problem 7: Verification/Approval Tied to Onboarding + +- `onboarding_submissions`, `onboarding_configs`, `onboarding_states` are legacy +- Need separate `verification_requests`, `approval_requests` + +### Problem 8: Incomplete Knowledge Base + +- Missing `kb_sections` +- Missing `kb_article_feedback` +- Missing `kb_article_related` + +### Problem 9: Missing Finance Tables + +- No `orders` / `order_items` +- No `tax_rules` +- No `payment_transactions` +- No `payment_gateway_configs` + +--- + +## 3. Page-Based Anti-Patterns + +The schema was influenced by admin pages rather than domain entities: + +| Page | Problem Table(s) | +| --------------------- | ------------------------------------------------------------------- | +| Onboarding Management | `onboarding_submissions`, `onboarding_configs`, `onboarding_states` | +| Account Settings | Should be in `user_settings` | +| Reviews | Mix of UI and domain | + +--- + +## 4. Inconsistent Naming + +| Issue | Examples | +| ----------------------- | ----------------------------------------------------------- | +| Mixed naming | `job_type` vs `employment_type` | +| Inconsistent timestamps | Some tables have `created_at`, some don't have `updated_at` | +| UUID vs text | Some IDs are UUID, some referenced tables use text | + +--- + +## 5. Recommendations + +1. **Create `user_role_profiles`** as the root profile table +2. **Update extension tables** to reference `user_role_profile_id` +3. **Remove external links** from profile tables +4. **Rename tables** to match target schema +5. **Create audit tables** for compliance +6. **Separate verification from approval** domains +7. **Add missing finance tables** for proper order management +8. **Complete KB structure** with sections and feedback diff --git a/docs/target_schema.md b/docs/target_schema.md new file mode 100644 index 0000000..5607343 --- /dev/null +++ b/docs/target_schema.md @@ -0,0 +1,1052 @@ +# Target Schema — Nxtgauge Database + +This document defines the **target state** for the Nxtgauge PostgreSQL schema based on the single source of truth. + +--- + +## 1. Identity & Access Control + +### users + +| Column | Type | Notes | +| ------------- | --------- | -------------------------- | +| id | UUID | Primary key | +| email | TEXT | Unique | +| phone | TEXT | Unique, nullable | +| password_hash | TEXT | | +| account_type | TEXT | INDIVIDUAL, COMPANY | +| status | TEXT | ACTIVE, PENDING, SUSPENDED | +| last_login_at | TIMESTAMP | | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +### refresh_tokens + +| Column | Type | Notes | +| ---------- | --------- | ----------- | +| id | UUID | Primary key | +| user_id | UUID | FK → users | +| token_hash | TEXT | Unique | +| expires_at | TIMESTAMP | | +| revoked | BOOLEAN | | +| created_at | TIMESTAMP | | + +### user_sessions + +| Column | Type | Notes | +| ------------- | --------- | ----------- | +| id | UUID | Primary key | +| user_id | UUID | FK → users | +| session_token | TEXT | Unique | +| ip_address | TEXT | | +| user_agent | TEXT | | +| expires_at | TIMESTAMP | | +| created_at | TIMESTAMP | | + +### user_settings + +| Column | Type | Notes | +| ------------- | --------- | ------------------ | +| id | UUID | Primary key | +| user_id | UUID | FK → users, Unique | +| settings_json | JSONB | | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +### roles + +| Column | Type | Notes | +| -------------------------- | --------- | ------------------ | +| id | UUID | Primary key | +| key | TEXT | Unique | +| name | TEXT | | +| audience | TEXT | INTERNAL, EXTERNAL | +| description | TEXT | | +| department_id | UUID | FK → departments | +| can_approve_requests | BOOLEAN | | +| can_manage_system_settings | BOOLEAN | | +| is_active | BOOLEAN | | +| created_at | TIMESTAMP | | + +### permissions + +| Column | Type | Notes | +| ----------- | --------- | ----------- | +| id | UUID | Primary key | +| key | TEXT | Unique | +| name | TEXT | | +| description | TEXT | | +| created_at | TIMESTAMP | | + +### role_permissions + +| Column | Type | Notes | +| -------------- | --------- | ---------------- | +| id | UUID | Primary key | +| role_id | UUID | FK → roles | +| permission_key | TEXT | FK → permissions | +| created_at | TIMESTAMP | | + +### user_roles + +| Column | Type | Notes | +| ----------- | --------- | --------------------------- | +| id | UUID | Primary key | +| user_id | UUID | FK → users | +| role_id | UUID | FK → roles | +| status | TEXT | PENDING, APPROVED, REJECTED | +| approved_at | TIMESTAMP | | +| created_at | TIMESTAMP | | + +### employees + +| Column | Type | Notes | +| ------------------- | --------- | ---------------------------- | +| id | UUID | Primary key | +| user_id | UUID | FK → users, Unique | +| employee_code | TEXT | | +| department_id | UUID | FK → departments | +| designation_id | UUID | FK → designations | +| joining_date | DATE | | +| employment_status | TEXT | ACTIVE, INACTIVE, TERMINATED | +| manager_employee_id | UUID | Self-reference | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +### departments + +| Column | Type | Notes | +| ----------------- | --------- | ----------- | +| id | UUID | Primary key | +| name | TEXT | Unique | +| code | TEXT | Unique | +| description | TEXT | | +| department_head | TEXT | | +| department_email | TEXT | | +| is_active | BOOLEAN | | +| visibility | TEXT | INTERNAL | +| transfers_enabled | BOOLEAN | | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +### designations + +| Column | Type | Notes | +| --------------- | --------- | ---------------- | +| id | UUID | Primary key | +| name | TEXT | Unique | +| code | TEXT | Unique | +| department_id | UUID | FK → departments | +| description | TEXT | | +| level | TEXT | | +| can_manage_team | BOOLEAN | | +| can_approve | BOOLEAN | | +| is_active | BOOLEAN | | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +--- + +## 2. User Role Profiles (NEW ROOT) + +### user_role_profiles + +| Column | Type | Notes | +| ------------------- | --------- | ------------------------------------ | +| id | UUID | Primary key | +| user_id | UUID | FK → users | +| role_key | TEXT | photographer, tutor, developer, etc. | +| display_name | TEXT | | +| bio | TEXT | | +| location | TEXT | | +| avatar_url | TEXT | | +| phone | TEXT | | +| email | TEXT | | +| status | TEXT | DRAFT, ACTIVE, SUSPENDED | +| verification_status | TEXT | PENDING, VERIFIED, REJECTED | +| approval_status | TEXT | PENDING, APPROVED, REJECTED | +| rejection_reason | TEXT | | +| approved_at | TIMESTAMP | | +| verified_at | TIMESTAMP | | +| is_profile_public | BOOLEAN | | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +**Indexes:** + +- `UNIQUE(user_id, role_key)` +- `INDEX(status)` +- `INDEX(verification_status)` +- `INDEX(approval_status)` + +--- + +## 3. Role Extension Tables + +### photographer_profiles + +| Column | Type | Notes | +| -------------------- | --------- | ------------------------------- | +| id | UUID | Primary key | +| user_role_profile_id | UUID | FK → user_role_profiles, Unique | +| specialties | TEXT[] | | +| camera_brands | TEXT[] | | +| studio_available | BOOLEAN | | +| outdoor_shoots | BOOLEAN | | +| travel_radius_km | INTEGER | | +| starting_price_inr | INTEGER | in paise | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +### tutor_profiles + +| Column | Type | Notes | +| -------------------- | --------- | ------------------------------- | +| id | UUID | Primary key | +| user_role_profile_id | UUID | FK → user_role_profiles, Unique | +| subjects | TEXT[] | | +| board_types | TEXT[] | | +| qualification | TEXT | | +| teaches_online | BOOLEAN | | +| teaches_offline | BOOLEAN | | +| experience_years | INTEGER | | +| hourly_rate_inr | INTEGER | in paise | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +### makeup_artist_profiles + +| Column | Type | Notes | +| -------------------- | --------- | ------------------------------- | +| id | UUID | Primary key | +| user_role_profile_id | UUID | FK → user_role_profiles, Unique | +| specializations | TEXT[] | | +| kit_brands | TEXT[] | | +| home_service | BOOLEAN | | +| studio_available | BOOLEAN | | +| starting_price_inr | INTEGER | in paise | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +### developer_profiles + +| Column | Type | Notes | +| -------------------- | --------- | ------------------------------- | +| id | UUID | Primary key | +| user_role_profile_id | UUID | FK → user_role_profiles, Unique | +| tech_stack | TEXT[] | | +| experience_years | INTEGER | | +| availability | TEXT | FULL_TIME, PART_TIME, FREELANCE | +| hourly_rate_inr | INTEGER | in paise | +| remote_ok | BOOLEAN | | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +**NO external links (github_url, portfolio_url removed)** + +### video_editor_profiles + +| Column | Type | Notes | +| -------------------- | --------- | ------------------------------- | +| id | UUID | Primary key | +| user_role_profile_id | UUID | FK → user_role_profiles, Unique | +| software_skills | TEXT[] | | +| style_tags | TEXT[] | | +| turnaround_days | INTEGER | | +| starting_price_inr | INTEGER | in paise | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +**NO reel_url** + +### graphic_designer_profiles + +| Column | Type | Notes | +| -------------------- | --------- | ------------------------------- | +| id | UUID | Primary key | +| user_role_profile_id | UUID | FK → user_role_profiles, Unique | +| design_tools | TEXT[] | | +| style_tags | TEXT[] | | +| brand_experience | BOOLEAN | | +| starting_price_inr | INTEGER | in paise | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +**NO portfolio_url** + +### social_media_manager_profiles + +| Column | Type | Notes | +| ----------------------- | --------- | ------------------------------- | +| id | UUID | Primary key | +| user_role_profile_id | UUID | FK → user_role_profiles, Unique | +| platforms | TEXT[] | | +| industries | TEXT[] | | +| content_types | TEXT[] | | +| avg_follower_growth_pct | INTEGER | | +| starting_price_inr | INTEGER | in paise | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +### fitness_trainer_profiles + +| Column | Type | Notes | +| -------------------- | --------- | ------------------------------- | +| id | UUID | Primary key | +| user_role_profile_id | UUID | FK → user_role_profiles, Unique | +| disciplines | TEXT[] | | +| certifications | TEXT[] | | +| online_sessions | BOOLEAN | | +| home_visits | BOOLEAN | | +| gym_based | BOOLEAN | | +| per_session_rate_inr | INTEGER | in paise | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +### catering_service_profiles + +| Column | Type | Notes | +| -------------------- | --------- | ------------------------------- | +| id | UUID | Primary key | +| user_role_profile_id | UUID | FK → user_role_profiles, Unique | +| business_name | TEXT | | +| cuisine_types | TEXT[] | | +| event_types | TEXT[] | | +| min_guests | INTEGER | | +| max_guests | INTEGER | | +| has_setup_team | BOOLEAN | | +| has_serving_staff | BOOLEAN | | +| price_per_head_inr | INTEGER | in paise | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +### ugc_content_creator_profiles + +| Column | Type | Notes | +| -------------------- | --------- | ------------------------------- | +| id | UUID | Primary key | +| user_role_profile_id | UUID | FK → user_role_profiles, Unique | +| niche_tags | TEXT[] | | +| content_formats | TEXT[] | | +| platforms | TEXT[] | | +| turnaround_days | INTEGER | | +| starting_price_inr | INTEGER | in paise | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +--- + +## 4. Other Profiles + +### company_profiles + +| Column | Type | Notes | +| ------------------- | --------- | ------------------ | +| id | UUID | Primary key | +| user_id | UUID | FK → users, Unique | +| company_name | TEXT | | +| business_type | TEXT | | +| industry | TEXT | | +| contact_person_name | TEXT | | +| email | TEXT | | +| phone | TEXT | | +| location | TEXT | | +| bio | TEXT | | +| status | TEXT | | +| verification_status | TEXT | | +| approval_status | TEXT | | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +### customer_profiles + +| Column | Type | Notes | +| ------------ | --------- | ------------------ | +| id | UUID | Primary key | +| user_id | UUID | FK → users, Unique | +| display_name | TEXT | | +| phone | TEXT | | +| email | TEXT | | +| location | TEXT | | +| status | TEXT | | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +### candidate_profiles + +| Column | Type | Notes | +| ------------------- | --------- | ------------------ | +| id | UUID | Primary key | +| user_id | UUID | FK → users, Unique | +| display_name | TEXT | | +| bio | TEXT | | +| location | TEXT | | +| experience_years | INTEGER | | +| preferred_roles | TEXT[] | | +| expected_salary_inr | INTEGER | | +| resume_file_url | TEXT | | +| status | TEXT | | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +--- + +## 5. Portfolio Domain + +### portfolio_items + +| Column | Type | Notes | +| -------------------- | --------- | ----------------------- | +| id | UUID | Primary key | +| user_role_profile_id | UUID | FK → user_role_profiles | +| title | TEXT | | +| description | TEXT | | +| tags | TEXT[] | | +| display_order | INTEGER | | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +**NO external links** + +### portfolio_images + +| Column | Type | Notes | +| ----------------- | --------- | -------------------- | +| id | UUID | Primary key | +| portfolio_item_id | UUID | FK → portfolio_items | +| file_url | TEXT | | +| display_order | INTEGER | | +| created_at | TIMESTAMP | | + +### services + +| Column | Type | Notes | +| -------------------- | --------- | ----------------------- | +| id | UUID | Primary key | +| user_role_profile_id | UUID | FK → user_role_profiles | +| name | TEXT | | +| description | TEXT | | +| price | INTEGER | in paise | +| duration_minutes | INTEGER | | +| is_active | BOOLEAN | | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +--- + +## 6. Verification Domain + +### verification_requests + +| Column | Type | Notes | +| -------------------- | --------- | ---------------------------- | +| id | UUID | Primary key | +| user_role_profile_id | UUID | FK → user_role_profiles | +| verification_type | TEXT | IDENTITY, BUSINESS, DOCUMENT | +| status | TEXT | PENDING, APPROVED, REJECTED | +| submitted_at | TIMESTAMP | | +| reviewed_at | TIMESTAMP | | +| reviewed_by_user_id | UUID | FK → users | +| remarks | TEXT | | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +### verification_documents + +| Column | Type | Notes | +| ----------------------- | --------- | --------------------------- | +| id | UUID | Primary key | +| verification_request_id | UUID | FK → verification_requests | +| document_type | TEXT | | +| file_url | TEXT | | +| file_name | TEXT | | +| mime_type | TEXT | | +| status | TEXT | PENDING, APPROVED, REJECTED | +| uploaded_at | TIMESTAMP | | +| reviewed_at | TIMESTAMP | | +| reviewed_by_user_id | UUID | FK → users | +| remarks | TEXT | | +| created_at | TIMESTAMP | | + +### verification_logs + +| Column | Type | Notes | +| ----------------------- | --------- | ------------------------------------------------- | +| id | UUID | Primary key | +| verification_request_id | UUID | FK → verification_requests | +| action | TEXT | SUBMITTED, APPROVED, REJECTED, DOCUMENT_REQUESTED | +| old_status | TEXT | | +| new_status | TEXT | | +| acted_by_user_id | UUID | FK → users | +| remarks | TEXT | | +| created_at | TIMESTAMP | | + +--- + +## 7. Approval Domain + +### approval_requests + +| Column | Type | Notes | +| -------------------- | --------- | ------------------------------ | +| id | UUID | Primary key | +| entity_type | TEXT | job, lead, profile, company | +| entity_id | UUID | | +| approval_type | TEXT | CONTENT, VERIFICATION, FEATURE | +| status | TEXT | PENDING, APPROVED, REJECTED | +| submitted_by_user_id | UUID | FK → users | +| reviewed_by_user_id | UUID | FK → users | +| submitted_at | TIMESTAMP | | +| reviewed_at | TIMESTAMP | | +| remarks | TEXT | | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +### approval_logs + +| Column | Type | Notes | +| ------------------- | --------- | ----------------------------- | +| id | UUID | Primary key | +| approval_request_id | UUID | FK → approval_requests | +| action | TEXT | SUBMITTED, APPROVED, REJECTED | +| old_status | TEXT | | +| new_status | TEXT | | +| acted_by_user_id | UUID | FK → users | +| remarks | TEXT | | +| created_at | TIMESTAMP | | + +--- + +## 8. Marketplace Domain + +### jobs + +| Column | Type | Notes | +| ------------------ | --------- | ------------------------------ | +| id | UUID | Primary key | +| company_profile_id | UUID | FK → company_profiles | +| posted_by_user_id | UUID | FK → users | +| title | TEXT | | +| description | TEXT | | +| location | TEXT | | +| employment_type | TEXT | FULL_TIME, PART_TIME, CONTRACT | +| mode_of_work | TEXT | ONSITE, REMOTE, HYBRID | +| budget_inr | INTEGER | | +| salary_range_json | JSONB | | +| status | TEXT | DRAFT, PENDING, LIVE, CLOSED | +| approved_at | TIMESTAMP | | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +### job_applications (RENAMED from applications) + +| Column | Type | Notes | +| ----------------- | --------- | -------------------------------------------------- | +| id | UUID | Primary key | +| job_id | UUID | FK → jobs | +| applicant_user_id | UUID | FK → users | +| status | TEXT | APPLIED, SHORTLISTED, INTERVIEW, OFFERED, REJECTED | +| cover_note | TEXT | | +| applied_at | TIMESTAMP | | +| created_at | TIMESTAMP | | + +### leads (RENAMED from requirements) + +| Column | Type | Notes | +| ------------------ | --------- | ---------------------------- | +| id | UUID | Primary key | +| created_by_user_id | UUID | FK → users | +| profession_key | TEXT | | +| title | TEXT | | +| description | TEXT | | +| location | TEXT | | +| budget_inr | INTEGER | | +| required_date | DATE | | +| status | TEXT | DRAFT, PENDING, OPEN, CLOSED | +| approved_at | TIMESTAMP | | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +### lead_requests + +| Column | Type | Notes | +| -------------------- | --------- | --------------------------- | +| id | UUID | Primary key | +| lead_id | UUID | FK → leads | +| user_role_profile_id | UUID | FK → user_role_profiles | +| status | TEXT | PENDING, ACCEPTED, REJECTED | +| remarks | TEXT | | +| requested_at | TIMESTAMP | | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +### reviews + +| Column | Type | Notes | +| ---------------- | --------- | -------------------------- | +| id | UUID | Primary key | +| reviewer_user_id | UUID | FK → users | +| entity_type | TEXT | professional, company | +| entity_id | UUID | | +| rating | SMALLINT | 1-5 | +| review_text | TEXT | | +| status | TEXT | PENDING, PUBLISHED, HIDDEN | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +--- + +## 9. Finance Domain + +### pricing_packages + +| Column | Type | Notes | +| ------------------- | --------- | -------------------------------------------- | +| id | UUID | Primary key | +| name | TEXT | | +| description | TEXT | | +| package_type | TEXT | JOB_POSTING, CONTACT_VIEWS, TRACECOIN_BUNDLE | +| price_inr | INTEGER | in paise | +| tracecoins_included | INTEGER | | +| validity_days | INTEGER | | +| is_active | BOOLEAN | | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +### tracecoin_wallets + +| Column | Type | Notes | +| --------------- | --------- | ------------------ | +| id | UUID | Primary key | +| user_id | UUID | FK → users, Unique | +| current_balance | INTEGER | | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +### tracecoin_ledger (IMMUTABLE) + +| Column | Type | Notes | +| ---------------- | --------- | -------------------------- | +| id | UUID | Primary key | +| wallet_id | UUID | FK → tracecoin_wallets | +| transaction_type | TEXT | CREDIT, DEBIT | +| amount | INTEGER | | +| balance_after | INTEGER | | +| reference_type | TEXT | JOB, LEAD, PURCHASE, BONUS | +| reference_id | UUID | | +| remarks | TEXT | | +| created_at | TIMESTAMP | | + +### orders + +| Column | Type | Notes | +| ------------ | --------- | ----------------------------- | +| id | UUID | Primary key | +| user_id | UUID | FK → users | +| order_type | TEXT | PACKAGE, SERVICE | +| subtotal_inr | INTEGER | | +| discount_inr | INTEGER | | +| tax_inr | INTEGER | | +| total_inr | INTEGER | | +| status | TEXT | PENDING, COMPLETED, CANCELLED | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +### order_items + +| Column | Type | Notes | +| --------------- | --------- | ---------------- | +| id | UUID | Primary key | +| order_id | UUID | FK → orders | +| item_type | TEXT | PACKAGE, SERVICE | +| item_id | UUID | | +| item_name | TEXT | | +| quantity | INTEGER | | +| unit_price_inr | INTEGER | | +| total_price_inr | INTEGER | | +| created_at | TIMESTAMP | | + +### invoices + +| Column | Type | Notes | +| -------------- | --------- | ------------ | +| id | UUID | Primary key | +| order_id | UUID | FK → orders | +| user_id | UUID | FK → users | +| invoice_number | TEXT | Unique | +| subtotal_inr | INTEGER | | +| discount_inr | INTEGER | | +| tax_inr | INTEGER | | +| total_inr | INTEGER | | +| status | TEXT | ISSUED, PAID | +| issued_at | TIMESTAMP | | +| due_at | TIMESTAMP | | +| paid_at | TIMESTAMP | | +| created_at | TIMESTAMP | | + +### payments + +| Column | Type | Notes | +| ------------------------- | --------- | ---------------------------- | +| id | UUID | Primary key | +| order_id | UUID | FK → orders | +| invoice_id | UUID | FK → invoices | +| user_id | UUID | FK → users | +| payment_gateway_config_id | UUID | FK → payment_gateway_configs | +| payment_method | TEXT | | +| provider_payment_ref | TEXT | | +| amount_inr | INTEGER | | +| currency_code | TEXT | INR | +| status | TEXT | PENDING, SUCCESS, FAILED | +| initiated_at | TIMESTAMP | | +| completed_at | TIMESTAMP | | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +### payment_gateway_configs + +| Column | Type | Notes | +| ------------ | --------- | ---------------- | +| id | UUID | Primary key | +| gateway_key | TEXT | RAZORPAY, STRIPE | +| display_name | TEXT | | +| config_json | JSONB | | +| is_active | BOOLEAN | | +| created_at | TIMESTAMP | | + +### payment_transactions + +| Column | Type | Notes | +| --------------------- | --------- | ---------------------------- | +| id | UUID | Primary key | +| payment_id | UUID | FK → payments | +| transaction_type | TEXT | INITIATED, COMPLETED, FAILED | +| provider_reference | TEXT | | +| request_payload_json | JSONB | | +| response_payload_json | JSONB | | +| status | TEXT | | +| created_at | TIMESTAMP | | + +### coupons + +| Column | Type | Notes | +| ------------------- | --------- | ---------------- | +| id | UUID | Primary key | +| code | TEXT | Unique | +| description | TEXT | | +| discount_type | TEXT | PERCENT, FLAT | +| discount_value | INTEGER | | +| max_discount_inr | INTEGER | | +| min_order_value_inr | INTEGER | | +| valid_from | TIMESTAMP | | +| valid_to | TIMESTAMP | | +| usage_limit | INTEGER | NULL = unlimited | +| is_active | BOOLEAN | | +| created_at | TIMESTAMP | | + +### coupon_redemptions (RENAMED from coupon_uses) + +| Column | Type | Notes | +| ------------------- | --------- | ------------ | +| id | UUID | Primary key | +| coupon_id | UUID | FK → coupons | +| user_id | UUID | FK → users | +| order_id | UUID | FK → orders | +| redeemed_at | TIMESTAMP | | +| discount_amount_inr | INTEGER | | +| created_at | TIMESTAMP | | + +### discount_rules + +| Column | Type | Notes | +| -------------- | --------- | ------------------------- | +| id | UUID | Primary key | +| name | TEXT | | +| scope_type | TEXT | GLOBAL, CATEGORY, SERVICE | +| scope_id | UUID | | +| discount_type | TEXT | PERCENT, FLAT | +| discount_value | INTEGER | | +| starts_at | TIMESTAMP | | +| ends_at | TIMESTAMP | | +| is_active | BOOLEAN | | +| created_at | TIMESTAMP | | + +### tax_rules + +| Column | Type | Notes | +| ---------- | --------- | ----------- | +| id | UUID | Primary key | +| name | TEXT | | +| tax_type | TEXT | GST, TCS | +| tax_rate | DECIMAL | | +| applies_to | TEXT | | +| is_active | BOOLEAN | | +| created_at | TIMESTAMP | | + +--- + +## 10. Knowledge Base + +### kb_categories + +| Column | Type | Notes | +| ------------- | --------- | ----------- | +| id | UUID | Primary key | +| name | TEXT | | +| slug | TEXT | Unique | +| description | TEXT | | +| display_order | INTEGER | | +| is_active | BOOLEAN | | +| created_at | TIMESTAMP | | + +### kb_sections + +| Column | Type | Notes | +| ------------- | --------- | ------------------ | +| id | UUID | Primary key | +| category_id | UUID | FK → kb_categories | +| name | TEXT | | +| slug | TEXT | | +| description | TEXT | | +| display_order | INTEGER | | +| is_active | BOOLEAN | | +| created_at | TIMESTAMP | | + +### kb_articles + +| Column | Type | Notes | +| ------------------- | --------- | --------------------------------------------- | +| id | UUID | Primary key | +| category_id | UUID | FK → kb_categories | +| section_id | UUID | FK → kb_sections | +| title | TEXT | | +| slug | TEXT | Unique | +| summary | TEXT | | +| content_markdown | TEXT | | +| article_type | TEXT | HOW_TO, TROUBLESHOOTING, FAQ, FEATURE, POLICY | +| status | TEXT | DRAFT, PUBLISHED | +| audience_type | TEXT | INTERNAL, EXTERNAL, ALL | +| tags | TEXT[] | | +| author_user_id | UUID | FK → users | +| reviewed_by_user_id | UUID | FK → users | +| published_at | TIMESTAMP | | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +### kb_article_feedback + +| Column | Type | Notes | +| ------------- | --------- | ---------------- | +| id | UUID | Primary key | +| article_id | UUID | FK → kb_articles | +| user_id | UUID | FK → users | +| is_helpful | BOOLEAN | | +| feedback_text | TEXT | | +| created_at | TIMESTAMP | | + +--- + +## 11. Support System + +### support_tickets + +| Column | Type | Notes | +| ------------------- | --------- | ------------------------------------ | +| id | UUID | Primary key | +| created_by_user_id | UUID | FK → users | +| assigned_to_user_id | UUID | FK → users | +| category | TEXT | GENERAL, BILLING, TECHNICAL, ACCOUNT | +| priority | TEXT | LOW, NORMAL, HIGH, URGENT | +| status | TEXT | OPEN, IN_PROGRESS, RESOLVED, CLOSED | +| subject | TEXT | | +| description | TEXT | | +| related_entity_type | TEXT | | +| related_entity_id | UUID | | +| closed_at | TIMESTAMP | | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +### support_ticket_messages + +| Column | Type | Notes | +| ----------------- | --------- | -------------------- | +| id | UUID | Primary key | +| support_ticket_id | UUID | FK → support_tickets | +| sender_user_id | UUID | FK → users | +| message_body | TEXT | | +| attachment_url | TEXT | | +| is_internal | BOOLEAN | | +| created_at | TIMESTAMP | | + +--- + +## 12. Notifications & Communication + +### notification_templates + +| Column | Type | Notes | +| -------------- | --------- | ---------------- | +| id | UUID | Primary key | +| template_key | TEXT | Unique | +| channel | TEXT | EMAIL, SMS, PUSH | +| title_template | TEXT | | +| body_template | TEXT | | +| is_active | BOOLEAN | | +| created_at | TIMESTAMP | | + +### notifications + +| Column | Type | Notes | +| ------------------- | --------- | ------------------------ | +| id | UUID | Primary key | +| user_id | UUID | FK → users | +| channel | TEXT | EMAIL, SMS, PUSH, IN_APP | +| title | TEXT | | +| body | TEXT | | +| status | TEXT | PENDING, SENT, READ | +| related_entity_type | TEXT | | +| related_entity_id | UUID | | +| sent_at | TIMESTAMP | | +| read_at | TIMESTAMP | | +| created_at | TIMESTAMP | | + +### email_logs + +| Column | Type | Notes | +| ------------------------ | --------- | --------------------------- | +| id | UUID | Primary key | +| user_id | UUID | FK → users | +| notification_template_id | UUID | FK → notification_templates | +| to_email | TEXT | | +| subject | TEXT | | +| body_snapshot | TEXT | | +| status | TEXT | PENDING, SENT, FAILED | +| provider_reference | TEXT | | +| error_message | TEXT | | +| sent_at | TIMESTAMP | | +| created_at | TIMESTAMP | | + +### smtp_configs + +| Column | Type | Notes | +| --------------- | --------- | ----------- | +| id | UUID | Primary key | +| provider_name | TEXT | | +| host | TEXT | | +| port | INTEGER | | +| username | TEXT | | +| encryption_mode | TEXT | SSL, TLS | +| from_name | TEXT | | +| from_email | TEXT | | +| is_default | BOOLEAN | | +| is_active | BOOLEAN | | +| created_at | TIMESTAMP | | + +--- + +## 13. Dashboard & Config + +### dashboard_configs + +| Column | Type | Notes | +| -------------- | --------- | ------------------ | +| id | UUID | Primary key | +| dashboard_type | TEXT | INTERNAL, EXTERNAL | +| owner_type | TEXT | ROLE, USER | +| owner_id | UUID | | +| name | TEXT | | +| layout_json | JSONB | | +| is_default | BOOLEAN | | +| is_active | BOOLEAN | | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +### dashboard_widgets + +| Column | Type | Notes | +| ------------------- | --------- | ---------------------- | +| id | UUID | Primary key | +| dashboard_config_id | UUID | FK → dashboard_configs | +| widget_key | TEXT | | +| widget_title | TEXT | | +| config_json | JSONB | | +| display_order | INTEGER | | +| width_units | INTEGER | | +| height_units | INTEGER | | +| is_visible | BOOLEAN | | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +### runtime_configs + +| Column | Type | Notes | +| ----------------- | --------- | ----------- | +| id | UUID | Primary key | +| config_group | TEXT | | +| config_key | TEXT | | +| config_value_json | JSONB | | +| is_active | BOOLEAN | | +| description | TEXT | | +| created_at | TIMESTAMP | | +| updated_at | TIMESTAMP | | + +--- + +## 14. Audit Management + +### audit_logs + +| Column | Type | Notes | +| ----------------- | --------- | ----------------------------------------- | +| id | UUID | Primary key | +| actor_user_id | UUID | FK → users | +| actor_employee_id | UUID | FK → employees | +| actor_type | TEXT | USER, EMPLOYEE, SYSTEM, CRON | +| action | TEXT | CREATE, UPDATE, DELETE, APPROVE, REJECT | +| entity_type | TEXT | | +| entity_id | UUID | | +| entity_label | TEXT | | +| module_key | TEXT | users, verification, jobs, leads, finance | +| source_type | TEXT | UI, API, WORKER, CRON | +| source_id | UUID | | +| request_id | UUID | | +| correlation_id | UUID | | +| ip_address | TEXT | | +| user_agent | TEXT | | +| status | TEXT | SUCCESS, FAILED | +| summary | TEXT | | +| metadata_json | JSONB | | +| created_at | TIMESTAMP | | + +### audit_log_changes + +| Column | Type | Notes | +| -------------- | --------- | --------------- | +| id | UUID | Primary key | +| audit_log_id | UUID | FK → audit_logs | +| field_name | TEXT | | +| old_value_text | TEXT | | +| new_value_text | TEXT | | +| created_at | TIMESTAMP | | + +--- + +## 15. Summary of Table Counts + +| Domain | Tables | Status | +| ------------------ | ------ | --------------------------------------- | +| Identity & Access | 10 | Existing + user_sessions, user_settings | +| User Role Profiles | 1 | NEW | +| Role Extensions | 10 | Updated FK | +| Other Profiles | 3 | company, customer, candidate | +| Portfolio | 3 | Updated FK | +| Verification | 3 | NEW structure | +| Approval | 2 | NEW structure | +| Marketplace | 5 | Renamed tables | +| Finance | 12 | Expanded | +| Knowledge Base | 4 | Added sections, feedback | +| Support | 2 | Enhanced | +| Notifications | 4 | Added templates, smtp | +| Dashboard | 3 | Added widgets | +| Audit | 2 | NEW | +| **TOTAL** | **64** | |