fix: verification submission no longer blocks login or misreports status
All checks were successful
build-and-release / build (customers) (push) Successful in 12s
build-and-release / build (catering-services) (push) Successful in 15s
build-and-release / build (developers) (push) Successful in 13s
build-and-release / build (cron) (push) Successful in 17s
build-and-release / build (employees) (push) Successful in 5s
build-and-release / build (gateway) (push) Successful in 5s
build-and-release / build (fitness-trainers) (push) Successful in 6s
build-and-release / build (graphic-designers) (push) Successful in 7s
build-and-release / build (makeup-artists) (push) Successful in 5s
build-and-release / build (jobs) (push) Successful in 8s
build-and-release / build (payments) (push) Successful in 7s
build-and-release / build (photographers) (push) Successful in 7s
build-and-release / build (social-media-managers) (push) Successful in 5s
build-and-release / build (tutors) (push) Successful in 6s
build-and-release / build (ugc-content-creators) (push) Successful in 5s
build-and-release / build (video-editors) (push) Successful in 4s
build-and-release / build (companies) (push) Successful in 1m10s
build-and-release / build (job-seekers) (push) Successful in 1m8s
build-and-release / build (users) (push) Successful in 3m7s
All checks were successful
build-and-release / build (customers) (push) Successful in 12s
build-and-release / build (catering-services) (push) Successful in 15s
build-and-release / build (developers) (push) Successful in 13s
build-and-release / build (cron) (push) Successful in 17s
build-and-release / build (employees) (push) Successful in 5s
build-and-release / build (gateway) (push) Successful in 5s
build-and-release / build (fitness-trainers) (push) Successful in 6s
build-and-release / build (graphic-designers) (push) Successful in 7s
build-and-release / build (makeup-artists) (push) Successful in 5s
build-and-release / build (jobs) (push) Successful in 8s
build-and-release / build (payments) (push) Successful in 7s
build-and-release / build (photographers) (push) Successful in 7s
build-and-release / build (social-media-managers) (push) Successful in 5s
build-and-release / build (tutors) (push) Successful in 6s
build-and-release / build (ugc-content-creators) (push) Successful in 5s
build-and-release / build (video-editors) (push) Successful in 4s
build-and-release / build (companies) (push) Successful in 1m10s
build-and-release / build (job-seekers) (push) Successful in 1m8s
build-and-release / build (users) (push) Successful in 3m7s
- submit_for_verification: stop downgrading user_role_assignments.status to PENDING. This was locking users out of login while their verification was under review. Verification progress is tracked in the verifications table; role assignment stays APPROVED throughout. - verification_status: remove role_assignment_approved from the status calculation. Because the assignment now stays APPROVED, using it as a proxy was incorrectly overriding PENDING verification status to APPROVED immediately after submission. - companies submit_with_documents: encode document_type from filename prefix before '|' separator (set by frontend) rather than file stem; add duplicate-verification guard to match users service. - job_seekers: add get_or_create_job_seeker_profile helper to ensure profile row exists before upsert operations. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
5f1b48d27b
commit
2f47308498
3 changed files with 66 additions and 58 deletions
|
|
@ -869,11 +869,14 @@ async fn submit_with_documents(
|
|||
|
||||
// storage_backend is always "b2" (set at top of function).
|
||||
|
||||
// Derive a stable document_type from the original filename (stem) so the admin UI can group.
|
||||
// The frontend encodes the document type as the part before the first '|'
|
||||
// in the filename (e.g. "registration_doc|scan001.pdf" → "registration_doc").
|
||||
// Splitting on '|' is unambiguous because doc-type keys never contain '|'.
|
||||
let document_type = if !original_filename.is_empty() {
|
||||
std::path::Path::new(&original_filename)
|
||||
.file_stem()
|
||||
.and_then(|s| s.to_str())
|
||||
original_filename
|
||||
.splitn(2, '|')
|
||||
.next()
|
||||
.filter(|s| !s.is_empty())
|
||||
.unwrap_or("document")
|
||||
.to_string()
|
||||
} else {
|
||||
|
|
@ -1061,6 +1064,29 @@ async fn submit_with_documents(
|
|||
}
|
||||
|
||||
// ---- 4. Create the verification record. ----
|
||||
// Guard: reject if an active verification already exists (mirrors users/src/handlers/profile.rs).
|
||||
let existing: Result<Option<Uuid>, sqlx::Error> = sqlx::query_scalar(
|
||||
r#"
|
||||
SELECT id FROM verifications
|
||||
WHERE user_id = $1 AND role_key = 'COMPANY'
|
||||
AND status IN ('PENDING', 'UNDER_REVIEW', 'DOCUMENTS_REQUESTED', 'REVISION_REQUESTED')
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
.bind(auth.user_id)
|
||||
.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. Please wait for it to be reviewed."
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
// Signature: VerificationRepository::create(pool, user_id, role_key, case_type, priority, payload, documents)
|
||||
// (see crates/db/src/models/verification.rs:38). The DB column is `case_type`; the user service
|
||||
// populates it with "PROFILE_VERIFICATION" — we do the same.
|
||||
|
|
|
|||
|
|
@ -58,6 +58,31 @@ pub struct PaginationQuery {
|
|||
|
||||
// ── Handlers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
async fn get_or_create_job_seeker_profile(
|
||||
pool: &sqlx::PgPool,
|
||||
user_id: Uuid,
|
||||
) -> Result<db::models::job_seeker::JobSeekerProfile, sqlx::Error> {
|
||||
if let Some(profile) = JobSeekerRepository::get_by_user_id(pool, user_id).await? {
|
||||
return Ok(profile);
|
||||
}
|
||||
|
||||
JobSeekerRepository::upsert(
|
||||
pool,
|
||||
user_id,
|
||||
UpsertJobSeekerProfilePayload {
|
||||
full_name: None,
|
||||
location: None,
|
||||
summary: None,
|
||||
experience_years: None,
|
||||
skills: None,
|
||||
resume_url: None,
|
||||
bio: None,
|
||||
custom_data: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn get_profile(
|
||||
State(state): State<AppState>,
|
||||
auth: AuthUser,
|
||||
|
|
@ -73,13 +98,12 @@ async fn get_profile(
|
|||
}
|
||||
}
|
||||
|
||||
match JobSeekerRepository::get_by_user_id(&state.pool, auth.user_id).await {
|
||||
Ok(Some(profile)) => {
|
||||
match get_or_create_job_seeker_profile(&state.pool, auth.user_id).await {
|
||||
Ok(profile) => {
|
||||
// Cache for 5 minutes
|
||||
let _: Result<(), _> = redis.set_ex(&cache_key, &serde_json::to_string(&profile).unwrap_or_default(), 300).await;
|
||||
(StatusCode::OK, Json(profile)).into_response()
|
||||
}
|
||||
Ok(None) => (StatusCode::NOT_FOUND, "Job seeker profile not found").into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
|
@ -681,24 +705,8 @@ async fn upload_document(
|
|||
// A job seeker who hasn't saved any basic profile info yet won't have a
|
||||
// job_seeker_profiles row - lazily create a blank one instead of 404ing,
|
||||
// since uploading a verification document doesn't depend on that data.
|
||||
let seeker = match JobSeekerRepository::get_by_user_id(&state.pool, auth.user_id).await {
|
||||
Ok(Some(s)) => s,
|
||||
Ok(None) => {
|
||||
let empty_payload = UpsertJobSeekerProfilePayload {
|
||||
full_name: None,
|
||||
location: None,
|
||||
summary: None,
|
||||
experience_years: None,
|
||||
skills: None,
|
||||
resume_url: None,
|
||||
bio: None,
|
||||
custom_data: None,
|
||||
};
|
||||
match JobSeekerRepository::upsert(&state.pool, auth.user_id, empty_payload).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))).into_response(),
|
||||
}
|
||||
}
|
||||
let seeker = match get_or_create_job_seeker_profile(&state.pool, auth.user_id).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))).into_response(),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -742,17 +742,10 @@ async fn submit_for_verification(
|
|||
// 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_role_assignments SET status = 'PENDING' WHERE user_id = $1 AND role_id = $2",
|
||||
)
|
||||
.bind(auth.user_id)
|
||||
.bind(role.id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
// NOTE: we intentionally do NOT change user_role_assignments.status here.
|
||||
// Verification status is tracked in the verifications table. Downgrading
|
||||
// the role assignment to PENDING would block the user from logging in
|
||||
// while their verification is under review, which is broken UX.
|
||||
|
||||
// Create verification record — appears in admin Verification Management
|
||||
match VerificationRepository::create(
|
||||
|
|
@ -858,30 +851,11 @@ 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;
|
||||
// Use the verification record's status as the source of truth.
|
||||
// role_assignment_approved is NOT used here — keeping the role APPROVED
|
||||
// so users can log in while under review means it cannot drive this check.
|
||||
let status = if raw_status.eq_ignore_ascii_case("COMPLETED")
|
||||
|| role_assignment_approved
|
||||
|| profile_status.eq_ignore_ascii_case("APPROVED")
|
||||
{
|
||||
"APPROVED".to_string()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue