use axum::{ extract::{Path, Query, State}, http::StatusCode, response::IntoResponse, routing::{get, post}, Json, Router, }; use serde::Deserialize; use uuid::Uuid; use db::models::customer::{CustomerRepository, UpsertCustomerProfilePayload}; use db::models::requirement::{RequirementRepository, CreateRequirementPayload as DbCreateRequirementPayload, UpdateRequirementPayload as DbUpdateRequirementPayload}; use db::models::lead_request::LeadRequestRepository; use db::models::user::UserRepository; use db::models::verification::VerificationRepository; use db::models::tracecoin_wallet::TracecoinWalletRepository; use contracts::auth_middleware::AuthUser; use crate::AppState; pub fn router() -> Router { Router::new() .route("/profile/me", get(get_profile).patch(update_profile)) .route("/profile/submit", post(submit_for_verification)) .route("/requirements", get(list_requirements).post(create_requirement)) .route("/requirements/{id}", get(get_requirement).patch(update_requirement)) .route("/requirements/{id}/submit", post(submit_requirement)) .route("/requests", get(list_requests)) .route("/requests/{lead_id}/approve", post(approve_request)) .route("/requests/{lead_id}/reject", post(reject_request)) } #[derive(Deserialize)] pub struct PaginationQuery { pub page: Option, pub limit: Option, } #[derive(Deserialize)] pub struct CreateRequirementRequest { pub profession_key: String, pub title: String, pub description: String, pub location: String, pub budget: Option, pub preferred_date: Option, pub extra_data_json: Option, } #[derive(Deserialize)] #[allow(dead_code)] pub struct RejectRequestPayload { pub reason: Option, } async fn get_profile( State(state): State, auth: AuthUser, ) -> impl IntoResponse { match CustomerRepository::get_by_user_id(&state.pool, auth.user_id).await { Ok(Some(profile)) => (StatusCode::OK, Json(profile)).into_response(), Ok(None) => (StatusCode::NOT_FOUND, "Customer profile not found").into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } async fn update_profile( State(state): State, auth: AuthUser, Json(payload): Json, ) -> impl IntoResponse { match CustomerRepository::upsert(&state.pool, auth.user_id, payload).await { Ok(profile) => (StatusCode::OK, Json(profile)).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } async fn submit_for_verification( State(state): State, auth: AuthUser, ) -> impl IntoResponse { let customer = match CustomerRepository::get_by_user_id(&state.pool, auth.user_id).await { Ok(Some(c)) => c, Ok(None) => return (StatusCode::NOT_FOUND, "Customer profile not found").into_response(), Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), }; if matches!( customer.status.as_str(), "PENDING_REVIEW" | "PENDING" | "UNDER_REVIEW" | "DOCUMENTS_REQUESTED" | "REVISION_REQUESTED" | "APPROVED" ) { return (StatusCode::BAD_REQUEST, format!("Profile is already {}", customer.status)).into_response(); } match CustomerRepository::submit_for_verification(&state.pool, auth.user_id).await { Ok(profile) => (StatusCode::OK, Json(serde_json::json!({ "status": profile.status, "message": "Profile submitted for verification" }))).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } async fn list_requirements( State(state): State, auth: AuthUser, Query(q): Query, ) -> impl IntoResponse { let page = q.page.unwrap_or(1); let limit = q.limit.unwrap_or(20); match RequirementRepository::list_by_user_id(&state.pool, auth.user_id, page, limit).await { Ok(reqs) => (StatusCode::OK, Json(serde_json::json!({ "data": reqs, "pagination": { "page": page, "limit": limit } }))).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } async fn create_requirement( State(state): State, auth: AuthUser, Json(payload): Json, ) -> impl IntoResponse { let p_date = payload.preferred_date.and_then(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok()); let db_payload = DbCreateRequirementPayload { profession_key: payload.profession_key, title: payload.title, description: payload.description, location: payload.location, budget: payload.budget, preferred_date: p_date, extra_data_json: payload.extra_data_json, }; match RequirementRepository::create(&state.pool, db_payload).await { Ok(req) => (StatusCode::CREATED, Json(req)).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } async fn get_requirement( State(state): State, Path(id): Path, _auth: AuthUser, ) -> impl IntoResponse { match RequirementRepository::get_by_id(&state.pool, id).await { Ok(Some(req)) => (StatusCode::OK, Json(req)).into_response(), Ok(None) => (StatusCode::NOT_FOUND, "Requirement not found").into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } async fn update_requirement( State(state): State, Path(id): Path, _auth: AuthUser, Json(payload): Json, ) -> impl IntoResponse { let req = match RequirementRepository::get_by_id(&state.pool, id).await { Ok(Some(r)) => r, _ => return (StatusCode::NOT_FOUND, "Requirement not found").into_response(), }; match RequirementRepository::update(&state.pool, req.id, payload).await { Ok(updated) => (StatusCode::OK, Json(updated)).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } async fn submit_requirement( State(state): State, Path(id): Path, auth: AuthUser, ) -> impl IntoResponse { let req = match RequirementRepository::get_by_id(&state.pool, id).await { Ok(Some(r)) => r, _ => return (StatusCode::NOT_FOUND, "Requirement not found").into_response(), }; if req.status != "DRAFT" { return (StatusCode::BAD_REQUEST, "Requirement already submitted or closed").into_response(); } match RequirementRepository::update_status(&state.pool, req.id, "PENDING_APPROVAL").await { Ok(updated) => { // Fire email to customer (ignore failures) if let Ok(user) = UserRepository::get_by_id(&state.pool, auth.user_id).await { let _ = state.mail.send_requirement_submitted_email(&user.email, user.name.as_deref().unwrap_or("User"), &updated.title).await; } // Create verification case so this request enters Verification Management first. let verification_payload = serde_json::json!({ "entity_type": "REQUIREMENT", "entity_id": updated.id, "title": updated.title, "profession_key": updated.profession_key, "location": updated.location, "budget": updated.budget, "status": updated.status, "created_by_user_id": updated.created_by_user_id, }); let _ = VerificationRepository::create( &state.pool, auth.user_id, "CUSTOMER", "REQUIREMENT_APPROVAL", "MEDIUM", verification_payload, serde_json::json!([]), ) .await; (StatusCode::OK, Json(updated)).into_response() } Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } async fn list_requests( State(state): State, Path(id): Path, _auth: AuthUser, Query(q): Query, ) -> impl IntoResponse { let page = q.page.unwrap_or(1); let limit = q.limit.unwrap_or(20); let offset = (page - 1) * limit; let rows_result = sqlx::query_as::<_, db::models::lead_request::LeadRequest>( r#" SELECT * FROM lead_requests WHERE user_role_profile_id = $1 ORDER BY requested_at DESC LIMIT $2 OFFSET $3 "# ) .bind(id) .bind(limit) .bind(offset) .fetch_all(&state.pool) .await; match rows_result { Ok(leads) => (StatusCode::OK, Json(serde_json::json!({ "data": leads, "pagination": { "page": page, "limit": limit } }))).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } async fn approve_request( State(state): State, Path(lead_id): Path, auth: AuthUser, ) -> impl IntoResponse { let lead = match LeadRequestRepository::get_by_id(&state.pool, lead_id).await { Ok(Some(l)) => l, _ => return (StatusCode::NOT_FOUND, "Lead request not found").into_response(), }; if lead.status != "PENDING" { return (StatusCode::BAD_REQUEST, "Lead already resolved").into_response(); } match LeadRequestRepository::update_status(&state.pool, lead.id, "ACCEPTED").await { Ok(updated) => { match TracecoinWalletRepository::try_debit_reserved_tracecoins( &state.pool, lead.user_role_profile_id, lead.tracecoins_reserved, lead.id, ).await { Ok(true) => {} Ok(false) => return (StatusCode::CONFLICT, "Reserved Tracecoins unavailable").into_response(), Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } (StatusCode::OK, Json(serde_json::json!({ "lead_request": updated, }))).into_response() }, Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } async fn reject_request( State(state): State, Path(lead_id): Path, _auth: AuthUser, Json(_payload): Json, ) -> impl IntoResponse { let lead = match LeadRequestRepository::get_by_id(&state.pool, lead_id).await { Ok(Some(l)) => l, _ => return (StatusCode::NOT_FOUND, "Lead request not found").into_response(), }; if lead.status != "PENDING" { return (StatusCode::BAD_REQUEST, "Lead already resolved").into_response(); } match LeadRequestRepository::update_status(&state.pool, lead.id, "REJECTED").await { Ok(updated) => { match TracecoinWalletRepository::try_release_reserved_tracecoins( &state.pool, lead.user_role_profile_id, lead.tracecoins_reserved, lead.id, "LEAD_REJECTED", ).await { Ok(true) => {} Ok(false) => return (StatusCode::CONFLICT, "Reserved Tracecoins unavailable").into_response(), Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } (StatusCode::OK, Json(updated)).into_response() }, Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } }