From 5f1b48d27be44714b87f3018f3db0ea121e5f450 Mon Sep 17 00:00:00 2001 From: Tracewebstudio Dev Date: Wed, 29 Jul 2026 15:14:37 +0200 Subject: [PATCH] fix: sync profile approval unlock state --- apps/users/src/handlers/approvals.rs | 231 ++++++++++++++++++++++++++- apps/users/src/handlers/profile.rs | 35 +++- 2 files changed, 260 insertions(+), 6 deletions(-) diff --git a/apps/users/src/handlers/approvals.rs b/apps/users/src/handlers/approvals.rs index 1e354e3..edb4323 100644 --- a/apps/users/src/handlers/approvals.rs +++ b/apps/users/src/handlers/approvals.rs @@ -19,6 +19,14 @@ use uuid::Uuid; use super::role_meta::{role_key_to_display, role_to_table}; +#[derive(Default)] +struct RoleProfileSnapshot { + display_name: Option, + email: Option, + phone: Option, + location: Option, +} + pub fn router() -> Router { Router::new() .route("/", get(list_pending)) @@ -258,6 +266,8 @@ async fn activate_profile_after_final_approval( .await .ok(); + sync_user_role_profile_after_final_decision(state, user_id, &role_key, "APPROVED", None).await?; + Ok(()) } @@ -285,11 +295,19 @@ async fn reject_profile_after_final_approval( None => return Ok(()), }; - let query = format!( - "UPDATE {} SET status = 'REJECTED', updated_at = NOW() WHERE id = $1", - table - ); - sqlx::query(&query).bind(user_role_profile_id).execute(&state.pool).await?; + if role_key == "COMPANY" || role_key == "JOB_SEEKER" || role_key == "CUSTOMER" { + let query = format!( + "UPDATE {} SET status = 'REJECTED', updated_at = NOW() WHERE user_id = $1", + table + ); + sqlx::query(&query).bind(user_id).execute(&state.pool).await?; + } else { + let query = format!( + "UPDATE {} SET status = 'REJECTED', updated_at = NOW() WHERE user_role_profile_id = $1", + table + ); + sqlx::query(&query).bind(user_role_profile_id).execute(&state.pool).await?; + } if let Ok(user) = UserRepository::get_by_id(&state.pool, user_id).await { let display = role_key_to_display(&role_key); @@ -319,6 +337,209 @@ async fn reject_profile_after_final_approval( .await .ok(); + sync_user_role_profile_after_final_decision( + state, + user_id, + &role_key, + "REJECTED", + reason, + ) + .await?; + + Ok(()) +} + +async fn load_role_profile_snapshot( + state: &AppState, + user_id: Uuid, + role_key: &str, +) -> Result { + use sqlx::Row; + + let user = UserRepository::get_by_id(&state.pool, user_id).await.ok(); + let fallback_name = user.as_ref().map(|u| { + let full = format!( + "{} {}", + u.first_name.clone().unwrap_or_default(), + u.last_name.clone().unwrap_or_default() + ) + .trim() + .to_string(); + if full.is_empty() { None } else { Some(full) } + }).flatten(); + let fallback_email = user.as_ref().map(|u| u.email.clone()); + + if role_key == "COMPANY" { + let row = sqlx::query( + r#" + SELECT company_name, contact_email, contact_phone, city + FROM company_profiles + WHERE user_id = $1 + "#, + ) + .bind(user_id) + .fetch_optional(&state.pool) + .await?; + + if let Some(row) = row { + return Ok(RoleProfileSnapshot { + display_name: row.try_get("company_name").ok().flatten().or(fallback_name), + email: row.try_get("contact_email").ok().flatten().or(fallback_email), + phone: row.try_get("contact_phone").ok().flatten(), + location: row.try_get("city").ok().flatten(), + }); + } + } + + let mut custom_data = serde_json::Value::Null; + + if role_key == "JOB_SEEKER" || role_key == "CUSTOMER" { + let table = if role_key == "JOB_SEEKER" { + "job_seeker_profiles" + } else { + "customer_profiles" + }; + let row = sqlx::query(&format!( + r#"SELECT custom_data FROM {} WHERE user_id = $1"#, + table + )) + .bind(user_id) + .fetch_optional(&state.pool) + .await?; + + if let Some(row) = row { + custom_data = row + .try_get::("custom_data") + .unwrap_or(serde_json::Value::Null) + .get("basic_info") + .cloned() + .unwrap_or(serde_json::Value::Null); + } + } else if let Some(table) = role_to_table(role_key) { + let urp_id = sqlx::query_scalar::<_, Uuid>( + "SELECT id FROM user_role_profiles WHERE user_id = $1 AND role_key = $2", + ) + .bind(user_id) + .bind(role_key) + .fetch_optional(&state.pool) + .await?; + + if let Some(urp_id) = urp_id { + let row = sqlx::query(&format!( + r#"SELECT custom_data FROM {} WHERE user_role_profile_id = $1"#, + table + )) + .bind(urp_id) + .fetch_optional(&state.pool) + .await?; + + if let Some(row) = row { + custom_data = row + .try_get::("custom_data") + .unwrap_or(serde_json::Value::Null); + } + } + } + + let first_name = custom_data.get("first_name").and_then(|v| v.as_str()).map(str::trim).filter(|v| !v.is_empty()); + let last_name = custom_data.get("last_name").and_then(|v| v.as_str()).map(str::trim).filter(|v| !v.is_empty()); + let display_name = match (first_name, last_name) { + (Some(first), Some(last)) => Some(format!("{} {}", first, last)), + (Some(first), None) => Some(first.to_string()), + (None, Some(last)) => Some(last.to_string()), + (None, None) => custom_data + .get("business_name") + .or_else(|| custom_data.get("owner_name")) + .or_else(|| custom_data.get("company_name")) + .and_then(|v| v.as_str()) + .map(str::trim) + .filter(|v| !v.is_empty()) + .map(String::from) + .or(fallback_name), + }; + + Ok(RoleProfileSnapshot { + display_name, + email: custom_data + .get("email") + .or_else(|| custom_data.get("company_email")) + .and_then(|v| v.as_str()) + .map(str::trim) + .filter(|v| !v.is_empty()) + .map(String::from) + .or(fallback_email), + phone: custom_data + .get("phone") + .or_else(|| custom_data.get("company_phone")) + .and_then(|v| v.as_str()) + .map(str::trim) + .filter(|v| !v.is_empty()) + .map(String::from), + location: custom_data + .get("location") + .or_else(|| custom_data.get("city")) + .and_then(|v| v.as_str()) + .map(str::trim) + .filter(|v| !v.is_empty()) + .map(String::from), + }) +} + +async fn sync_user_role_profile_after_final_decision( + state: &AppState, + user_id: Uuid, + role_key: &str, + final_status: &str, + rejection_reason: Option<&str>, +) -> Result<(), sqlx::Error> { + let snapshot = load_role_profile_snapshot(state, user_id, role_key).await.unwrap_or_default(); + let approval_status = if final_status == "APPROVED" { "APPROVED" } else { "REJECTED" }; + + sqlx::query( + r#" + INSERT INTO user_role_profiles ( + user_id, role_key, display_name, email, phone, location, status, + verification_status, approval_status, rejection_reason, approved_at, verified_at + ) + VALUES ( + $1, $2, $3, $4, $5, $6, $7, + $8, $9, $10, + CASE WHEN $9 = 'APPROVED' THEN NOW() ELSE NULL END, + CASE WHEN $8 = 'APPROVED' THEN NOW() ELSE NULL END + ) + ON CONFLICT (user_id, role_key) DO UPDATE SET + display_name = COALESCE(EXCLUDED.display_name, user_role_profiles.display_name), + email = COALESCE(EXCLUDED.email, user_role_profiles.email), + phone = COALESCE(EXCLUDED.phone, user_role_profiles.phone), + location = COALESCE(EXCLUDED.location, user_role_profiles.location), + status = EXCLUDED.status, + verification_status = EXCLUDED.verification_status, + approval_status = EXCLUDED.approval_status, + rejection_reason = EXCLUDED.rejection_reason, + approved_at = CASE + WHEN EXCLUDED.approval_status = 'APPROVED' THEN NOW() + ELSE user_role_profiles.approved_at + END, + verified_at = CASE + WHEN EXCLUDED.verification_status = 'APPROVED' THEN NOW() + ELSE user_role_profiles.verified_at + END, + updated_at = NOW() + "#, + ) + .bind(user_id) + .bind(role_key) + .bind(snapshot.display_name) + .bind(snapshot.email) + .bind(snapshot.phone) + .bind(snapshot.location) + .bind(final_status) + .bind(final_status) + .bind(approval_status) + .bind(rejection_reason) + .execute(&state.pool) + .await?; + Ok(()) } diff --git a/apps/users/src/handlers/profile.rs b/apps/users/src/handlers/profile.rs index 970da9b..62b4ed2 100644 --- a/apps/users/src/handlers/profile.rs +++ b/apps/users/src/handlers/profile.rs @@ -849,7 +849,7 @@ pub async fn verification_status( use sqlx::Row; let id: Uuid = r.try_get("id").unwrap_or(Uuid::nil()); let reference_number: String = r.try_get("reference_number").unwrap_or_default(); - let status: String = r.try_get("status").unwrap_or_default(); + let raw_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> = @@ -858,6 +858,39 @@ pub async fn verification_status( .try_get("documents") .unwrap_or(serde_json::Value::Array(vec![])); + let role_assignment_approved = if let Ok(role) = RoleRepository::get_by_key(&state.pool, &role_key).await { + sqlx::query_scalar::<_, String>( + r#" + SELECT status + FROM user_role_assignments + WHERE user_id = $1 AND role_id = $2 + LIMIT 1 + "#, + ) + .bind(auth.user_id) + .bind(role.id) + .fetch_optional(&state.pool) + .await + .ok() + .flatten() + .map(|status| status.eq_ignore_ascii_case("APPROVED")) + .unwrap_or(false) + } else { + false + }; + + let profile_status = fetch_current_profile_state(&state.pool, auth.user_id, &role_key).await.0; + let status = if raw_status.eq_ignore_ascii_case("COMPLETED") + || role_assignment_approved + || profile_status.eq_ignore_ascii_case("APPROVED") + { + "APPROVED".to_string() + } else if raw_status.eq_ignore_ascii_case("FINAL_REJECTED") { + "REJECTED".to_string() + } else { + raw_status + }; + ( StatusCode::OK, Json(serde_json::json!({