fix: sync profile approval unlock state
All checks were successful
build-and-release / build (developers) (push) Successful in 9s
build-and-release / build (customers) (push) Successful in 13s
build-and-release / build (companies) (push) Successful in 12s
build-and-release / build (cron) (push) Successful in 15s
build-and-release / build (catering-services) (push) Successful in 14s
build-and-release / build (fitness-trainers) (push) Successful in 6s
build-and-release / build (employees) (push) Successful in 15s
build-and-release / build (gateway) (push) Successful in 6s
build-and-release / build (graphic-designers) (push) Successful in 7s
build-and-release / build (job-seekers) (push) Successful in 6s
build-and-release / build (payments) (push) Successful in 7s
build-and-release / build (jobs) (push) Successful in 8s
build-and-release / build (makeup-artists) (push) Successful in 9s
build-and-release / build (photographers) (push) Successful in 6s
build-and-release / build (social-media-managers) (push) Successful in 7s
build-and-release / build (ugc-content-creators) (push) Successful in 6s
build-and-release / build (tutors) (push) Successful in 8s
build-and-release / build (video-editors) (push) Successful in 9s
build-and-release / build (users) (push) Successful in 6m14s

This commit is contained in:
Tracewebstudio Dev 2026-07-29 15:14:37 +02:00
parent 2da1ebc8be
commit 5f1b48d27b
2 changed files with 260 additions and 6 deletions

View file

@ -19,6 +19,14 @@ use uuid::Uuid;
use super::role_meta::{role_key_to_display, role_to_table}; use super::role_meta::{role_key_to_display, role_to_table};
#[derive(Default)]
struct RoleProfileSnapshot {
display_name: Option<String>,
email: Option<String>,
phone: Option<String>,
location: Option<String>,
}
pub fn router() -> Router<AppState> { pub fn router() -> Router<AppState> {
Router::new() Router::new()
.route("/", get(list_pending)) .route("/", get(list_pending))
@ -258,6 +266,8 @@ async fn activate_profile_after_final_approval(
.await .await
.ok(); .ok();
sync_user_role_profile_after_final_decision(state, user_id, &role_key, "APPROVED", None).await?;
Ok(()) Ok(())
} }
@ -285,11 +295,19 @@ async fn reject_profile_after_final_approval(
None => return Ok(()), None => return Ok(()),
}; };
let query = format!( if role_key == "COMPANY" || role_key == "JOB_SEEKER" || role_key == "CUSTOMER" {
"UPDATE {} SET status = 'REJECTED', updated_at = NOW() WHERE id = $1", let query = format!(
table "UPDATE {} SET status = 'REJECTED', updated_at = NOW() WHERE user_id = $1",
); table
sqlx::query(&query).bind(user_role_profile_id).execute(&state.pool).await?; );
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 { if let Ok(user) = UserRepository::get_by_id(&state.pool, user_id).await {
let display = role_key_to_display(&role_key); let display = role_key_to_display(&role_key);
@ -319,6 +337,209 @@ async fn reject_profile_after_final_approval(
.await .await
.ok(); .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<RoleProfileSnapshot, sqlx::Error> {
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::<serde_json::Value, _>("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::<serde_json::Value, _>("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(()) Ok(())
} }

View file

@ -849,7 +849,7 @@ pub async fn verification_status(
use sqlx::Row; use sqlx::Row;
let id: Uuid = r.try_get("id").unwrap_or(Uuid::nil()); let id: Uuid = r.try_get("id").unwrap_or(Uuid::nil());
let reference_number: String = r.try_get("reference_number").unwrap_or_default(); 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<String> = r.try_get("notes").ok().flatten(); let notes: Option<String> = r.try_get("notes").ok().flatten();
let rejection_reason: Option<String> = r.try_get("rejection_reason").ok().flatten(); let rejection_reason: Option<String> = r.try_get("rejection_reason").ok().flatten();
let updated_at: Option<chrono::DateTime<chrono::Utc>> = let updated_at: Option<chrono::DateTime<chrono::Utc>> =
@ -858,6 +858,39 @@ pub async fn verification_status(
.try_get("documents") .try_get("documents")
.unwrap_or(serde_json::Value::Array(vec![])); .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, StatusCode::OK,
Json(serde_json::json!({ Json(serde_json::json!({