feat(users): profile save, submit-for-verification, verification status endpoints
- profile.rs: GET/PATCH /api/profile, POST /api/profile/submit-for-verification, GET /api/me/verification-status — all role-aware, guards against duplicate pending - verifications.rs: add POST /api/admin/verifications/:id/request-documents, fix RoleRepository/wallet_id match arm type errors - coupons.rs: fix update_discount missing match block and i64/i32 type mismatch - main.rs: mount /api/profile and /api/me verification-status routers Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
f3487ccff9
commit
ab25f7a994
5 changed files with 541 additions and 13 deletions
|
|
@ -432,6 +432,12 @@ async fn update_discount(
|
|||
.execute(&state.pool)
|
||||
.await;
|
||||
|
||||
match result {
|
||||
Ok(_) => (StatusCode::OK, Json(serde_json::json!({ "updated": true }))).into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct ValidateCouponPayload {
|
||||
coupon_code: String,
|
||||
|
|
@ -553,7 +559,7 @@ async fn validate_coupon(
|
|||
.await
|
||||
.unwrap_or(Some(0))
|
||||
.unwrap_or(0);
|
||||
if count >= max_uses {
|
||||
if count >= max_uses as i64 {
|
||||
return Ok((
|
||||
StatusCode::OK,
|
||||
Json(ValidateCouponResponse {
|
||||
|
|
@ -587,6 +593,4 @@ async fn validate_coupon(
|
|||
message: "Coupon applied".to_string(),
|
||||
}),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,3 +18,4 @@ pub mod support;
|
|||
pub mod user_roles;
|
||||
pub mod external_roles;
|
||||
pub mod verifications;
|
||||
pub mod profile;
|
||||
|
|
|
|||
479
apps/users/src/handlers/profile.rs
Normal file
479
apps/users/src/handlers/profile.rs
Normal file
|
|
@ -0,0 +1,479 @@
|
|||
use crate::AppState;
|
||||
use axum::{
|
||||
extract::{Query, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
routing::{get, patch, post},
|
||||
Json, Router,
|
||||
};
|
||||
use contracts::auth_middleware::AuthUser;
|
||||
use db::models::{role::RoleRepository, verification::VerificationRepository};
|
||||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
|
||||
// ── Routers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
pub fn router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/", get(get_profile).patch(save_profile))
|
||||
.route("/submit-for-verification", post(submit_for_verification))
|
||||
}
|
||||
|
||||
pub fn me_verification_router() -> Router<AppState> {
|
||||
Router::new()
|
||||
.route("/verification-status", get(verification_status))
|
||||
}
|
||||
|
||||
// ── DTOs ──────────────────────────────────────────────────────────────────────
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RoleKeyQuery {
|
||||
#[serde(rename = "roleKey", alias = "role_key")]
|
||||
pub role_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SaveProfileInput {
|
||||
#[serde(rename = "roleKey", alias = "role_key")]
|
||||
pub role_key: String,
|
||||
pub profile_data: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SubmitInput {
|
||||
#[serde(rename = "roleKey", alias = "role_key")]
|
||||
pub role_key: String,
|
||||
/// Optional: if provided, saves this data before submitting.
|
||||
/// If omitted, reads previously saved profile data from DB.
|
||||
pub profile_data: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
|
||||
fn role_to_table(role_key: &str) -> Option<&'static str> {
|
||||
match role_key.to_uppercase().as_str() {
|
||||
"PHOTOGRAPHER" => Some("photographer_profiles"),
|
||||
"MAKEUP_ARTIST" => Some("makeup_artist_profiles"),
|
||||
"TUTOR" => Some("tutor_profiles"),
|
||||
"DEVELOPER" => Some("developer_profiles"),
|
||||
"VIDEO_EDITOR" => Some("video_editor_profiles"),
|
||||
"GRAPHIC_DESIGNER" => Some("graphic_designer_profiles"),
|
||||
"SOCIAL_MEDIA_MANAGER" => Some("social_media_manager_profiles"),
|
||||
"FITNESS_TRAINER" => Some("fitness_trainer_profiles"),
|
||||
"CATERING_SERVICES" => Some("catering_service_profiles"),
|
||||
"UGC_CONTENT_CREATOR" => Some("ugc_content_creator_profiles"),
|
||||
"JOB_SEEKER" | "JOBSEEKER" => Some("job_seeker_profiles"),
|
||||
"CUSTOMER" => Some("customer_profiles"),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_documents(profile_data: &serde_json::Value) -> serde_json::Value {
|
||||
let doc_keys = [
|
||||
"aadhar_doc",
|
||||
"registration_doc",
|
||||
"gst_doc",
|
||||
"sample_work",
|
||||
"degree_certificate",
|
||||
"certification_doc",
|
||||
"fssai_license",
|
||||
"identity_proof",
|
||||
"address_proof",
|
||||
"portfolio_proof",
|
||||
];
|
||||
let mut docs = vec![];
|
||||
for key in &doc_keys {
|
||||
if let Some(val) = profile_data.get(key) {
|
||||
if !val.is_null() {
|
||||
docs.push(serde_json::json!({
|
||||
"type": key,
|
||||
"value": val,
|
||||
"status": "SUBMITTED"
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
serde_json::Value::Array(docs)
|
||||
}
|
||||
|
||||
fn resolve_role_key(auth_role: &str, query_role: Option<String>) -> String {
|
||||
query_role
|
||||
.filter(|k| !k.is_empty())
|
||||
.unwrap_or_else(|| auth_role.to_string())
|
||||
.to_uppercase()
|
||||
}
|
||||
|
||||
// ── Handlers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
/// GET /api/profile?roleKey=PHOTOGRAPHER
|
||||
async fn get_profile(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Query(q): Query<RoleKeyQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let role_key = resolve_role_key(&auth.claims.active_role, q.role_key);
|
||||
|
||||
if role_key == "COMPANY" {
|
||||
let row = sqlx::query(
|
||||
r#"SELECT name, status, "updatedAt" FROM companies WHERE "userId" = $1"#,
|
||||
)
|
||||
.bind(auth.user_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await;
|
||||
|
||||
return match row {
|
||||
Ok(Some(r)) => {
|
||||
use sqlx::Row;
|
||||
let name: Option<String> = r.try_get("name").ok();
|
||||
let status: String = r.try_get("status").unwrap_or_default();
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"role_key": role_key,
|
||||
"profile_data": { "company_name": name },
|
||||
"verification_status": status,
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Ok(None) => (
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"role_key": role_key,
|
||||
"profile_data": null,
|
||||
"verification_status": "NOT_STARTED",
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
}
|
||||
|
||||
let table = match role_to_table(&role_key) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "error": format!("Unknown role: {}", role_key) })),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
};
|
||||
|
||||
let query = format!(
|
||||
r#"SELECT "profileData", verification_status FROM {} WHERE user_id = $1"#,
|
||||
table
|
||||
);
|
||||
|
||||
match sqlx::query(&query)
|
||||
.bind(auth.user_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(Some(row)) => {
|
||||
use sqlx::Row;
|
||||
let profile_data: serde_json::Value = row
|
||||
.try_get("profileData")
|
||||
.unwrap_or(serde_json::Value::Null);
|
||||
let verification_status: String =
|
||||
row.try_get("verification_status").unwrap_or_default();
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"role_key": role_key,
|
||||
"profile_data": profile_data,
|
||||
"verification_status": verification_status,
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Ok(None) => (
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"role_key": role_key,
|
||||
"profile_data": null,
|
||||
"verification_status": "NOT_STARTED",
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
/// PATCH /api/profile
|
||||
async fn save_profile(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Json(input): Json<SaveProfileInput>,
|
||||
) -> impl IntoResponse {
|
||||
let role_key = input.role_key.to_uppercase();
|
||||
|
||||
if role_key == "COMPANY" {
|
||||
let name = input
|
||||
.profile_data
|
||||
.get("company_name")
|
||||
.and_then(|v| v.as_str())
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
|
||||
return match sqlx::query(
|
||||
r#"
|
||||
INSERT INTO companies ("userId", name, status, "updatedAt")
|
||||
VALUES ($1, $2, 'DRAFT', NOW())
|
||||
ON CONFLICT ("userId") DO UPDATE SET
|
||||
name = EXCLUDED.name,
|
||||
"updatedAt" = NOW()
|
||||
"#,
|
||||
)
|
||||
.bind(auth.user_id)
|
||||
.bind(&name)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(_) => (
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({ "saved": true, "role_key": role_key })),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
};
|
||||
}
|
||||
|
||||
let table = match role_to_table(&role_key) {
|
||||
Some(t) => t,
|
||||
None => {
|
||||
return (
|
||||
StatusCode::BAD_REQUEST,
|
||||
Json(serde_json::json!({ "error": format!("Unknown role: {}", role_key) })),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
};
|
||||
|
||||
let query = format!(
|
||||
r#"
|
||||
INSERT INTO {table} (user_id, "profileData", verification_status, updated_at)
|
||||
VALUES ($1, $2, 'DRAFT', NOW())
|
||||
ON CONFLICT (user_id) DO UPDATE SET
|
||||
"profileData" = EXCLUDED."profileData",
|
||||
updated_at = NOW()
|
||||
"#
|
||||
);
|
||||
|
||||
match sqlx::query(&query)
|
||||
.bind(auth.user_id)
|
||||
.bind(&input.profile_data)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(_) => (
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({ "saved": true, "role_key": role_key })),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
/// POST /api/profile/submit-for-verification
|
||||
async fn submit_for_verification(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Json(input): Json<SubmitInput>,
|
||||
) -> impl IntoResponse {
|
||||
let role_key = input.role_key.to_uppercase();
|
||||
|
||||
// Guard: reject if an active verification already exists
|
||||
let existing: Result<Option<Uuid>, sqlx::Error> = sqlx::query_scalar(
|
||||
r#"
|
||||
SELECT id FROM verifications
|
||||
WHERE user_id = $1 AND role_key = $2
|
||||
AND status IN ('PENDING', 'UNDER_REVIEW', 'DOCUMENTS_REQUESTED', 'REVISION_REQUESTED')
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
.bind(auth.user_id)
|
||||
.bind(&role_key)
|
||||
.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 for this role. Please wait for it to be reviewed."
|
||||
})),
|
||||
)
|
||||
.into_response();
|
||||
}
|
||||
|
||||
// Fetch saved profile data or use submitted data
|
||||
let profile_data = match input.profile_data {
|
||||
Some(data) => data,
|
||||
None => fetch_saved_profile(&state, auth.user_id, &role_key).await,
|
||||
};
|
||||
|
||||
let documents = extract_documents(&profile_data);
|
||||
|
||||
// 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_roles SET status = 'PENDING' WHERE user_id = $1 AND role_id = $2",
|
||||
)
|
||||
.bind(auth.user_id)
|
||||
.bind(role.id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
|
||||
// Create verification record — appears in admin Verification Management
|
||||
match VerificationRepository::create(
|
||||
&state.pool,
|
||||
auth.user_id,
|
||||
&role_key,
|
||||
"PROFILE_VERIFICATION",
|
||||
"MEDIUM",
|
||||
profile_data,
|
||||
documents,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(v) => (
|
||||
StatusCode::CREATED,
|
||||
Json(serde_json::json!({
|
||||
"verification_id": v.id,
|
||||
"status": v.status,
|
||||
"message": "Your profile has been submitted for verification. We will notify you once it has been reviewed."
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
/// GET /api/me/verification-status?roleKey=PHOTOGRAPHER
|
||||
pub async fn verification_status(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Query(q): Query<RoleKeyQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let role_key = resolve_role_key(&auth.claims.active_role, q.role_key);
|
||||
|
||||
let row = sqlx::query(
|
||||
r#"
|
||||
SELECT id, status, notes, rejection_reason, updated_at
|
||||
FROM verifications
|
||||
WHERE user_id = $1 AND role_key = $2
|
||||
ORDER BY created_at DESC
|
||||
LIMIT 1
|
||||
"#,
|
||||
)
|
||||
.bind(auth.user_id)
|
||||
.bind(&role_key)
|
||||
.fetch_optional(&state.pool)
|
||||
.await;
|
||||
|
||||
match row {
|
||||
Ok(Some(r)) => {
|
||||
use sqlx::Row;
|
||||
let id: Uuid = r.try_get("id").unwrap_or(Uuid::nil());
|
||||
let status: String = r.try_get("status").unwrap_or_default();
|
||||
let notes: Option<String> = r.try_get("notes").ok().flatten();
|
||||
let rejection_reason: Option<String> = r.try_get("rejection_reason").ok().flatten();
|
||||
let updated_at: Option<chrono::DateTime<chrono::Utc>> =
|
||||
r.try_get("updated_at").ok();
|
||||
|
||||
(
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"has_verification": true,
|
||||
"verification_id": id,
|
||||
"status": status,
|
||||
"document_request": notes,
|
||||
"rejection_reason": rejection_reason,
|
||||
"updated_at": updated_at,
|
||||
})),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
Ok(None) => (
|
||||
StatusCode::OK,
|
||||
Json(serde_json::json!({
|
||||
"has_verification": false,
|
||||
"status": "NOT_SUBMITTED",
|
||||
})),
|
||||
)
|
||||
.into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
// ── Internal helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
async fn fetch_saved_profile(
|
||||
state: &AppState,
|
||||
user_id: Uuid,
|
||||
role_key: &str,
|
||||
) -> serde_json::Value {
|
||||
if role_key == "COMPANY" {
|
||||
return match sqlx::query(r#"SELECT name FROM companies WHERE "userId" = $1"#)
|
||||
.bind(user_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
{
|
||||
Ok(Some(r)) => {
|
||||
use sqlx::Row;
|
||||
let name: Option<String> = r.try_get("name").ok();
|
||||
serde_json::json!({ "company_name": name })
|
||||
}
|
||||
_ => serde_json::Value::Object(Default::default()),
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(table) = role_to_table(role_key) {
|
||||
let q = format!(r#"SELECT "profileData" FROM {} WHERE user_id = $1"#, table);
|
||||
if let Ok(Some(row)) = sqlx::query(&q)
|
||||
.bind(user_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
{
|
||||
use sqlx::Row;
|
||||
return row
|
||||
.try_get::<serde_json::Value, _>("profileData")
|
||||
.unwrap_or(serde_json::Value::Object(Default::default()));
|
||||
}
|
||||
}
|
||||
|
||||
serde_json::Value::Object(Default::default())
|
||||
}
|
||||
|
||||
async fn set_profile_status(state: &AppState, user_id: Uuid, role_key: &str, status: &str) {
|
||||
if role_key == "COMPANY" {
|
||||
sqlx::query(
|
||||
r#"UPDATE companies SET status = $1, "updatedAt" = NOW() WHERE "userId" = $2"#,
|
||||
)
|
||||
.bind(status)
|
||||
.bind(user_id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.ok();
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(table) = role_to_table(role_key) {
|
||||
let q = format!(
|
||||
"UPDATE {} SET verification_status = $1, submitted_at = NOW(), updated_at = NOW() WHERE user_id = $2",
|
||||
table
|
||||
);
|
||||
sqlx::query(&q)
|
||||
.bind(status)
|
||||
.bind(user_id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
|
@ -19,6 +19,7 @@ pub fn router() -> Router<AppState> {
|
|||
.route("/{id}/approve", post(approve_verification))
|
||||
.route("/{id}/reject", post(reject_verification))
|
||||
.route("/{id}/notes", post(add_notes))
|
||||
.route("/{id}/request-documents", post(request_documents))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
|
@ -146,7 +147,7 @@ async fn trigger_activation(
|
|||
}
|
||||
|
||||
// Assign role to user in user_roles
|
||||
if let Ok(Some(role)) = RoleRepository::get_by_key(&state.pool, &role_key).await {
|
||||
if let Ok(role) = RoleRepository::get_by_key(&state.pool, &role_key).await {
|
||||
sqlx::query!(
|
||||
"INSERT INTO user_roles (user_id, role_id, status, approved_at) VALUES ($1, $2, 'APPROVED', NOW()) ON CONFLICT (user_id, role_id) DO UPDATE SET status = 'APPROVED', approved_at = NOW()",
|
||||
user_id,
|
||||
|
|
@ -162,7 +163,7 @@ async fn trigger_activation(
|
|||
).execute(&state.pool).await.ok();
|
||||
|
||||
// Get wallet id for ledger entry
|
||||
if let Ok(Some(wallet_id)) = sqlx::query_scalar!(
|
||||
if let Some(wallet_id) = sqlx::query_scalar!(
|
||||
"SELECT id FROM tracecoin_wallets WHERE user_id = $1",
|
||||
user_id
|
||||
).fetch_optional(&state.pool).await.ok().flatten() {
|
||||
|
|
@ -293,6 +294,8 @@ async fn reject_verification(
|
|||
}
|
||||
}
|
||||
|
||||
/// POST /api/admin/verifications/:id/notes
|
||||
/// Adds internal notes without changing status (for reviewer comments).
|
||||
async fn add_notes(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
|
|
@ -307,23 +310,61 @@ async fn add_notes(
|
|||
match VerificationRepository::update_status(
|
||||
&state.pool,
|
||||
id,
|
||||
"UNDER_REVIEW", // Or keep current status?
|
||||
"UNDER_REVIEW",
|
||||
Some(auth.user_id),
|
||||
Some(¬es),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(v) => (StatusCode::OK, Json(v)).into_response(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize)]
|
||||
pub struct RequestDocumentsPayload {
|
||||
/// Human-readable message to the user describing what's needed.
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
/// POST /api/admin/verifications/:id/request-documents
|
||||
/// Sets status to DOCUMENTS_REQUESTED and notifies the user.
|
||||
async fn request_documents(
|
||||
auth: AuthUser,
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
Json(payload): Json<RequestDocumentsPayload>,
|
||||
) -> impl IntoResponse {
|
||||
if let Err(e) = require_admin(&auth) {
|
||||
return e.into_response();
|
||||
}
|
||||
|
||||
match VerificationRepository::update_status(
|
||||
&state.pool,
|
||||
id,
|
||||
"DOCUMENTS_REQUESTED",
|
||||
Some(auth.user_id),
|
||||
Some(&payload.message),
|
||||
None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(v) => {
|
||||
// Notify user that admin added notes/requested documents
|
||||
// Notify the user
|
||||
sqlx::query!(
|
||||
"INSERT INTO notifications (user_id, title, body, type, reference_id) VALUES ($1, $2, $3, $4, $5)",
|
||||
r#"INSERT INTO notifications (user_id, title, body, type, reference_id)
|
||||
VALUES ($1, $2, $3, $4, $5)"#,
|
||||
v.user_id,
|
||||
"Action Required",
|
||||
format!("Admin requested: {}", notes),
|
||||
"Action Required — Documents Needed",
|
||||
format!("Please resubmit your documents: {}", payload.message),
|
||||
"DOCUMENT_REQUEST",
|
||||
v.id
|
||||
).execute(&state.pool).await.ok();
|
||||
)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
(StatusCode::OK, Json(v)).into_response()
|
||||
}
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
|
|
|
|||
|
|
@ -65,9 +65,12 @@ async fn main() {
|
|||
// ── Admin: Approvals (jobs/requirements) ─────────────────────────
|
||||
.nest("/api/admin/approvals", handlers::approvals::router())
|
||||
.nest("/api/admin/verifications", handlers::verifications::router())
|
||||
// ── Me: Profile Status ─────────────────────────────────────────────
|
||||
// ── Me: Profile Status + Verification Status ──────────────────────
|
||||
.nest("/api/me", handlers::onboarding::me_router())
|
||||
// ── Onboarding State (user-facing) ────────────────────────────────
|
||||
.nest("/api/me", handlers::profile::me_verification_router())
|
||||
// ── Profile (save + submit-for-verification) ──────────────────────
|
||||
.nest("/api/profile", handlers::profile::router())
|
||||
// ── Onboarding State (legacy, kept for compatibility) ────────────
|
||||
.nest("/api/onboarding", handlers::onboarding::onboarding_router())
|
||||
// ── Admin: Onboarding + Dashboard Config ──────────────────────────
|
||||
.nest("/api/admin/onboarding-config", handlers::config::onboarding_router())
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue