feat: Add database redesign documentation and Phase 1-2 migrations
- Add schema_audit.md documenting current schema issues - Add target_schema.md with complete target schema design - Add old_to_new_mapping.md with table mapping - Add migration_plan.md with phased migration strategy - Add Phase 1 migrations (core infrastructure): - user_sessions table - users missing columns - departments updates - designations updates - employees updates - Add Phase 2 migrations (profile domain - CRITICAL): - create user_role_profiles root table - backfill user_role_profiles from existing profiles - add user_role_profile_id to extension tables - remove forbidden external portfolio links - Add user_role_profile Rust model - Update photographer model to use user_role_profile_id
This commit is contained in:
parent
79fbba8107
commit
03376b9567
25 changed files with 3367 additions and 33 deletions
|
|
@ -0,0 +1,2 @@
|
||||||
|
-- Rollback: Drop user_sessions table
|
||||||
|
DROP TABLE IF EXISTS user_sessions;
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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;
|
||||||
12
crates/db/migrations/20260415000005_update_employees.up.sql
Normal file
12
crates/db/migrations/20260415000005_update_employees.up.sql
Normal file
|
|
@ -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);
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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';
|
||||||
|
|
@ -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'
|
||||||
|
);
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
-- Rollback: Cannot easily restore removed columns
|
||||||
|
-- This migration is NOT easily reversible
|
||||||
|
-- Only run after full backup and testing
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -25,4 +25,5 @@ pub mod employee;
|
||||||
pub mod department;
|
pub mod department;
|
||||||
pub mod designation;
|
pub mod designation;
|
||||||
pub mod verification;
|
pub mod verification;
|
||||||
|
pub mod user_role_profile;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6,58 +6,67 @@ use uuid::Uuid;
|
||||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||||
pub struct PhotographerProfile {
|
pub struct PhotographerProfile {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub user_id: Uuid,
|
pub user_role_profile_id: Uuid,
|
||||||
pub display_name: Option<String>,
|
pub specialties: Vec<String>,
|
||||||
pub bio: Option<String>,
|
pub camera_brands: Vec<String>,
|
||||||
pub location: Option<String>,
|
pub studio_available: bool,
|
||||||
pub custom_data: Option<serde_json::Value>,
|
pub outdoor_shoots: bool,
|
||||||
pub status: String,
|
pub travel_radius_km: Option<i32>,
|
||||||
|
pub starting_price_inr: Option<i32>,
|
||||||
pub created_at: DateTime<Utc>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct UpsertPhotographerProfilePayload {
|
pub struct UpsertPhotographerProfilePayload {
|
||||||
pub display_name: Option<String>,
|
pub specialties: Vec<String>,
|
||||||
pub bio: Option<String>,
|
pub camera_brands: Vec<String>,
|
||||||
pub location: Option<String>,
|
pub studio_available: bool,
|
||||||
pub custom_data: Option<serde_json::Value>,
|
pub outdoor_shoots: bool,
|
||||||
|
pub travel_radius_km: Option<i32>,
|
||||||
|
pub starting_price_inr: Option<i32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PhotographerRepository;
|
pub struct PhotographerRepository;
|
||||||
|
|
||||||
impl PhotographerRepository {
|
impl PhotographerRepository {
|
||||||
pub async fn get_by_user_id(pool: &PgPool, user_id: Uuid) -> Result<Option<PhotographerProfile>, sqlx::Error> {
|
pub async fn get_by_user_role_id(pool: &PgPool, user_role_profile_id: Uuid) -> Result<Option<PhotographerProfile>, sqlx::Error> {
|
||||||
sqlx::query_as::<_, PhotographerProfile>(
|
sqlx::query_as::<_, PhotographerProfile>(
|
||||||
r#"SELECT id, user_id, display_name, bio, location,
|
r#"SELECT id, user_role_profile_id, specialties, camera_brands,
|
||||||
custom_data,
|
studio_available, outdoor_shoots, travel_radius_km,
|
||||||
status, created_at, updated_at
|
starting_price_inr,
|
||||||
FROM photographer_profiles WHERE user_id = $1"#,
|
created_at, updated_at
|
||||||
|
FROM photographer_profiles WHERE user_role_profile_id = $1"#,
|
||||||
)
|
)
|
||||||
.bind(user_id)
|
.bind(user_role_profile_id)
|
||||||
.fetch_optional(pool)
|
.fetch_optional(pool)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn upsert(pool: &PgPool, user_id: Uuid, p: UpsertPhotographerProfilePayload) -> Result<PhotographerProfile, sqlx::Error> {
|
pub async fn upsert(pool: &PgPool, user_role_profile_id: Uuid, p: UpsertPhotographerProfilePayload) -> Result<PhotographerProfile, sqlx::Error> {
|
||||||
sqlx::query_as::<_, PhotographerProfile>(
|
sqlx::query_as::<_, PhotographerProfile>(
|
||||||
r#"INSERT INTO photographer_profiles (user_id, display_name, bio, location, custom_data)
|
r#"INSERT INTO photographer_profiles (user_role_profile_id, specialties, camera_brands,
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
studio_available, outdoor_shoots, travel_radius_km, starting_price_inr)
|
||||||
ON CONFLICT (user_id) DO UPDATE SET
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
display_name = COALESCE(EXCLUDED.display_name, photographer_profiles.display_name),
|
ON CONFLICT (user_role_profile_id) DO UPDATE SET
|
||||||
bio = EXCLUDED.bio,
|
specialties = COALESCE(EXCLUDED.specialties, photographer_profiles.specialties),
|
||||||
location = EXCLUDED.location,
|
camera_brands = COALESCE(EXCLUDED.camera_brands, photographer_profiles.camera_brands),
|
||||||
custom_data = EXCLUDED.custom_data,
|
studio_available = EXCLUDED.studio_available,
|
||||||
updated_at = NOW()
|
outdoor_shoots = EXCLUDED.outdoor_shoots,
|
||||||
RETURNING id, user_id, display_name, bio, location,
|
travel_radius_km = EXCLUDED.travel_radius_km,
|
||||||
custom_data,
|
starting_price_inr = EXCLUDED.starting_price_inr,
|
||||||
status, created_at, updated_at"#,
|
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(user_role_profile_id)
|
||||||
.bind(p.display_name)
|
.bind(&p.specialties)
|
||||||
.bind(p.bio)
|
.bind(&p.camera_brands)
|
||||||
.bind(p.location)
|
.bind(p.studio_available)
|
||||||
.bind(p.custom_data)
|
.bind(p.outdoor_shoots)
|
||||||
|
.bind(p.travel_radius_km)
|
||||||
|
.bind(p.starting_price_inr)
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
|
||||||
237
crates/db/src/models/user_role_profile.rs
Normal file
237
crates/db/src/models/user_role_profile.rs
Normal file
|
|
@ -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<String>,
|
||||||
|
pub bio: Option<String>,
|
||||||
|
pub location: Option<String>,
|
||||||
|
pub avatar_url: Option<String>,
|
||||||
|
pub phone: Option<String>,
|
||||||
|
pub email: Option<String>,
|
||||||
|
pub status: String,
|
||||||
|
pub verification_status: String,
|
||||||
|
pub approval_status: String,
|
||||||
|
pub rejection_reason: Option<String>,
|
||||||
|
pub approved_at: Option<DateTime<Utc>>,
|
||||||
|
pub verified_at: Option<DateTime<Utc>>,
|
||||||
|
pub is_profile_public: bool,
|
||||||
|
pub created_at: DateTime<Utc>,
|
||||||
|
pub updated_at: DateTime<Utc>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct UserRoleProfileWithExtension<T> {
|
||||||
|
#[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<Option<UserRoleProfile>, 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<Option<UserRoleProfile>, 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<Vec<UserRoleProfile>, 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<UserRoleProfile, sqlx::Error> {
|
||||||
|
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<String>,
|
||||||
|
bio: Option<String>,
|
||||||
|
location: Option<String>,
|
||||||
|
avatar_url: Option<String>,
|
||||||
|
phone: Option<String>,
|
||||||
|
email: Option<String>,
|
||||||
|
is_profile_public: bool,
|
||||||
|
) -> Result<UserRoleProfile, sqlx::Error> {
|
||||||
|
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<UserRoleProfile, sqlx::Error> {
|
||||||
|
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<UserRoleProfile, sqlx::Error> {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
767
docs/migration_plan.md
Normal file
767
docs/migration_plan.md
Normal file
|
|
@ -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
|
||||||
508
docs/old_to_new_mapping.md
Normal file
508
docs/old_to_new_mapping.md
Normal file
|
|
@ -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`
|
||||||
216
docs/schema_audit.md
Normal file
216
docs/schema_audit.md
Normal file
|
|
@ -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
|
||||||
1052
docs/target_schema.md
Normal file
1052
docs/target_schema.md
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue