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
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:
parent
2da1ebc8be
commit
5f1b48d27b
2 changed files with 260 additions and 6 deletions
|
|
@ -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(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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!({
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue