2026-04-06 03:39:41 +02:00
|
|
|
use chrono::{DateTime, Utc};
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use sqlx::{FromRow, PgPool};
|
|
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
|
|
|
|
pub struct Verification {
|
|
|
|
|
pub id: Uuid,
|
|
|
|
|
pub user_id: Uuid,
|
|
|
|
|
pub role_key: String,
|
|
|
|
|
pub status: String,
|
|
|
|
|
pub priority: String,
|
|
|
|
|
pub case_type: String,
|
|
|
|
|
pub payload: serde_json::Value,
|
|
|
|
|
pub documents: serde_json::Value,
|
|
|
|
|
pub notes: Option<String>,
|
|
|
|
|
pub rejection_reason: Option<String>,
|
|
|
|
|
pub assigned_to: Option<Uuid>,
|
|
|
|
|
pub created_at: DateTime<Utc>,
|
|
|
|
|
pub updated_at: DateTime<Utc>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
|
|
|
|
pub struct VerificationLog {
|
|
|
|
|
pub id: Uuid,
|
2026-04-15 06:23:27 +02:00
|
|
|
pub verification_request_id: Uuid,
|
2026-04-06 03:39:41 +02:00
|
|
|
pub action: String,
|
2026-04-15 06:23:27 +02:00
|
|
|
pub acted_by_user_id: Option<Uuid>,
|
2026-04-06 03:39:41 +02:00
|
|
|
pub old_status: Option<String>,
|
|
|
|
|
pub new_status: Option<String>,
|
2026-04-15 06:23:27 +02:00
|
|
|
pub remarks: Option<String>,
|
2026-04-06 03:39:41 +02:00
|
|
|
pub created_at: DateTime<Utc>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct VerificationRepository;
|
|
|
|
|
|
|
|
|
|
impl VerificationRepository {
|
|
|
|
|
pub async fn create(
|
|
|
|
|
pool: &PgPool,
|
|
|
|
|
user_id: Uuid,
|
|
|
|
|
role_key: &str,
|
|
|
|
|
case_type: &str,
|
|
|
|
|
priority: &str,
|
|
|
|
|
payload: serde_json::Value,
|
|
|
|
|
documents: serde_json::Value,
|
|
|
|
|
) -> Result<Verification, sqlx::Error> {
|
|
|
|
|
sqlx::query_as::<_, Verification>(
|
|
|
|
|
r#"
|
|
|
|
|
INSERT INTO verifications (user_id, role_key, case_type, priority, payload, documents)
|
|
|
|
|
VALUES ($1, $2, $3, $4, $5, $6)
|
|
|
|
|
RETURNING *
|
|
|
|
|
"#
|
|
|
|
|
)
|
|
|
|
|
.bind(user_id)
|
|
|
|
|
.bind(role_key)
|
|
|
|
|
.bind(case_type)
|
|
|
|
|
.bind(priority)
|
|
|
|
|
.bind(payload)
|
|
|
|
|
.bind(documents)
|
|
|
|
|
.fetch_one(pool)
|
|
|
|
|
.await
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-12 05:51:19 +05:30
|
|
|
pub async fn create_approved(
|
|
|
|
|
pool: &PgPool,
|
|
|
|
|
user_id: Uuid,
|
|
|
|
|
role_key: &str,
|
|
|
|
|
case_type: &str,
|
|
|
|
|
payload: serde_json::Value,
|
|
|
|
|
documents: serde_json::Value,
|
|
|
|
|
) -> Result<Verification, sqlx::Error> {
|
|
|
|
|
sqlx::query_as::<_, Verification>(
|
|
|
|
|
r#"
|
|
|
|
|
INSERT INTO verifications (user_id, role_key, case_type, priority, status, payload, documents, reviewed_at, reviewer_notes)
|
|
|
|
|
VALUES ($1, $2, $3, 'MEDIUM', 'APPROVED', $4, $5, NOW(), 'Auto-approved for demo account')
|
|
|
|
|
RETURNING *
|
|
|
|
|
"#
|
|
|
|
|
)
|
|
|
|
|
.bind(user_id)
|
|
|
|
|
.bind(role_key)
|
|
|
|
|
.bind(case_type)
|
|
|
|
|
.bind(payload)
|
|
|
|
|
.bind(documents)
|
|
|
|
|
.fetch_one(pool)
|
|
|
|
|
.await
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-06 03:39:41 +02:00
|
|
|
pub async fn get_by_id(pool: &PgPool, id: Uuid) -> Result<Option<Verification>, sqlx::Error> {
|
|
|
|
|
sqlx::query_as::<_, Verification>("SELECT * FROM verifications WHERE id = $1")
|
|
|
|
|
.bind(id)
|
|
|
|
|
.fetch_optional(pool)
|
|
|
|
|
.await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn list(
|
|
|
|
|
pool: &PgPool,
|
|
|
|
|
status: Option<&str>,
|
|
|
|
|
case_type: Option<&str>,
|
|
|
|
|
page: i64,
|
|
|
|
|
limit: i64,
|
|
|
|
|
) -> Result<Vec<Verification>, sqlx::Error> {
|
|
|
|
|
let offset = (page - 1) * limit;
|
|
|
|
|
let mut query = "SELECT * FROM verifications WHERE 1=1".to_string();
|
|
|
|
|
|
|
|
|
|
if status.is_some() { query.push_str(" AND status = $1"); }
|
|
|
|
|
if case_type.is_some() { query.push_str(if status.is_some() { " AND case_type = $2" } else { " AND case_type = $1" }); }
|
|
|
|
|
|
2026-04-15 06:23:27 +02:00
|
|
|
query.push_str(" ORDER BY created_at DESC LIMIT $3 OFFSET $4");
|
2026-04-06 03:39:41 +02:00
|
|
|
|
|
|
|
|
sqlx::query_as::<_, Verification>(
|
|
|
|
|
r#"
|
|
|
|
|
SELECT * FROM verifications
|
|
|
|
|
WHERE ($1::TEXT IS NULL OR status = $1)
|
|
|
|
|
AND ($2::TEXT IS NULL OR case_type = $2)
|
|
|
|
|
ORDER BY
|
|
|
|
|
CASE priority WHEN 'HIGH' THEN 1 WHEN 'MEDIUM' THEN 2 ELSE 3 END,
|
|
|
|
|
created_at DESC
|
|
|
|
|
LIMIT $3 OFFSET $4
|
|
|
|
|
"#
|
|
|
|
|
)
|
|
|
|
|
.bind(status)
|
|
|
|
|
.bind(case_type)
|
|
|
|
|
.bind(limit)
|
|
|
|
|
.bind(offset)
|
|
|
|
|
.fetch_all(pool)
|
|
|
|
|
.await
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub async fn update_status(
|
|
|
|
|
pool: &PgPool,
|
|
|
|
|
id: Uuid,
|
|
|
|
|
new_status: &str,
|
|
|
|
|
actor_id: Option<Uuid>,
|
|
|
|
|
notes: Option<&str>,
|
|
|
|
|
rejection_reason: Option<&str>,
|
|
|
|
|
) -> Result<Verification, sqlx::Error> {
|
|
|
|
|
let mut tx = pool.begin().await?;
|
|
|
|
|
|
Update backend services: catering_services, companies, developers, gateway, job_seekers, photographers, social_media_managers, tutors, ugc_content_creators, users; update cache (otp, token), contracts (profession_shared, profession_state), db (job_seeker, verification), email; add revision-requested email template; update init-db.sql and start-services.sh
2026-05-08 15:34:29 +02:00
|
|
|
// Validate actor_id exists in users table; if not, treat as NULL
|
|
|
|
|
// This handles cases where the token contains a user_id from an external auth system
|
|
|
|
|
let valid_actor_id = match actor_id {
|
|
|
|
|
Some(uid) => {
|
|
|
|
|
let exists = sqlx::query_scalar::<_, bool>("SELECT EXISTS(SELECT 1 FROM users WHERE id = $1)")
|
|
|
|
|
.bind(uid)
|
|
|
|
|
.fetch_one(&mut *tx)
|
|
|
|
|
.await?;
|
|
|
|
|
if exists { Some(uid) } else { None }
|
|
|
|
|
},
|
|
|
|
|
None => None,
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-06 03:39:41 +02:00
|
|
|
let old = sqlx::query_as::<_, Verification>("SELECT * FROM verifications WHERE id = $1 FOR UPDATE")
|
|
|
|
|
.bind(id)
|
|
|
|
|
.fetch_one(&mut *tx)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
let updated = sqlx::query_as::<_, Verification>(
|
|
|
|
|
r#"
|
|
|
|
|
UPDATE verifications
|
|
|
|
|
SET status = $2, notes = COALESCE($3, notes), rejection_reason = COALESCE($4, rejection_reason), updated_at = NOW()
|
|
|
|
|
WHERE id = $1
|
|
|
|
|
RETURNING *
|
|
|
|
|
"#
|
|
|
|
|
)
|
|
|
|
|
.bind(id)
|
|
|
|
|
.bind(new_status)
|
|
|
|
|
.bind(notes)
|
|
|
|
|
.bind(rejection_reason)
|
|
|
|
|
.fetch_one(&mut *tx)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
sqlx::query(
|
|
|
|
|
r#"
|
2026-04-15 06:23:27 +02:00
|
|
|
INSERT INTO verification_logs (verification_request_id, action, acted_by_user_id, old_status, new_status, remarks)
|
2026-04-06 03:39:41 +02:00
|
|
|
VALUES ($1, 'STATUS_CHANGE', $2, $3, $4, $5)
|
|
|
|
|
"#
|
|
|
|
|
)
|
|
|
|
|
.bind(id)
|
Update backend services: catering_services, companies, developers, gateway, job_seekers, photographers, social_media_managers, tutors, ugc_content_creators, users; update cache (otp, token), contracts (profession_shared, profession_state), db (job_seeker, verification), email; add revision-requested email template; update init-db.sql and start-services.sh
2026-05-08 15:34:29 +02:00
|
|
|
.bind(valid_actor_id)
|
2026-04-06 03:39:41 +02:00
|
|
|
.bind(&old.status)
|
|
|
|
|
.bind(new_status)
|
|
|
|
|
.bind(notes)
|
|
|
|
|
.execute(&mut *tx)
|
|
|
|
|
.await?;
|
|
|
|
|
|
|
|
|
|
tx.commit().await?;
|
|
|
|
|
Ok(updated)
|
|
|
|
|
}
|
|
|
|
|
}
|