chore: checkpoint current workspace changes

This commit is contained in:
Ashwin Kumar 2026-03-22 15:55:29 +01:00
parent cb36e2fa7d
commit 91534d74c0
34 changed files with 240 additions and 97 deletions

View file

@ -41,6 +41,6 @@ prost = "0.13"
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono", "json"] } sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono", "json"] }
uuid = { version = "1", features = ["serde", "v4"] } uuid = { version = "1", features = ["serde", "v4"] }
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
lettre = { version = "0.11", features = ["tokio1-rustls-tls", "serde"] } lettre = { version = "0.11", default-features = false, features = ["tokio1-rustls-tls", "smtp-transport", "builder", "serde"] }
redis = { version = "0.27", features = ["tokio-comp"] } redis = { version = "0.27", features = ["tokio-comp", "connection-manager"] }
async-trait = "0.1"

View file

@ -8,7 +8,7 @@ axum = { workspace = true }
tokio = { workspace = true } tokio = { workspace = true }
serde = { workspace = true } serde = { workspace = true }
serde_json = { workspace = true } serde_json = { workspace = true }
tower-http = { version = "0.6", features = ["proxy", "cors"] } tower-http = { version = "0.6", features = ["cors"] }
tracing = { workspace = true } tracing = { workspace = true }
tracing-subscriber = { workspace = true } tracing-subscriber = { workspace = true }
reqwest = { version = "0.12", features = ["json", "stream"] } reqwest = { version = "0.12", features = ["json", "stream"] }

View file

@ -193,7 +193,7 @@ async fn main() {
let cors = build_cors(); let cors = build_cors();
let app = Router::new() let app = Router::new()
.route("/api/*path", any(proxy_handler)) .route("/api/{*path}", any(proxy_handler))
.route("/health", any(|| async { "Gateway OK" })) .route("/health", any(|| async { "Gateway OK" }))
.layer(cors) .layer(cors)
.with_state(services); .with_state(services);

View file

@ -19,5 +19,5 @@ lettre = { workspace = true }
contracts = { path = "../../crates/contracts" } contracts = { path = "../../crates/contracts" }
cache = { path = "../../crates/cache" } cache = { path = "../../crates/cache" }
rand = "0.8" rand = "0.8"
anyhow = { workspace = true }

View file

@ -15,16 +15,16 @@ use uuid::Uuid;
pub fn router() -> Router<AppState> { pub fn router() -> Router<AppState> {
Router::new() Router::new()
.route("/", get(list_pending)) .route("/", get(list_pending))
.route("/profiles/company/:user_id/approve", post(approve_company_profile)) .route("/profiles/company/{user_id}/approve", post(approve_company_profile))
.route("/profiles/company/:user_id/reject", post(reject_company_profile)) .route("/profiles/company/{user_id}/reject", post(reject_company_profile))
.route("/profiles/customer/:user_id/approve", post(approve_customer_profile)) .route("/profiles/customer/{user_id}/approve", post(approve_customer_profile))
.route("/profiles/customer/:user_id/reject", post(reject_customer_profile)) .route("/profiles/customer/{user_id}/reject", post(reject_customer_profile))
.route("/profiles/professional/:role_key/:user_id/approve", post(approve_professional_profile)) .route("/profiles/professional/{role_key}/{user_id}/approve", post(approve_professional_profile))
.route("/profiles/professional/:role_key/:user_id/reject", post(reject_professional_profile)) .route("/profiles/professional/{role_key}/{user_id}/reject", post(reject_professional_profile))
.route("/jobs/:id/approve", post(approve_job)) .route("/jobs/{id}/approve", post(approve_job))
.route("/jobs/:id/reject", post(reject_job)) .route("/jobs/{id}/reject", post(reject_job))
.route("/requirements/:id/approve", post(approve_requirement)) .route("/requirements/{id}/approve", post(approve_requirement))
.route("/requirements/:id/reject", post(reject_requirement)) .route("/requirements/{id}/reject", post(reject_requirement))
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -269,18 +269,18 @@ async fn list_pending(
Json(serde_json::json!({ Json(serde_json::json!({
"jobs": jobs, "jobs": jobs,
"requirements": requirements, "requirements": requirements,
"profiles": { "profiles_summary": {
"company": company_profiles, "company": company_profiles.len(),
"customer": customer_profiles, "customer": customer_profiles.len(),
"photographer": photographer_profiles, "photographer": photographer_profiles.len(),
"makeup_artist": makeup_profiles, "makeup_artist": makeup_profiles.len(),
"tutor": tutor_profiles, "tutor": tutor_profiles.len(),
"developer": developer_profiles, "developer": developer_profiles.len(),
"video_editor": video_editor_profiles, "video_editor": video_editor_profiles.len(),
"graphic_designer": graphic_designer_profiles, "graphic_designer": graphic_designer_profiles.len(),
"social_media_manager": social_media_manager_profiles, "social_media_manager": social_media_manager_profiles.len(),
"fitness_trainer": fitness_trainer_profiles, "fitness_trainer": fitness_trainer_profiles.len(),
"catering_services": catering_profiles "catering_services": catering_profiles.len()
}, },
"pagination": { "page": page, "limit": limit } "pagination": { "page": page, "limit": limit }
})), })),

View file

@ -17,21 +17,21 @@ use uuid::Uuid;
pub fn onboarding_router() -> Router<AppState> { pub fn onboarding_router() -> Router<AppState> {
Router::new() Router::new()
.route("/", get(list_onboarding_configs).post(create_onboarding_config)) .route("/", get(list_onboarding_configs).post(create_onboarding_config))
.route("/:role_id", get(get_active_onboarding_config)) .route("/{role_id}", get(get_active_onboarding_config))
.route("/by-key/:role_key", get(get_onboarding_config_by_key)) .route("/by-key/{role_key}", get(get_onboarding_config_by_key))
} }
pub fn dashboard_router() -> Router<AppState> { pub fn dashboard_router() -> Router<AppState> {
Router::new() Router::new()
.route("/", get(list_dashboard_configs).post(create_dashboard_config)) .route("/", get(list_dashboard_configs).post(create_dashboard_config))
.route("/:role_id", get(get_active_dashboard_config)) .route("/{role_id}", get(get_active_dashboard_config))
.route("/by-key/:role_key", get(get_dashboard_config_by_key)) .route("/by-key/{role_key}", get(get_dashboard_config_by_key))
} }
pub fn runtime_router() -> Router<AppState> { pub fn runtime_router() -> Router<AppState> {
Router::new() Router::new()
.route("/", get(get_my_runtime_config).post(create_runtime_config)) .route("/", get(get_my_runtime_config).post(create_runtime_config))
.route("/:role_id", get(get_active_runtime_config)) .route("/{role_id}", get(get_active_runtime_config))
} }
async fn get_my_runtime_config( async fn get_my_runtime_config(

View file

@ -13,7 +13,7 @@ pub fn router() -> Router<AppState> {
Router::new() Router::new()
.route("/", get(list_notifications)) .route("/", get(list_notifications))
.route("/unread-count", get(unread_count)) .route("/unread-count", get(unread_count))
.route("/:id/read", patch(mark_read)) .route("/{id}/read", patch(mark_read))
.route("/read-all", patch(mark_all_read)) .route("/read-all", patch(mark_all_read))
} }

View file

@ -83,8 +83,8 @@ async fn get_state(
let response = match onboarding_state { let response = match onboarding_state {
Some(s) => serde_json::json!({ Some(s) => serde_json::json!({
"status": s.status, "status": s.status,
"currentStep": s.progress_json.as_ref() "currentStep": s.progress_json
.and_then(|p| p.get("step")) .get("step")
.and_then(|v| v.as_i64()) .and_then(|v| v.as_i64())
.unwrap_or(0), .unwrap_or(0),
"progress": s.progress_json, "progress": s.progress_json,

View file

@ -11,7 +11,7 @@ use db::models::role::{CreateRolePayload, RoleRepository};
pub fn router() -> Router<AppState> { pub fn router() -> Router<AppState> {
Router::new() Router::new()
.route("/", get(list_roles).post(create_role)) .route("/", get(list_roles).post(create_role))
.route("/:key", get(get_role_by_key)) .route("/{key}", get(get_role_by_key))
} }
async fn create_role( async fn create_role(

View file

@ -13,4 +13,5 @@ chrono = { workspace = true }
uuid = { workspace = true } uuid = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
axum = { workspace = true } axum = { workspace = true }
async-trait = { workspace = true }
db = { path = "../db" } db = { path = "../db" }

View file

@ -2,34 +2,39 @@ use axum::{
extract::FromRequestParts, extract::FromRequestParts,
http::{request::Parts, StatusCode}, http::{request::Parts, StatusCode},
}; };
use axum::async_trait; use std::future::Future;
pub struct RequireAuth(pub crate::jwt::Claims); pub struct RequireAuth(pub crate::jwt::Claims);
#[async_trait]
impl<S> FromRequestParts<S> for RequireAuth impl<S> FromRequestParts<S> for RequireAuth
where where
S: Send + Sync, S: Send + Sync,
{ {
type Rejection = (StatusCode, &'static str); type Rejection = (StatusCode, &'static str);
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> { fn from_request_parts(
let auth_header = parts parts: &mut Parts,
_state: &S,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
let token = parts
.headers .headers
.get("Authorization") .get("Authorization")
.and_then(|value| value.to_str().ok()) .and_then(|value| value.to_str().ok())
.filter(|value| value.starts_with("Bearer ")); .filter(|value| value.starts_with("Bearer "))
.map(|header| header.trim_start_matches("Bearer ").to_string());
let token = match auth_header { async move {
Some(header) => header.trim_start_matches("Bearer "), let token = match token {
Some(token) => token,
None => return Err((StatusCode::UNAUTHORIZED, "Missing Bearer token")), None => return Err((StatusCode::UNAUTHORIZED, "Missing Bearer token")),
}; };
let jwt_secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set"); let jwt_secret = std::env::var("JWT_SECRET").expect("JWT_SECRET must be set");
match crate::jwt::verify_access_token(token, &jwt_secret) { match crate::jwt::verify_access_token(&token, &jwt_secret) {
Ok(claims) => Ok(RequireAuth(claims)), Ok(claims) => Ok(RequireAuth(claims)),
Err(_) => Err((StatusCode::UNAUTHORIZED, "Invalid or expired token")), Err(_) => Err((StatusCode::UNAUTHORIZED, "Invalid or expired token")),
} }
} }
} }
}

View file

@ -36,7 +36,7 @@ pub async fn invalidate_marketplace(
let pattern = format!("jobs:marketplace:{profession}:*"); let pattern = format!("jobs:marketplace:{profession}:*");
let keys: Vec<String> = redis.keys(pattern).await?; let keys: Vec<String> = redis.keys(pattern).await?;
if !keys.is_empty() { if !keys.is_empty() {
redis.del(keys).await?; redis.del::<_, ()>(keys).await?;
} }
Ok(()) Ok(())
} }

View file

@ -43,7 +43,7 @@ pub async fn record_resend(redis: &mut RedisPool, user_id: &str) -> Result<(), r
let count: i64 = redis.incr(&key, 1i64).await?; let count: i64 = redis.incr(&key, 1i64).await?;
// Only set expiry on first increment so window is fixed from first request // Only set expiry on first increment so window is fixed from first request
if count == 1 { if count == 1 {
redis.expire(&key, RESEND_WINDOW_SECS).await?; redis.expire::<_, ()>(&key, RESEND_WINDOW_SECS).await?;
} }
Ok(()) Ok(())
} }

View file

@ -24,7 +24,7 @@ pub async fn check(
let key = format!("rate:{namespace}:{identifier}"); let key = format!("rate:{namespace}:{identifier}");
let count: i64 = redis.incr(&key, 1i64).await?; let count: i64 = redis.incr(&key, 1i64).await?;
if count == 1 { if count == 1 {
redis.expire(&key, window_secs).await?; redis.expire::<_, ()>(&key, window_secs).await?;
} }
Ok(count <= max) Ok(count <= max)
} }

View file

@ -11,6 +11,8 @@ tracing = { workspace = true }
uuid = { workspace = true } uuid = { workspace = true }
chrono = { workspace = true } chrono = { workspace = true }
anyhow = { workspace = true } anyhow = { workspace = true }
sqlx = { workspace = true }
async-trait = { workspace = true }
jsonwebtoken = "9.3" jsonwebtoken = "9.3"
db = { path = "../db" } db = { path = "../db" }
cache = { path = "../cache" } cache = { path = "../cache" }

View file

@ -6,6 +6,7 @@ use axum::{
}; };
use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm}; use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::future::Future;
use uuid::Uuid; use uuid::Uuid;
// ── JWT Claims ──────────────────────────────────────────────────────────────── // ── JWT Claims ────────────────────────────────────────────────────────────────
@ -31,20 +32,24 @@ pub struct AuthUser {
pub claims: Claims, pub claims: Claims,
} }
#[axum::async_trait]
impl<S> FromRequestParts<S> for AuthUser impl<S> FromRequestParts<S> for AuthUser
where where
S: Send + Sync, S: Send + Sync,
{ {
type Rejection = AuthError; type Rejection = AuthError;
async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> { fn from_request_parts(
// 1. Extract Authorization header parts: &mut Parts,
_state: &S,
) -> impl Future<Output = Result<Self, Self::Rejection>> + Send {
let auth_header = parts let auth_header = parts
.headers .headers
.get("Authorization") .get("Authorization")
.and_then(|v| v.to_str().ok()) .and_then(|v| v.to_str().ok())
.ok_or(AuthError::MissingToken)?; .map(str::to_string);
async move {
let auth_header = auth_header.ok_or(AuthError::MissingToken)?;
// 2. Strip "Bearer " prefix // 2. Strip "Bearer " prefix
let token = auth_header let token = auth_header
@ -76,6 +81,7 @@ where
}) })
} }
} }
}
// ── Auth Error types ────────────────────────────────────────────────────────── // ── Auth Error types ──────────────────────────────────────────────────────────

View file

@ -142,7 +142,10 @@ async fn send_lead_request(
) )
.into_response() .into_response()
} }
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), Err(e) => {
tracing::error!("Failed to check profile approval: {}", e);
return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to validate profile approval").into_response();
}
} }
// ── Deduplication: one lead per requirement per professional (24 h) ──────── // ── Deduplication: one lead per requirement per professional (24 h) ────────

View file

@ -204,6 +204,126 @@ ALTER TABLE services
ALTER TABLE lead_requests ALTER TABLE lead_requests
ADD COLUMN IF NOT EXISTS professional_user_id UUID REFERENCES users(id) ON DELETE CASCADE; ADD COLUMN IF NOT EXISTS professional_user_id UUID REFERENCES users(id) ON DELETE CASCADE;
-- Backfill columns when legacy minimal profile tables already exist.
-- This keeps migrations idempotent while upgrading old schemas to the new profile shape.
ALTER TABLE photographer_profiles
ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS bio TEXT,
ADD COLUMN IF NOT EXISTS location VARCHAR(255),
ADD COLUMN IF NOT EXISTS specialties TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS camera_brands TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS studio_available BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS outdoor_shoots BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN IF NOT EXISTS travel_radius_km INTEGER DEFAULT 50,
ADD COLUMN IF NOT EXISTS starting_price_inr INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
ADD COLUMN IF NOT EXISTS rejection_reason TEXT,
ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ;
ALTER TABLE tutor_profiles
ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS bio TEXT,
ADD COLUMN IF NOT EXISTS location VARCHAR(255),
ADD COLUMN IF NOT EXISTS subjects TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS board_types TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS qualification VARCHAR(255),
ADD COLUMN IF NOT EXISTS teaches_online BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN IF NOT EXISTS teaches_offline BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN IF NOT EXISTS hourly_rate_inr INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
ADD COLUMN IF NOT EXISTS rejection_reason TEXT,
ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ;
ALTER TABLE makeup_artist_profiles
ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS location VARCHAR(255),
ADD COLUMN IF NOT EXISTS specializations TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS kit_brands TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS home_service BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN IF NOT EXISTS studio_available BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS starting_price_inr INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
ADD COLUMN IF NOT EXISTS rejection_reason TEXT,
ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ;
ALTER TABLE developer_profiles
ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS location VARCHAR(255),
ADD COLUMN IF NOT EXISTS tech_stack TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS github_url VARCHAR(500),
ADD COLUMN IF NOT EXISTS portfolio_url VARCHAR(500),
ADD COLUMN IF NOT EXISTS availability VARCHAR(50) DEFAULT 'FULL_TIME',
ADD COLUMN IF NOT EXISTS hourly_rate_inr INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS remote_ok BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
ADD COLUMN IF NOT EXISTS rejection_reason TEXT,
ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ;
ALTER TABLE video_editor_profiles
ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS location VARCHAR(255),
ADD COLUMN IF NOT EXISTS software_skills TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS style_tags TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS turnaround_days INTEGER DEFAULT 7,
ADD COLUMN IF NOT EXISTS reel_url VARCHAR(500),
ADD COLUMN IF NOT EXISTS starting_price_inr INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
ADD COLUMN IF NOT EXISTS rejection_reason TEXT,
ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ;
ALTER TABLE graphic_designer_profiles
ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS location VARCHAR(255),
ADD COLUMN IF NOT EXISTS design_tools TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS style_tags TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS brand_experience BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS portfolio_url VARCHAR(500),
ADD COLUMN IF NOT EXISTS starting_price_inr INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
ADD COLUMN IF NOT EXISTS rejection_reason TEXT,
ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ;
ALTER TABLE social_media_manager_profiles
ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS location VARCHAR(255),
ADD COLUMN IF NOT EXISTS platforms TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS industries TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS content_types TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS avg_follower_growth_pct INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS starting_price_inr INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
ADD COLUMN IF NOT EXISTS rejection_reason TEXT,
ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ;
ALTER TABLE fitness_trainer_profiles
ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS location VARCHAR(255),
ADD COLUMN IF NOT EXISTS disciplines TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS certifications TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS online_sessions BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN IF NOT EXISTS home_visits BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS gym_based BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN IF NOT EXISTS per_session_rate_inr INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
ADD COLUMN IF NOT EXISTS rejection_reason TEXT,
ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ;
ALTER TABLE catering_service_profiles
ADD COLUMN IF NOT EXISTS business_name VARCHAR(255) NOT NULL DEFAULT '',
ADD COLUMN IF NOT EXISTS location VARCHAR(255),
ADD COLUMN IF NOT EXISTS cuisine_types TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS event_types TEXT[] DEFAULT '{}',
ADD COLUMN IF NOT EXISTS min_guests INTEGER DEFAULT 10,
ADD COLUMN IF NOT EXISTS max_guests INTEGER DEFAULT 500,
ADD COLUMN IF NOT EXISTS has_setup_team BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN IF NOT EXISTS has_serving_staff BOOLEAN NOT NULL DEFAULT true,
ADD COLUMN IF NOT EXISTS price_per_head_inr INTEGER DEFAULT 0,
ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
ADD COLUMN IF NOT EXISTS rejection_reason TEXT,
ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ;
ALTER TABLE lead_requests
ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW();
-- Indexes -- Indexes
CREATE INDEX IF NOT EXISTS idx_photographer_profiles_status ON photographer_profiles(status); CREATE INDEX IF NOT EXISTS idx_photographer_profiles_status ON photographer_profiles(status);
CREATE INDEX IF NOT EXISTS idx_tutor_profiles_status ON tutor_profiles(status); CREATE INDEX IF NOT EXISTS idx_tutor_profiles_status ON tutor_profiles(status);

View file

@ -2,7 +2,7 @@
CREATE TABLE IF NOT EXISTS reviews ( CREATE TABLE IF NOT EXISTS reviews (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(), id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
lead_request_id UUID NOT NULL REFERENCES lead_requests(id) ON DELETE CASCADE UNIQUE, lead_request_id UUID NOT NULL REFERENCES lead_requests(id) ON DELETE CASCADE UNIQUE,
customer_id UUID NOT NULL REFERENCES customers(id) ON DELETE CASCADE, customer_id UUID NOT NULL REFERENCES customer_profiles(id) ON DELETE CASCADE,
professional_id UUID NOT NULL REFERENCES professionals(id) ON DELETE CASCADE, professional_id UUID NOT NULL REFERENCES professionals(id) ON DELETE CASCADE,
rating SMALLINT NOT NULL CHECK (rating >= 1 AND rating <= 5), rating SMALLINT NOT NULL CHECK (rating >= 1 AND rating <= 5),
comment TEXT, comment TEXT,

View file

@ -33,7 +33,7 @@ impl CateringServiceRepository {
sqlx::query_as!( sqlx::query_as!(
CateringServiceProfile, CateringServiceProfile,
r#"SELECT id, user_id, business_name, bio, location, r#"SELECT id, user_id, business_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>", custom_data,
status, created_at, updated_at status, created_at, updated_at
FROM catering_service_profiles WHERE user_id = $1"#, FROM catering_service_profiles WHERE user_id = $1"#,
user_id user_id
@ -52,7 +52,7 @@ impl CateringServiceRepository {
custom_data = EXCLUDED.custom_data, custom_data = EXCLUDED.custom_data,
updated_at = NOW() updated_at = NOW()
RETURNING id, user_id, business_name, bio, location, RETURNING id, user_id, business_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>", custom_data,
status, created_at, updated_at"#, status, created_at, updated_at"#,
user_id, p.business_name, p.bio, p.location, p.custom_data user_id, p.business_name, p.bio, p.location, p.custom_data
).fetch_one(pool).await ).fetch_one(pool).await

View file

@ -183,7 +183,7 @@ impl ConfigRepository {
r#" r#"
UPDATE dashboard_configs UPDATE dashboard_configs
SET is_active = false SET is_active = false
WHERE role_id = $1 AND audience = $2 AND is_active = true WHERE role_id = $1 AND audience = $2::text AND is_active = true
"#, "#,
payload.role_id, payload.role_id,
payload.audience payload.audience
@ -198,9 +198,9 @@ impl ConfigRepository {
INSERT INTO dashboard_configs (role_id, audience, config_json, version, is_active) INSERT INTO dashboard_configs (role_id, audience, config_json, version, is_active)
VALUES ( VALUES (
$1, $1,
$2, $2::text,
$3, $3,
COALESCE((SELECT MAX(version) FROM dashboard_configs WHERE role_id = $1 AND audience = $2), 0) + 1, COALESCE((SELECT MAX(version) FROM dashboard_configs WHERE role_id = $1 AND audience = $2::text), 0) + 1,
true true
) )
RETURNING id, role_id, audience, config_json, version, is_active, updated_at RETURNING id, role_id, audience, config_json, version, is_active, updated_at

View file

@ -11,7 +11,7 @@ pub struct CustomerProfile {
pub phone: Option<String>, pub phone: Option<String>,
pub city: Option<String>, pub city: Option<String>,
pub area: Option<String>, pub area: Option<String>,
pub preferred_professions: Vec<String>, pub preferred_professions: Option<Vec<String>>,
pub active_requirement_count: i32, pub active_requirement_count: i32,
pub status: String, pub status: String,
pub bio: Option<String>, pub bio: Option<String>,

View file

@ -31,7 +31,7 @@ impl DeveloperRepository {
sqlx::query_as!( sqlx::query_as!(
DeveloperProfile, DeveloperProfile,
r#"SELECT id, user_id, display_name, bio, location, r#"SELECT id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>", custom_data,
status, created_at, updated_at status, created_at, updated_at
FROM developer_profiles WHERE user_id = $1"#, FROM developer_profiles WHERE user_id = $1"#,
user_id user_id
@ -50,7 +50,7 @@ impl DeveloperRepository {
custom_data = EXCLUDED.custom_data, custom_data = EXCLUDED.custom_data,
updated_at = NOW() updated_at = NOW()
RETURNING id, user_id, display_name, bio, location, RETURNING id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>", custom_data,
status, created_at, updated_at"#, status, created_at, updated_at"#,
user_id, p.display_name, p.bio, p.location, p.custom_data user_id, p.display_name, p.bio, p.location, p.custom_data
).fetch_one(pool).await ).fetch_one(pool).await

View file

@ -31,7 +31,7 @@ impl FitnessTrainerRepository {
sqlx::query_as!( sqlx::query_as!(
FitnessTrainerProfile, FitnessTrainerProfile,
r#"SELECT id, user_id, display_name, bio, location, r#"SELECT id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>", custom_data,
status, created_at, updated_at status, created_at, updated_at
FROM fitness_trainer_profiles WHERE user_id = $1"#, FROM fitness_trainer_profiles WHERE user_id = $1"#,
user_id user_id
@ -50,7 +50,7 @@ impl FitnessTrainerRepository {
custom_data = EXCLUDED.custom_data, custom_data = EXCLUDED.custom_data,
updated_at = NOW() updated_at = NOW()
RETURNING id, user_id, display_name, bio, location, RETURNING id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>", custom_data,
status, created_at, updated_at"#, status, created_at, updated_at"#,
user_id, p.display_name, p.bio, p.location, p.custom_data user_id, p.display_name, p.bio, p.location, p.custom_data
).fetch_one(pool).await ).fetch_one(pool).await

View file

@ -31,7 +31,7 @@ impl GraphicDesignerRepository {
sqlx::query_as!( sqlx::query_as!(
GraphicDesignerProfile, GraphicDesignerProfile,
r#"SELECT id, user_id, display_name, bio, location, r#"SELECT id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>", custom_data,
status, created_at, updated_at status, created_at, updated_at
FROM graphic_designer_profiles WHERE user_id = $1"#, FROM graphic_designer_profiles WHERE user_id = $1"#,
user_id user_id
@ -50,7 +50,7 @@ impl GraphicDesignerRepository {
custom_data = EXCLUDED.custom_data, custom_data = EXCLUDED.custom_data,
updated_at = NOW() updated_at = NOW()
RETURNING id, user_id, display_name, bio, location, RETURNING id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>", custom_data,
status, created_at, updated_at"#, status, created_at, updated_at"#,
user_id, p.display_name, p.bio, p.location, p.custom_data user_id, p.display_name, p.bio, p.location, p.custom_data
).fetch_one(pool).await ).fetch_one(pool).await

View file

@ -15,7 +15,7 @@ pub struct Job {
pub salary_min: Option<i32>, pub salary_min: Option<i32>,
pub salary_max: Option<i32>, pub salary_max: Option<i32>,
pub experience_years: Option<i32>, pub experience_years: Option<i32>,
pub skills: Vec<String>, pub skills: Option<Vec<String>>,
pub status: String, // DRAFT, PENDING_APPROVAL, LIVE, EXPIRED, CLOSED, REJECTED pub status: String, // DRAFT, PENDING_APPROVAL, LIVE, EXPIRED, CLOSED, REJECTED
pub rejection_reason: Option<String>, pub rejection_reason: Option<String>,
pub expires_at: Option<DateTime<Utc>>, pub expires_at: Option<DateTime<Utc>>,

View file

@ -10,8 +10,8 @@ pub struct JobSeekerProfile {
pub full_name: Option<String>, pub full_name: Option<String>,
pub location: Option<String>, pub location: Option<String>,
pub summary: Option<String>, pub summary: Option<String>,
pub experience_years: i32, pub experience_years: Option<i32>,
pub skills: Vec<String>, pub skills: Option<Vec<String>>,
pub resume_url: Option<String>, pub resume_url: Option<String>,
pub active_application_count: i32, pub active_application_count: i32,
pub status: String, pub status: String,

View file

@ -13,6 +13,8 @@ pub struct LeadRequest {
pub expires_at: DateTime<Utc>, pub expires_at: DateTime<Utc>,
pub requested_at: DateTime<Utc>, pub requested_at: DateTime<Utc>,
pub resolved_at: Option<DateTime<Utc>>, pub resolved_at: Option<DateTime<Utc>>,
pub professional_user_id: Option<Uuid>,
pub updated_at: DateTime<Utc>,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]

View file

@ -31,7 +31,7 @@ impl MakeupArtistRepository {
sqlx::query_as!( sqlx::query_as!(
MakeupArtistProfile, MakeupArtistProfile,
r#"SELECT id, user_id, display_name, bio, location, r#"SELECT id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>", custom_data,
status, created_at, updated_at status, created_at, updated_at
FROM makeup_artist_profiles WHERE user_id = $1"#, FROM makeup_artist_profiles WHERE user_id = $1"#,
user_id user_id
@ -50,7 +50,7 @@ impl MakeupArtistRepository {
custom_data = EXCLUDED.custom_data, custom_data = EXCLUDED.custom_data,
updated_at = NOW() updated_at = NOW()
RETURNING id, user_id, display_name, bio, location, RETURNING id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>", custom_data,
status, created_at, updated_at"#, status, created_at, updated_at"#,
user_id, p.display_name, p.bio, p.location, p.custom_data user_id, p.display_name, p.bio, p.location, p.custom_data
).fetch_one(pool).await ).fetch_one(pool).await

View file

@ -31,7 +31,7 @@ impl PhotographerRepository {
sqlx::query_as!( sqlx::query_as!(
PhotographerProfile, PhotographerProfile,
r#"SELECT id, user_id, display_name, bio, location, r#"SELECT id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>", custom_data,
status, created_at, updated_at status, created_at, updated_at
FROM photographer_profiles WHERE user_id = $1"#, FROM photographer_profiles WHERE user_id = $1"#,
user_id user_id
@ -50,7 +50,7 @@ impl PhotographerRepository {
custom_data = EXCLUDED.custom_data, custom_data = EXCLUDED.custom_data,
updated_at = NOW() updated_at = NOW()
RETURNING id, user_id, display_name, bio, location, RETURNING id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>", custom_data,
status, created_at, updated_at"#, status, created_at, updated_at"#,
user_id, p.display_name, p.bio, p.location, p.custom_data user_id, p.display_name, p.bio, p.location, p.custom_data
).fetch_one(pool).await ).fetch_one(pool).await

View file

@ -28,6 +28,8 @@ pub struct PortfolioItem {
pub tags: Option<Vec<String>>, pub tags: Option<Vec<String>>,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>, pub updated_at: DateTime<Utc>,
pub user_id: Option<Uuid>,
pub profession_key: Option<String>,
} }
#[derive(Debug, Serialize, Deserialize, FromRow)] #[derive(Debug, Serialize, Deserialize, FromRow)]
@ -41,6 +43,8 @@ pub struct Service {
pub is_active: bool, pub is_active: bool,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>, pub updated_at: DateTime<Utc>,
pub user_id: Option<Uuid>,
pub profession_key: Option<String>,
} }
#[derive(Debug, Serialize, Deserialize, FromRow)] #[derive(Debug, Serialize, Deserialize, FromRow)]

View file

@ -31,7 +31,7 @@ impl SocialMediaManagerRepository {
sqlx::query_as!( sqlx::query_as!(
SocialMediaManagerProfile, SocialMediaManagerProfile,
r#"SELECT id, user_id, display_name, bio, location, r#"SELECT id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>", custom_data,
status, created_at, updated_at status, created_at, updated_at
FROM social_media_manager_profiles WHERE user_id = $1"#, FROM social_media_manager_profiles WHERE user_id = $1"#,
user_id user_id
@ -50,7 +50,7 @@ impl SocialMediaManagerRepository {
custom_data = EXCLUDED.custom_data, custom_data = EXCLUDED.custom_data,
updated_at = NOW() updated_at = NOW()
RETURNING id, user_id, display_name, bio, location, RETURNING id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>", custom_data,
status, created_at, updated_at"#, status, created_at, updated_at"#,
user_id, p.display_name, p.bio, p.location, p.custom_data user_id, p.display_name, p.bio, p.location, p.custom_data
).fetch_one(pool).await ).fetch_one(pool).await

View file

@ -31,7 +31,7 @@ impl TutorRepository {
sqlx::query_as!( sqlx::query_as!(
TutorProfile, TutorProfile,
r#"SELECT id, user_id, display_name, bio, location, r#"SELECT id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>", custom_data,
status, created_at, updated_at status, created_at, updated_at
FROM tutor_profiles WHERE user_id = $1"#, FROM tutor_profiles WHERE user_id = $1"#,
user_id user_id
@ -50,7 +50,7 @@ impl TutorRepository {
custom_data = EXCLUDED.custom_data, custom_data = EXCLUDED.custom_data,
updated_at = NOW() updated_at = NOW()
RETURNING id, user_id, display_name, bio, location, RETURNING id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>", custom_data,
status, created_at, updated_at"#, status, created_at, updated_at"#,
user_id, p.display_name, p.bio, p.location, p.custom_data user_id, p.display_name, p.bio, p.location, p.custom_data
).fetch_one(pool).await ).fetch_one(pool).await

View file

@ -31,7 +31,7 @@ impl VideoEditorRepository {
sqlx::query_as!( sqlx::query_as!(
VideoEditorProfile, VideoEditorProfile,
r#"SELECT id, user_id, display_name, bio, location, r#"SELECT id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>", custom_data,
status, created_at, updated_at status, created_at, updated_at
FROM video_editor_profiles WHERE user_id = $1"#, FROM video_editor_profiles WHERE user_id = $1"#,
user_id user_id
@ -50,7 +50,7 @@ impl VideoEditorRepository {
custom_data = EXCLUDED.custom_data, custom_data = EXCLUDED.custom_data,
updated_at = NOW() updated_at = NOW()
RETURNING id, user_id, display_name, bio, location, RETURNING id, user_id, display_name, bio, location,
custom_data as "custom_data: Option<serde_json::Value>", custom_data,
status, created_at, updated_at"#, status, created_at, updated_at"#,
user_id, p.display_name, p.bio, p.location, p.custom_data user_id, p.display_name, p.bio, p.location, p.custom_data
).fetch_one(pool).await ).fetch_one(pool).await