diff --git a/Cargo.toml b/Cargo.toml index 1ab7497..651b877 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,6 +41,6 @@ prost = "0.13" sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono", "json"] } uuid = { version = "1", features = ["serde", "v4"] } chrono = { version = "0.4", features = ["serde"] } -lettre = { version = "0.11", features = ["tokio1-rustls-tls", "serde"] } -redis = { version = "0.27", features = ["tokio-comp"] } - +lettre = { version = "0.11", default-features = false, features = ["tokio1-rustls-tls", "smtp-transport", "builder", "serde"] } +redis = { version = "0.27", features = ["tokio-comp", "connection-manager"] } +async-trait = "0.1" diff --git a/apps/gateway/Cargo.toml b/apps/gateway/Cargo.toml index c292353..4b55ae9 100644 --- a/apps/gateway/Cargo.toml +++ b/apps/gateway/Cargo.toml @@ -8,7 +8,7 @@ axum = { workspace = true } tokio = { workspace = true } serde = { 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-subscriber = { workspace = true } reqwest = { version = "0.12", features = ["json", "stream"] } diff --git a/apps/gateway/src/main.rs b/apps/gateway/src/main.rs index d2a6e29..2d5a5b4 100644 --- a/apps/gateway/src/main.rs +++ b/apps/gateway/src/main.rs @@ -193,7 +193,7 @@ async fn main() { let cors = build_cors(); let app = Router::new() - .route("/api/*path", any(proxy_handler)) + .route("/api/{*path}", any(proxy_handler)) .route("/health", any(|| async { "Gateway OK" })) .layer(cors) .with_state(services); diff --git a/apps/users/Cargo.toml b/apps/users/Cargo.toml index 23f5cd0..c19ec47 100644 --- a/apps/users/Cargo.toml +++ b/apps/users/Cargo.toml @@ -19,5 +19,5 @@ lettre = { workspace = true } contracts = { path = "../../crates/contracts" } cache = { path = "../../crates/cache" } rand = "0.8" - +anyhow = { workspace = true } diff --git a/apps/users/src/handlers/approvals.rs b/apps/users/src/handlers/approvals.rs index 7184541..297782a 100644 --- a/apps/users/src/handlers/approvals.rs +++ b/apps/users/src/handlers/approvals.rs @@ -15,16 +15,16 @@ use uuid::Uuid; pub fn router() -> Router { Router::new() .route("/", get(list_pending)) - .route("/profiles/company/:user_id/approve", post(approve_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/reject", post(reject_customer_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("/jobs/:id/approve", post(approve_job)) - .route("/jobs/:id/reject", post(reject_job)) - .route("/requirements/:id/approve", post(approve_requirement)) - .route("/requirements/:id/reject", post(reject_requirement)) + .route("/profiles/company/{user_id}/approve", post(approve_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}/reject", post(reject_customer_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("/jobs/{id}/approve", post(approve_job)) + .route("/jobs/{id}/reject", post(reject_job)) + .route("/requirements/{id}/approve", post(approve_requirement)) + .route("/requirements/{id}/reject", post(reject_requirement)) } #[derive(Deserialize)] @@ -269,18 +269,18 @@ async fn list_pending( Json(serde_json::json!({ "jobs": jobs, "requirements": requirements, - "profiles": { - "company": company_profiles, - "customer": customer_profiles, - "photographer": photographer_profiles, - "makeup_artist": makeup_profiles, - "tutor": tutor_profiles, - "developer": developer_profiles, - "video_editor": video_editor_profiles, - "graphic_designer": graphic_designer_profiles, - "social_media_manager": social_media_manager_profiles, - "fitness_trainer": fitness_trainer_profiles, - "catering_services": catering_profiles + "profiles_summary": { + "company": company_profiles.len(), + "customer": customer_profiles.len(), + "photographer": photographer_profiles.len(), + "makeup_artist": makeup_profiles.len(), + "tutor": tutor_profiles.len(), + "developer": developer_profiles.len(), + "video_editor": video_editor_profiles.len(), + "graphic_designer": graphic_designer_profiles.len(), + "social_media_manager": social_media_manager_profiles.len(), + "fitness_trainer": fitness_trainer_profiles.len(), + "catering_services": catering_profiles.len() }, "pagination": { "page": page, "limit": limit } })), diff --git a/apps/users/src/handlers/config.rs b/apps/users/src/handlers/config.rs index a6b86f6..e0c5d2d 100644 --- a/apps/users/src/handlers/config.rs +++ b/apps/users/src/handlers/config.rs @@ -17,21 +17,21 @@ use uuid::Uuid; pub fn onboarding_router() -> Router { Router::new() .route("/", get(list_onboarding_configs).post(create_onboarding_config)) - .route("/:role_id", get(get_active_onboarding_config)) - .route("/by-key/:role_key", get(get_onboarding_config_by_key)) + .route("/{role_id}", get(get_active_onboarding_config)) + .route("/by-key/{role_key}", get(get_onboarding_config_by_key)) } pub fn dashboard_router() -> Router { Router::new() .route("/", get(list_dashboard_configs).post(create_dashboard_config)) - .route("/:role_id", get(get_active_dashboard_config)) - .route("/by-key/:role_key", get(get_dashboard_config_by_key)) + .route("/{role_id}", get(get_active_dashboard_config)) + .route("/by-key/{role_key}", get(get_dashboard_config_by_key)) } pub fn runtime_router() -> Router { Router::new() .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( diff --git a/apps/users/src/handlers/notifications.rs b/apps/users/src/handlers/notifications.rs index 3a46dbf..d7321b4 100644 --- a/apps/users/src/handlers/notifications.rs +++ b/apps/users/src/handlers/notifications.rs @@ -13,7 +13,7 @@ pub fn router() -> Router { Router::new() .route("/", get(list_notifications)) .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)) } diff --git a/apps/users/src/handlers/onboarding.rs b/apps/users/src/handlers/onboarding.rs index df02fd6..206889e 100644 --- a/apps/users/src/handlers/onboarding.rs +++ b/apps/users/src/handlers/onboarding.rs @@ -83,8 +83,8 @@ async fn get_state( let response = match onboarding_state { Some(s) => serde_json::json!({ "status": s.status, - "currentStep": s.progress_json.as_ref() - .and_then(|p| p.get("step")) + "currentStep": s.progress_json + .get("step") .and_then(|v| v.as_i64()) .unwrap_or(0), "progress": s.progress_json, diff --git a/apps/users/src/handlers/roles.rs b/apps/users/src/handlers/roles.rs index 95524d9..e54bc8b 100644 --- a/apps/users/src/handlers/roles.rs +++ b/apps/users/src/handlers/roles.rs @@ -11,7 +11,7 @@ use db::models::role::{CreateRolePayload, RoleRepository}; pub fn router() -> Router { Router::new() .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( diff --git a/crates/auth/Cargo.toml b/crates/auth/Cargo.toml index 3605e3b..e597cba 100644 --- a/crates/auth/Cargo.toml +++ b/crates/auth/Cargo.toml @@ -13,4 +13,5 @@ chrono = { workspace = true } uuid = { workspace = true } anyhow = { workspace = true } axum = { workspace = true } +async-trait = { workspace = true } db = { path = "../db" } diff --git a/crates/auth/src/middleware.rs b/crates/auth/src/middleware.rs index c96c4e6..dbdd13a 100644 --- a/crates/auth/src/middleware.rs +++ b/crates/auth/src/middleware.rs @@ -2,34 +2,39 @@ use axum::{ extract::FromRequestParts, http::{request::Parts, StatusCode}, }; -use axum::async_trait; +use std::future::Future; pub struct RequireAuth(pub crate::jwt::Claims); -#[async_trait] impl FromRequestParts for RequireAuth where S: Send + Sync, { type Rejection = (StatusCode, &'static str); - async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { - let auth_header = parts + fn from_request_parts( + parts: &mut Parts, + _state: &S, + ) -> impl Future> + Send { + let token = parts .headers .get("Authorization") .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 { - Some(header) => header.trim_start_matches("Bearer "), - None => return Err((StatusCode::UNAUTHORIZED, "Missing Bearer token")), - }; + async move { + let token = match token { + Some(token) => 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) { - Ok(claims) => Ok(RequireAuth(claims)), - Err(_) => Err((StatusCode::UNAUTHORIZED, "Invalid or expired token")), + match crate::jwt::verify_access_token(&token, &jwt_secret) { + Ok(claims) => Ok(RequireAuth(claims)), + Err(_) => Err((StatusCode::UNAUTHORIZED, "Invalid or expired token")), + } } } } diff --git a/crates/cache/src/jobs.rs b/crates/cache/src/jobs.rs index 72ded60..c0e0e7a 100644 --- a/crates/cache/src/jobs.rs +++ b/crates/cache/src/jobs.rs @@ -36,7 +36,7 @@ pub async fn invalidate_marketplace( let pattern = format!("jobs:marketplace:{profession}:*"); let keys: Vec = redis.keys(pattern).await?; if !keys.is_empty() { - redis.del(keys).await?; + redis.del::<_, ()>(keys).await?; } Ok(()) } diff --git a/crates/cache/src/otp.rs b/crates/cache/src/otp.rs index fd921a8..f5ab1f5 100644 --- a/crates/cache/src/otp.rs +++ b/crates/cache/src/otp.rs @@ -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?; // Only set expiry on first increment so window is fixed from first request if count == 1 { - redis.expire(&key, RESEND_WINDOW_SECS).await?; + redis.expire::<_, ()>(&key, RESEND_WINDOW_SECS).await?; } Ok(()) } diff --git a/crates/cache/src/rate_limit.rs b/crates/cache/src/rate_limit.rs index 1d688bc..2416d03 100644 --- a/crates/cache/src/rate_limit.rs +++ b/crates/cache/src/rate_limit.rs @@ -24,7 +24,7 @@ pub async fn check( let key = format!("rate:{namespace}:{identifier}"); let count: i64 = redis.incr(&key, 1i64).await?; if count == 1 { - redis.expire(&key, window_secs).await?; + redis.expire::<_, ()>(&key, window_secs).await?; } Ok(count <= max) } diff --git a/crates/contracts/Cargo.toml b/crates/contracts/Cargo.toml index a3e8a89..41d85aa 100644 --- a/crates/contracts/Cargo.toml +++ b/crates/contracts/Cargo.toml @@ -11,6 +11,8 @@ tracing = { workspace = true } uuid = { workspace = true } chrono = { workspace = true } anyhow = { workspace = true } +sqlx = { workspace = true } +async-trait = { workspace = true } jsonwebtoken = "9.3" db = { path = "../db" } cache = { path = "../cache" } diff --git a/crates/contracts/src/auth_middleware.rs b/crates/contracts/src/auth_middleware.rs index 2093af8..38eafd4 100644 --- a/crates/contracts/src/auth_middleware.rs +++ b/crates/contracts/src/auth_middleware.rs @@ -6,6 +6,7 @@ use axum::{ }; use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm}; use serde::{Deserialize, Serialize}; +use std::future::Future; use uuid::Uuid; // ── JWT Claims ──────────────────────────────────────────────────────────────── @@ -31,49 +32,54 @@ pub struct AuthUser { pub claims: Claims, } -#[axum::async_trait] impl FromRequestParts for AuthUser where S: Send + Sync, { type Rejection = AuthError; - async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { - // 1. Extract Authorization header + fn from_request_parts( + parts: &mut Parts, + _state: &S, + ) -> impl Future> + Send { let auth_header = parts .headers .get("Authorization") .and_then(|v| v.to_str().ok()) - .ok_or(AuthError::MissingToken)?; + .map(str::to_string); - // 2. Strip "Bearer " prefix - let token = auth_header + async move { + let auth_header = auth_header.ok_or(AuthError::MissingToken)?; + + // 2. Strip "Bearer " prefix + let token = auth_header .strip_prefix("Bearer ") .ok_or(AuthError::InvalidToken)?; - // 3. Decode & verify - let jwt_secret = std::env::var("JWT_SECRET") + // 3. Decode & verify + let jwt_secret = std::env::var("JWT_SECRET") .expect("JWT_SECRET must be set — refusing to start with insecure default"); - let token_data = decode::( + let token_data = decode::( token, &DecodingKey::from_secret(jwt_secret.as_bytes()), &Validation::new(Algorithm::HS256), - ) + ) .map_err(|e| { tracing::debug!("JWT decode error: {}", e); AuthError::InvalidToken })?; - // 4. Parse user_id as UUID - let user_id = Uuid::parse_str(&token_data.claims.sub) + // 4. Parse user_id as UUID + let user_id = Uuid::parse_str(&token_data.claims.sub) .map_err(|_| AuthError::InvalidToken)?; - Ok(AuthUser { - user_id, - email: token_data.claims.email.clone(), - claims: token_data.claims, - }) + Ok(AuthUser { + user_id, + email: token_data.claims.email.clone(), + claims: token_data.claims, + }) + } } } diff --git a/crates/contracts/src/profession_shared.rs b/crates/contracts/src/profession_shared.rs index 8cf0cfb..f10177b 100644 --- a/crates/contracts/src/profession_shared.rs +++ b/crates/contracts/src/profession_shared.rs @@ -142,7 +142,10 @@ async fn send_lead_request( ) .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) ──────── diff --git a/crates/db/migrations/20260317195000_profession_specific_profiles.up.sql b/crates/db/migrations/20260317195000_profession_specific_profiles.up.sql index 9055d73..dd45ab9 100644 --- a/crates/db/migrations/20260317195000_profession_specific_profiles.up.sql +++ b/crates/db/migrations/20260317195000_profession_specific_profiles.up.sql @@ -204,6 +204,126 @@ ALTER TABLE services ALTER TABLE lead_requests ADD COLUMN IF NOT EXISTS professional_user_id UUID REFERENCES users(id) ON DELETE CASCADE; +-- Backfill columns when legacy minimal profile tables already exist. +-- This keeps migrations idempotent while upgrading old schemas to the new profile shape. +ALTER TABLE photographer_profiles + ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS bio TEXT, + ADD COLUMN IF NOT EXISTS location VARCHAR(255), + ADD COLUMN IF NOT EXISTS specialties TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS camera_brands TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS studio_available BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS outdoor_shoots BOOLEAN NOT NULL DEFAULT true, + ADD COLUMN IF NOT EXISTS travel_radius_km INTEGER DEFAULT 50, + ADD COLUMN IF NOT EXISTS starting_price_inr INTEGER DEFAULT 0, + ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + ADD COLUMN IF NOT EXISTS rejection_reason TEXT, + ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; + +ALTER TABLE tutor_profiles + ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS bio TEXT, + ADD COLUMN IF NOT EXISTS location VARCHAR(255), + ADD COLUMN IF NOT EXISTS subjects TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS board_types TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS qualification VARCHAR(255), + ADD COLUMN IF NOT EXISTS teaches_online BOOLEAN NOT NULL DEFAULT true, + ADD COLUMN IF NOT EXISTS teaches_offline BOOLEAN NOT NULL DEFAULT true, + ADD COLUMN IF NOT EXISTS hourly_rate_inr INTEGER DEFAULT 0, + ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + ADD COLUMN IF NOT EXISTS rejection_reason TEXT, + ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; + +ALTER TABLE makeup_artist_profiles + ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS location VARCHAR(255), + ADD COLUMN IF NOT EXISTS specializations TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS kit_brands TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS home_service BOOLEAN NOT NULL DEFAULT true, + ADD COLUMN IF NOT EXISTS studio_available BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS starting_price_inr INTEGER DEFAULT 0, + ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + ADD COLUMN IF NOT EXISTS rejection_reason TEXT, + ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; + +ALTER TABLE developer_profiles + ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS location VARCHAR(255), + ADD COLUMN IF NOT EXISTS tech_stack TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS github_url VARCHAR(500), + ADD COLUMN IF NOT EXISTS portfolio_url VARCHAR(500), + ADD COLUMN IF NOT EXISTS availability VARCHAR(50) DEFAULT 'FULL_TIME', + ADD COLUMN IF NOT EXISTS hourly_rate_inr INTEGER DEFAULT 0, + ADD COLUMN IF NOT EXISTS remote_ok BOOLEAN NOT NULL DEFAULT true, + ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + ADD COLUMN IF NOT EXISTS rejection_reason TEXT, + ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; + +ALTER TABLE video_editor_profiles + ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS location VARCHAR(255), + ADD COLUMN IF NOT EXISTS software_skills TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS style_tags TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS turnaround_days INTEGER DEFAULT 7, + ADD COLUMN IF NOT EXISTS reel_url VARCHAR(500), + ADD COLUMN IF NOT EXISTS starting_price_inr INTEGER DEFAULT 0, + ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + ADD COLUMN IF NOT EXISTS rejection_reason TEXT, + ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; + +ALTER TABLE graphic_designer_profiles + ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS location VARCHAR(255), + ADD COLUMN IF NOT EXISTS design_tools TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS style_tags TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS brand_experience BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS portfolio_url VARCHAR(500), + ADD COLUMN IF NOT EXISTS starting_price_inr INTEGER DEFAULT 0, + ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + ADD COLUMN IF NOT EXISTS rejection_reason TEXT, + ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; + +ALTER TABLE social_media_manager_profiles + ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS location VARCHAR(255), + ADD COLUMN IF NOT EXISTS platforms TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS industries TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS content_types TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS avg_follower_growth_pct INTEGER DEFAULT 0, + ADD COLUMN IF NOT EXISTS starting_price_inr INTEGER DEFAULT 0, + ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + ADD COLUMN IF NOT EXISTS rejection_reason TEXT, + ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; + +ALTER TABLE fitness_trainer_profiles + ADD COLUMN IF NOT EXISTS display_name VARCHAR(255) NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS location VARCHAR(255), + ADD COLUMN IF NOT EXISTS disciplines TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS certifications TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS online_sessions BOOLEAN NOT NULL DEFAULT true, + ADD COLUMN IF NOT EXISTS home_visits BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS gym_based BOOLEAN NOT NULL DEFAULT false, + ADD COLUMN IF NOT EXISTS per_session_rate_inr INTEGER DEFAULT 0, + ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + ADD COLUMN IF NOT EXISTS rejection_reason TEXT, + ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; + +ALTER TABLE catering_service_profiles + ADD COLUMN IF NOT EXISTS business_name VARCHAR(255) NOT NULL DEFAULT '', + ADD COLUMN IF NOT EXISTS location VARCHAR(255), + ADD COLUMN IF NOT EXISTS cuisine_types TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS event_types TEXT[] DEFAULT '{}', + ADD COLUMN IF NOT EXISTS min_guests INTEGER DEFAULT 10, + ADD COLUMN IF NOT EXISTS max_guests INTEGER DEFAULT 500, + ADD COLUMN IF NOT EXISTS has_setup_team BOOLEAN NOT NULL DEFAULT true, + ADD COLUMN IF NOT EXISTS has_serving_staff BOOLEAN NOT NULL DEFAULT true, + ADD COLUMN IF NOT EXISTS price_per_head_inr INTEGER DEFAULT 0, + ADD COLUMN IF NOT EXISTS status VARCHAR(50) NOT NULL DEFAULT 'PENDING', + ADD COLUMN IF NOT EXISTS rejection_reason TEXT, + ADD COLUMN IF NOT EXISTS approved_at TIMESTAMPTZ; + +ALTER TABLE lead_requests + ADD COLUMN IF NOT EXISTS updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(); -- Indexes CREATE INDEX IF NOT EXISTS idx_photographer_profiles_status ON photographer_profiles(status); CREATE INDEX IF NOT EXISTS idx_tutor_profiles_status ON tutor_profiles(status); diff --git a/crates/db/migrations/20260317202000_reviews.up.sql b/crates/db/migrations/20260317202000_reviews.up.sql index 034f3c9..971ac82 100644 --- a/crates/db/migrations/20260317202000_reviews.up.sql +++ b/crates/db/migrations/20260317202000_reviews.up.sql @@ -2,7 +2,7 @@ CREATE TABLE IF NOT EXISTS reviews ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), lead_request_id UUID NOT NULL REFERENCES lead_requests(id) ON DELETE CASCADE UNIQUE, - customer_id UUID NOT NULL REFERENCES 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, rating SMALLINT NOT NULL CHECK (rating >= 1 AND rating <= 5), comment TEXT, diff --git a/crates/db/src/models/catering_service.rs b/crates/db/src/models/catering_service.rs index f5f9eed..fd37afc 100644 --- a/crates/db/src/models/catering_service.rs +++ b/crates/db/src/models/catering_service.rs @@ -33,7 +33,7 @@ impl CateringServiceRepository { sqlx::query_as!( CateringServiceProfile, r#"SELECT id, user_id, business_name, bio, location, - custom_data as "custom_data: Option", + custom_data, status, created_at, updated_at FROM catering_service_profiles WHERE user_id = $1"#, user_id @@ -52,7 +52,7 @@ impl CateringServiceRepository { custom_data = EXCLUDED.custom_data, updated_at = NOW() RETURNING id, user_id, business_name, bio, location, - custom_data as "custom_data: Option", + custom_data, status, created_at, updated_at"#, user_id, p.business_name, p.bio, p.location, p.custom_data ).fetch_one(pool).await diff --git a/crates/db/src/models/config.rs b/crates/db/src/models/config.rs index 6de4537..b9460a4 100644 --- a/crates/db/src/models/config.rs +++ b/crates/db/src/models/config.rs @@ -183,7 +183,7 @@ impl ConfigRepository { r#" UPDATE dashboard_configs 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.audience @@ -198,9 +198,9 @@ impl ConfigRepository { INSERT INTO dashboard_configs (role_id, audience, config_json, version, is_active) VALUES ( $1, - $2, + $2::text, $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 ) RETURNING id, role_id, audience, config_json, version, is_active, updated_at diff --git a/crates/db/src/models/customer.rs b/crates/db/src/models/customer.rs index 4568fb5..2c8ef4c 100644 --- a/crates/db/src/models/customer.rs +++ b/crates/db/src/models/customer.rs @@ -11,7 +11,7 @@ pub struct CustomerProfile { pub phone: Option, pub city: Option, pub area: Option, - pub preferred_professions: Vec, + pub preferred_professions: Option>, pub active_requirement_count: i32, pub status: String, pub bio: Option, diff --git a/crates/db/src/models/developer.rs b/crates/db/src/models/developer.rs index baee1d4..054c570 100644 --- a/crates/db/src/models/developer.rs +++ b/crates/db/src/models/developer.rs @@ -31,7 +31,7 @@ impl DeveloperRepository { sqlx::query_as!( DeveloperProfile, r#"SELECT id, user_id, display_name, bio, location, - custom_data as "custom_data: Option", + custom_data, status, created_at, updated_at FROM developer_profiles WHERE user_id = $1"#, user_id @@ -50,7 +50,7 @@ impl DeveloperRepository { custom_data = EXCLUDED.custom_data, updated_at = NOW() RETURNING id, user_id, display_name, bio, location, - custom_data as "custom_data: Option", + custom_data, status, created_at, updated_at"#, user_id, p.display_name, p.bio, p.location, p.custom_data ).fetch_one(pool).await diff --git a/crates/db/src/models/fitness_trainer.rs b/crates/db/src/models/fitness_trainer.rs index f878dec..f57b99e 100644 --- a/crates/db/src/models/fitness_trainer.rs +++ b/crates/db/src/models/fitness_trainer.rs @@ -31,7 +31,7 @@ impl FitnessTrainerRepository { sqlx::query_as!( FitnessTrainerProfile, r#"SELECT id, user_id, display_name, bio, location, - custom_data as "custom_data: Option", + custom_data, status, created_at, updated_at FROM fitness_trainer_profiles WHERE user_id = $1"#, user_id @@ -50,7 +50,7 @@ impl FitnessTrainerRepository { custom_data = EXCLUDED.custom_data, updated_at = NOW() RETURNING id, user_id, display_name, bio, location, - custom_data as "custom_data: Option", + custom_data, status, created_at, updated_at"#, user_id, p.display_name, p.bio, p.location, p.custom_data ).fetch_one(pool).await diff --git a/crates/db/src/models/graphic_designer.rs b/crates/db/src/models/graphic_designer.rs index 26529e6..0fb1712 100644 --- a/crates/db/src/models/graphic_designer.rs +++ b/crates/db/src/models/graphic_designer.rs @@ -31,7 +31,7 @@ impl GraphicDesignerRepository { sqlx::query_as!( GraphicDesignerProfile, r#"SELECT id, user_id, display_name, bio, location, - custom_data as "custom_data: Option", + custom_data, status, created_at, updated_at FROM graphic_designer_profiles WHERE user_id = $1"#, user_id @@ -50,7 +50,7 @@ impl GraphicDesignerRepository { custom_data = EXCLUDED.custom_data, updated_at = NOW() RETURNING id, user_id, display_name, bio, location, - custom_data as "custom_data: Option", + custom_data, status, created_at, updated_at"#, user_id, p.display_name, p.bio, p.location, p.custom_data ).fetch_one(pool).await diff --git a/crates/db/src/models/job.rs b/crates/db/src/models/job.rs index 61ec62a..a23a978 100644 --- a/crates/db/src/models/job.rs +++ b/crates/db/src/models/job.rs @@ -15,7 +15,7 @@ pub struct Job { pub salary_min: Option, pub salary_max: Option, pub experience_years: Option, - pub skills: Vec, + pub skills: Option>, pub status: String, // DRAFT, PENDING_APPROVAL, LIVE, EXPIRED, CLOSED, REJECTED pub rejection_reason: Option, pub expires_at: Option>, diff --git a/crates/db/src/models/job_seeker.rs b/crates/db/src/models/job_seeker.rs index a8ff34b..715fd7c 100644 --- a/crates/db/src/models/job_seeker.rs +++ b/crates/db/src/models/job_seeker.rs @@ -10,8 +10,8 @@ pub struct JobSeekerProfile { pub full_name: Option, pub location: Option, pub summary: Option, - pub experience_years: i32, - pub skills: Vec, + pub experience_years: Option, + pub skills: Option>, pub resume_url: Option, pub active_application_count: i32, pub status: String, diff --git a/crates/db/src/models/lead_request.rs b/crates/db/src/models/lead_request.rs index 8649fa5..c033d7b 100644 --- a/crates/db/src/models/lead_request.rs +++ b/crates/db/src/models/lead_request.rs @@ -13,6 +13,8 @@ pub struct LeadRequest { pub expires_at: DateTime, pub requested_at: DateTime, pub resolved_at: Option>, + pub professional_user_id: Option, + pub updated_at: DateTime, } #[derive(Debug, Serialize, Deserialize)] diff --git a/crates/db/src/models/makeup_artist.rs b/crates/db/src/models/makeup_artist.rs index 0521f0e..10724cc 100644 --- a/crates/db/src/models/makeup_artist.rs +++ b/crates/db/src/models/makeup_artist.rs @@ -31,7 +31,7 @@ impl MakeupArtistRepository { sqlx::query_as!( MakeupArtistProfile, r#"SELECT id, user_id, display_name, bio, location, - custom_data as "custom_data: Option", + custom_data, status, created_at, updated_at FROM makeup_artist_profiles WHERE user_id = $1"#, user_id @@ -50,7 +50,7 @@ impl MakeupArtistRepository { custom_data = EXCLUDED.custom_data, updated_at = NOW() RETURNING id, user_id, display_name, bio, location, - custom_data as "custom_data: Option", + custom_data, status, created_at, updated_at"#, user_id, p.display_name, p.bio, p.location, p.custom_data ).fetch_one(pool).await diff --git a/crates/db/src/models/photographer.rs b/crates/db/src/models/photographer.rs index ed78b24..c5caee0 100644 --- a/crates/db/src/models/photographer.rs +++ b/crates/db/src/models/photographer.rs @@ -31,7 +31,7 @@ impl PhotographerRepository { sqlx::query_as!( PhotographerProfile, r#"SELECT id, user_id, display_name, bio, location, - custom_data as "custom_data: Option", + custom_data, status, created_at, updated_at FROM photographer_profiles WHERE user_id = $1"#, user_id @@ -50,7 +50,7 @@ impl PhotographerRepository { custom_data = EXCLUDED.custom_data, updated_at = NOW() RETURNING id, user_id, display_name, bio, location, - custom_data as "custom_data: Option", + custom_data, status, created_at, updated_at"#, user_id, p.display_name, p.bio, p.location, p.custom_data ).fetch_one(pool).await diff --git a/crates/db/src/models/professional.rs b/crates/db/src/models/professional.rs index fc275d8..c594720 100644 --- a/crates/db/src/models/professional.rs +++ b/crates/db/src/models/professional.rs @@ -28,6 +28,8 @@ pub struct PortfolioItem { pub tags: Option>, pub created_at: DateTime, pub updated_at: DateTime, + pub user_id: Option, + pub profession_key: Option, } #[derive(Debug, Serialize, Deserialize, FromRow)] @@ -41,6 +43,8 @@ pub struct Service { pub is_active: bool, pub created_at: DateTime, pub updated_at: DateTime, + pub user_id: Option, + pub profession_key: Option, } #[derive(Debug, Serialize, Deserialize, FromRow)] diff --git a/crates/db/src/models/social_media_manager.rs b/crates/db/src/models/social_media_manager.rs index 78b14d7..b5e2f9d 100644 --- a/crates/db/src/models/social_media_manager.rs +++ b/crates/db/src/models/social_media_manager.rs @@ -31,7 +31,7 @@ impl SocialMediaManagerRepository { sqlx::query_as!( SocialMediaManagerProfile, r#"SELECT id, user_id, display_name, bio, location, - custom_data as "custom_data: Option", + custom_data, status, created_at, updated_at FROM social_media_manager_profiles WHERE user_id = $1"#, user_id @@ -50,7 +50,7 @@ impl SocialMediaManagerRepository { custom_data = EXCLUDED.custom_data, updated_at = NOW() RETURNING id, user_id, display_name, bio, location, - custom_data as "custom_data: Option", + custom_data, status, created_at, updated_at"#, user_id, p.display_name, p.bio, p.location, p.custom_data ).fetch_one(pool).await diff --git a/crates/db/src/models/tutor.rs b/crates/db/src/models/tutor.rs index 409bdf4..95ca445 100644 --- a/crates/db/src/models/tutor.rs +++ b/crates/db/src/models/tutor.rs @@ -31,7 +31,7 @@ impl TutorRepository { sqlx::query_as!( TutorProfile, r#"SELECT id, user_id, display_name, bio, location, - custom_data as "custom_data: Option", + custom_data, status, created_at, updated_at FROM tutor_profiles WHERE user_id = $1"#, user_id @@ -50,7 +50,7 @@ impl TutorRepository { custom_data = EXCLUDED.custom_data, updated_at = NOW() RETURNING id, user_id, display_name, bio, location, - custom_data as "custom_data: Option", + custom_data, status, created_at, updated_at"#, user_id, p.display_name, p.bio, p.location, p.custom_data ).fetch_one(pool).await diff --git a/crates/db/src/models/video_editor.rs b/crates/db/src/models/video_editor.rs index 94ba36c..aa73d5a 100644 --- a/crates/db/src/models/video_editor.rs +++ b/crates/db/src/models/video_editor.rs @@ -31,7 +31,7 @@ impl VideoEditorRepository { sqlx::query_as!( VideoEditorProfile, r#"SELECT id, user_id, display_name, bio, location, - custom_data as "custom_data: Option", + custom_data, status, created_at, updated_at FROM video_editor_profiles WHERE user_id = $1"#, user_id @@ -50,7 +50,7 @@ impl VideoEditorRepository { custom_data = EXCLUDED.custom_data, updated_at = NOW() RETURNING id, user_id, display_name, bio, location, - custom_data as "custom_data: Option", + custom_data, status, created_at, updated_at"#, user_id, p.display_name, p.bio, p.location, p.custom_data ).fetch_one(pool).await