use crate::AppState; use axum::{ extract::{Query, State}, http::StatusCode, response::IntoResponse, routing::{get, post}, Json, Router, }; use contracts::auth_middleware::AuthUser; use db::models::{role::RoleRepository, verification::VerificationRepository}; use serde::Deserialize; use uuid::Uuid; // ── Routers ─────────────────────────────────────────────────────────────────── pub fn router() -> Router { Router::new() .route("/", get(get_profile).patch(save_profile)) .route("/submit-for-verification", post(submit_for_verification)) } pub fn me_verification_router() -> Router { Router::new() .route("/verification-status", get(verification_status)) } // ── DTOs ────────────────────────────────────────────────────────────────────── #[derive(Deserialize)] pub struct RoleKeyQuery { #[serde(rename = "roleKey", alias = "role_key")] pub role_key: Option, } #[derive(Deserialize)] pub struct SaveProfileInput { #[serde(rename = "roleKey", alias = "role_key")] pub role_key: String, pub profile_data: serde_json::Value, } #[derive(Deserialize)] pub struct SubmitInput { #[serde(rename = "roleKey", alias = "role_key")] pub role_key: String, /// Optional: if provided, saves this data before submitting. /// If omitted, reads previously saved profile data from DB. pub profile_data: Option, } // ── Helpers ─────────────────────────────────────────────────────────────────── fn role_to_table(role_key: &str) -> Option<&'static str> { match role_key.to_uppercase().as_str() { "PHOTOGRAPHER" => Some("photographer_profiles"), "MAKEUP_ARTIST" => Some("makeup_artist_profiles"), "TUTOR" => Some("tutor_profiles"), "DEVELOPER" => Some("developer_profiles"), "VIDEO_EDITOR" => Some("video_editor_profiles"), "GRAPHIC_DESIGNER" => Some("graphic_designer_profiles"), "SOCIAL_MEDIA_MANAGER" => Some("social_media_manager_profiles"), "FITNESS_TRAINER" => Some("fitness_trainer_profiles"), "CATERING_SERVICES" => Some("catering_service_profiles"), "UGC_CONTENT_CREATOR" => Some("ugc_content_creator_profiles"), "JOB_SEEKER" | "JOBSEEKER" => Some("job_seeker_profiles"), "CUSTOMER" => Some("customer_profiles"), _ => None, } } fn extract_documents(profile_data: &serde_json::Value) -> serde_json::Value { let doc_keys = [ "aadhar_doc", "registration_doc", "gst_doc", "sample_work", "degree_certificate", "certification_doc", "fssai_license", "identity_proof", "address_proof", "portfolio_proof", ]; let mut docs = vec![]; for key in &doc_keys { if let Some(val) = profile_data.get(key) { if !val.is_null() { docs.push(serde_json::json!({ "type": key, "value": val, "status": "SUBMITTED" })); } } } serde_json::Value::Array(docs) } fn resolve_role_key(auth_role: &str, query_role: Option) -> String { query_role .filter(|k| !k.is_empty()) .unwrap_or_else(|| auth_role.to_string()) .to_uppercase() } // ── Handlers ────────────────────────────────────────────────────────────────── /// GET /api/profile?roleKey=PHOTOGRAPHER async fn get_profile( auth: AuthUser, State(state): State, Query(q): Query, ) -> impl IntoResponse { let role_key = resolve_role_key(&auth.claims.active_role, q.role_key); if role_key == "COMPANY" { let row = sqlx::query( r#"SELECT name, status, "updatedAt" FROM companies WHERE "userId" = $1"#, ) .bind(auth.user_id) .fetch_optional(&state.pool) .await; return match row { Ok(Some(r)) => { use sqlx::Row; let name: Option = r.try_get("name").ok(); let status: String = r.try_get("status").unwrap_or_default(); ( StatusCode::OK, Json(serde_json::json!({ "role_key": role_key, "profile_data": { "company_name": name }, "verification_status": status, })), ) .into_response() } Ok(None) => ( StatusCode::OK, Json(serde_json::json!({ "role_key": role_key, "profile_data": null, "verification_status": "NOT_STARTED", })), ) .into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), }; } let table = match role_to_table(&role_key) { Some(t) => t, None => { return ( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": format!("Unknown role: {}", role_key) })), ) .into_response() } }; let query = format!( r#"SELECT "profileData", verification_status FROM {} WHERE user_id = $1"#, table ); match sqlx::query(&query) .bind(auth.user_id) .fetch_optional(&state.pool) .await { Ok(Some(row)) => { use sqlx::Row; let profile_data: serde_json::Value = row .try_get("profileData") .unwrap_or(serde_json::Value::Null); let verification_status: String = row.try_get("verification_status").unwrap_or_default(); ( StatusCode::OK, Json(serde_json::json!({ "role_key": role_key, "profile_data": profile_data, "verification_status": verification_status, })), ) .into_response() } Ok(None) => ( StatusCode::OK, Json(serde_json::json!({ "role_key": role_key, "profile_data": null, "verification_status": "NOT_STARTED", })), ) .into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } /// PATCH /api/profile async fn save_profile( auth: AuthUser, State(state): State, Json(input): Json, ) -> impl IntoResponse { let role_key = input.role_key.to_uppercase(); if role_key == "COMPANY" { let name = input .profile_data .get("company_name") .and_then(|v| v.as_str()) .unwrap_or_default() .to_string(); return match sqlx::query( r#" INSERT INTO companies ("userId", name, status, "updatedAt") VALUES ($1, $2, 'DRAFT', NOW()) ON CONFLICT ("userId") DO UPDATE SET name = EXCLUDED.name, "updatedAt" = NOW() "#, ) .bind(auth.user_id) .bind(&name) .execute(&state.pool) .await { Ok(_) => ( StatusCode::OK, Json(serde_json::json!({ "saved": true, "role_key": role_key })), ) .into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), }; } let table = match role_to_table(&role_key) { Some(t) => t, None => { return ( StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": format!("Unknown role: {}", role_key) })), ) .into_response() } }; let query = format!( r#" INSERT INTO {table} (user_id, "profileData", verification_status, updated_at) VALUES ($1, $2, 'DRAFT', NOW()) ON CONFLICT (user_id) DO UPDATE SET "profileData" = EXCLUDED."profileData", updated_at = NOW() "# ); match sqlx::query(&query) .bind(auth.user_id) .bind(&input.profile_data) .execute(&state.pool) .await { Ok(_) => ( StatusCode::OK, Json(serde_json::json!({ "saved": true, "role_key": role_key })), ) .into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } /// POST /api/profile/submit-for-verification async fn submit_for_verification( auth: AuthUser, State(state): State, Json(input): Json, ) -> impl IntoResponse { let role_key = input.role_key.to_uppercase(); // Guard: reject if an active verification already exists let existing: Result, sqlx::Error> = sqlx::query_scalar( r#" SELECT id FROM verifications WHERE user_id = $1 AND role_key = $2 AND status IN ('PENDING', 'UNDER_REVIEW', 'DOCUMENTS_REQUESTED', 'REVISION_REQUESTED') LIMIT 1 "#, ) .bind(auth.user_id) .bind(&role_key) .fetch_optional(&state.pool) .await; if existing.unwrap_or(None).is_some() { return ( StatusCode::CONFLICT, Json(serde_json::json!({ "error": "A verification is already in progress for this role. Please wait for it to be reviewed." })), ) .into_response(); } // Fetch saved profile data or use submitted data let profile_data = match input.profile_data { Some(data) => data, None => fetch_saved_profile(&state, auth.user_id, &role_key).await, }; let documents = extract_documents(&profile_data); // Mark profile as PENDING in role-specific table set_profile_status(&state, auth.user_id, &role_key, "PENDING").await; // Mark user_role as PENDING if let Ok(role) = RoleRepository::get_by_key(&state.pool, &role_key).await { sqlx::query( "UPDATE user_roles SET status = 'PENDING' WHERE user_id = $1 AND role_id = $2", ) .bind(auth.user_id) .bind(role.id) .execute(&state.pool) .await .ok(); } // Create verification record — appears in admin Verification Management match VerificationRepository::create( &state.pool, auth.user_id, &role_key, "PROFILE_VERIFICATION", "MEDIUM", profile_data, documents, ) .await { Ok(v) => ( StatusCode::CREATED, Json(serde_json::json!({ "verification_id": v.id, "status": v.status, "message": "Your profile has been submitted for verification. We will notify you once it has been reviewed." })), ) .into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } /// GET /api/me/verification-status?roleKey=PHOTOGRAPHER pub async fn verification_status( auth: AuthUser, State(state): State, Query(q): Query, ) -> impl IntoResponse { let role_key = resolve_role_key(&auth.claims.active_role, q.role_key); let row = sqlx::query( r#" SELECT id, status, notes, rejection_reason, updated_at FROM verifications WHERE user_id = $1 AND role_key = $2 ORDER BY created_at DESC LIMIT 1 "#, ) .bind(auth.user_id) .bind(&role_key) .fetch_optional(&state.pool) .await; match row { Ok(Some(r)) => { use sqlx::Row; let id: Uuid = r.try_get("id").unwrap_or(Uuid::nil()); let status: String = r.try_get("status").unwrap_or_default(); let notes: Option = r.try_get("notes").ok().flatten(); let rejection_reason: Option = r.try_get("rejection_reason").ok().flatten(); let updated_at: Option> = r.try_get("updated_at").ok(); ( StatusCode::OK, Json(serde_json::json!({ "has_verification": true, "verification_id": id, "status": status, "document_request": notes, "rejection_reason": rejection_reason, "updated_at": updated_at, })), ) .into_response() } Ok(None) => ( StatusCode::OK, Json(serde_json::json!({ "has_verification": false, "status": "NOT_SUBMITTED", })), ) .into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } // ── Internal helpers ────────────────────────────────────────────────────────── async fn fetch_saved_profile( state: &AppState, user_id: Uuid, role_key: &str, ) -> serde_json::Value { if role_key == "COMPANY" { return match sqlx::query(r#"SELECT name FROM companies WHERE "userId" = $1"#) .bind(user_id) .fetch_optional(&state.pool) .await { Ok(Some(r)) => { use sqlx::Row; let name: Option = r.try_get("name").ok(); serde_json::json!({ "company_name": name }) } _ => serde_json::Value::Object(Default::default()), }; } if let Some(table) = role_to_table(role_key) { let q = format!(r#"SELECT "profileData" FROM {} WHERE user_id = $1"#, table); if let Ok(Some(row)) = sqlx::query(&q) .bind(user_id) .fetch_optional(&state.pool) .await { use sqlx::Row; return row .try_get::("profileData") .unwrap_or(serde_json::Value::Object(Default::default())); } } serde_json::Value::Object(Default::default()) } async fn set_profile_status(state: &AppState, user_id: Uuid, role_key: &str, status: &str) { if role_key == "COMPANY" { sqlx::query( r#"UPDATE companies SET status = $1, "updatedAt" = NOW() WHERE "userId" = $2"#, ) .bind(status) .bind(user_id) .execute(&state.pool) .await .ok(); return; } if let Some(table) = role_to_table(role_key) { let q = format!( "UPDATE {} SET verification_status = $1, submitted_at = NOW(), updated_at = NOW() WHERE user_id = $2", table ); sqlx::query(&q) .bind(status) .bind(user_id) .execute(&state.pool) .await .ok(); } }