use axum::{ extract::{Path, Query, State}, http::StatusCode, response::IntoResponse, routing::{get, patch, post}, Json, Router, }; use serde::Deserialize; use sqlx::PgPool; use uuid::Uuid; use db::models::customer::{CustomerRepository, UpsertCustomerProfilePayload}; use db::models::professional::ProfessionalRepository; use db::models::requirement::{RequirementRepository, CreateRequirementPayload as DbCreateRequirementPayload, UpdateRequirementPayload as DbUpdateRequirementPayload}; use db::models::lead_request::LeadRequestRepository; use contracts::auth_middleware::AuthUser; pub fn router() -> Router { Router::new() .route("/profile/me", get(get_profile).patch(update_profile)) .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("/requirements/{id}/requests", get(list_requests)) .route("/requirements/{id}/requests/{lead_id}/approve", post(approve_request)) .route("/requirements/{id}/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)] pub struct RejectRequestPayload { pub reason: Option, } async fn get_profile( State(pool): State, auth: AuthUser, ) -> impl IntoResponse { match CustomerRepository::get_by_user_id(&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(pool): State, auth: AuthUser, Json(payload): Json, ) -> impl IntoResponse { match CustomerRepository::upsert(&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 list_requirements( State(pool): State, auth: AuthUser, Query(q): Query, ) -> impl IntoResponse { let customer = match CustomerRepository::get_by_user_id(&pool, auth.user_id).await { Ok(Some(c)) => c, _ => return (StatusCode::NOT_FOUND, "Customer not found").into_response(), }; let page = q.page.unwrap_or(1); let limit = q.limit.unwrap_or(20); match RequirementRepository::list_by_customer_id(&pool, customer.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(pool): State, auth: AuthUser, Json(payload): Json, ) -> impl IntoResponse { let customer = match CustomerRepository::get_by_user_id(&pool, auth.user_id).await { Ok(Some(c)) => c, _ => return (StatusCode::NOT_FOUND, "Customer not found").into_response(), }; if customer.status != "APPROVED" { return (StatusCode::FORBIDDEN, "Customer profile approval is required before posting requirements").into_response(); } if customer.active_requirement_count >= 2 { return (StatusCode::TOO_MANY_REQUESTS, "Max 2 active requirements allowed").into_response(); } let p_date = payload.preferred_date.and_then(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok()); let db_payload = DbCreateRequirementPayload { customer_id: customer.id, 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(&pool, db_payload).await { Ok(req) => { let _ = CustomerRepository::update_active_requirement_count(&pool, customer.id, 1).await; (StatusCode::CREATED, Json(req)).into_response() }, Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } async fn get_requirement( State(pool): State, Path(id): Path, _auth: AuthUser, ) -> impl IntoResponse { match RequirementRepository::get_by_id(&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(pool): State, Path(id): Path, auth: AuthUser, Json(payload): Json, ) -> impl IntoResponse { let customer = match CustomerRepository::get_by_user_id(&pool, auth.user_id).await { Ok(Some(c)) => c, _ => return (StatusCode::NOT_FOUND, "Customer not found").into_response(), }; let req = match RequirementRepository::get_by_id(&pool, id).await { Ok(Some(r)) if r.customer_id == customer.id => r, Ok(Some(_)) => return (StatusCode::FORBIDDEN, "Access denied").into_response(), _ => return (StatusCode::NOT_FOUND, "Requirement not found").into_response(), }; match RequirementRepository::update(&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 list_requests( State(pool): State, Path(id): Path, auth: AuthUser, Query(q): Query, ) -> impl IntoResponse { let customer = match CustomerRepository::get_by_user_id(&pool, auth.user_id).await { Ok(Some(c)) => c, _ => return (StatusCode::NOT_FOUND, "Customer not found").into_response(), }; let req = match RequirementRepository::get_by_id(&pool, id).await { Ok(Some(r)) if r.customer_id == customer.id => r, Ok(Some(_)) => return (StatusCode::FORBIDDEN, "Access denied").into_response(), _ => return (StatusCode::NOT_FOUND, "Requirement not found").into_response(), }; let page = q.page.unwrap_or(1); let limit = q.limit.unwrap_or(20); match LeadRequestRepository::list_by_requirement_id(&pool, req.id, page, limit).await { 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(pool): State, Path((req_id, lead_id)): Path<(Uuid, Uuid)>, auth: AuthUser, ) -> impl IntoResponse { let customer = match CustomerRepository::get_by_user_id(&pool, auth.user_id).await { Ok(Some(c)) => c, _ => return (StatusCode::NOT_FOUND, "Customer not found").into_response(), }; let req = match RequirementRepository::get_by_id(&pool, req_id).await { Ok(Some(r)) if r.customer_id == customer.id => r, Ok(Some(_)) => return (StatusCode::FORBIDDEN, "Access denied").into_response(), _ => return (StatusCode::NOT_FOUND, "Requirement not found").into_response(), }; let lead = match LeadRequestRepository::get_by_id(&pool, lead_id).await { Ok(Some(l)) if l.requirement_id == req.id => 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(&pool, lead.id, "ACCEPTED").await { Ok(updated) => { let prof_user_id = match ProfessionalRepository::get_user_id_by_professional_id(&pool, lead.professional_id).await { Ok(Some(user_id)) => user_id, Ok(None) => return (StatusCode::NOT_FOUND, "Professional not found").into_response(), Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), }; match ProfessionalRepository::try_debit_reserved_tracecoins( &pool, prof_user_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(), } let req_after = match RequirementRepository::increment_accepted_count_and_get(&pool, req.id).await { Ok(r) => r, Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), }; if req_after.accepted_count >= 10 && req_after.status != "CLOSED" { let _ = RequirementRepository::update_status(&pool, req.id, "CLOSED").await; } (StatusCode::OK, Json(serde_json::json!({ "lead_request": updated, "requirement_status": if req_after.accepted_count >= 10 { "CLOSED" } else { req_after.status.as_str() }, "accepted_count": req_after.accepted_count, }))).into_response() }, Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } async fn reject_request( State(pool): State, Path((req_id, lead_id)): Path<(Uuid, Uuid)>, auth: AuthUser, Json(_payload): Json, ) -> impl IntoResponse { let customer = match CustomerRepository::get_by_user_id(&pool, auth.user_id).await { Ok(Some(c)) => c, _ => return (StatusCode::NOT_FOUND, "Customer not found").into_response(), }; let req = match RequirementRepository::get_by_id(&pool, req_id).await { Ok(Some(r)) if r.customer_id == customer.id => r, Ok(Some(_)) => return (StatusCode::FORBIDDEN, "Access denied").into_response(), _ => return (StatusCode::NOT_FOUND, "Requirement not found").into_response(), }; let lead = match LeadRequestRepository::get_by_id(&pool, lead_id).await { Ok(Some(l)) if l.requirement_id == req.id => 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(&pool, lead.id, "REJECTED").await { Ok(updated) => { let prof_user_id = match ProfessionalRepository::get_user_id_by_professional_id(&pool, lead.professional_id).await { Ok(Some(user_id)) => user_id, Ok(None) => return (StatusCode::NOT_FOUND, "Professional not found").into_response(), Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), }; match ProfessionalRepository::try_release_reserved_tracecoins( &pool, prof_user_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(), } } async fn submit_requirement( State(pool): State, Path(id): Path, auth: AuthUser, ) -> impl IntoResponse { let customer = match CustomerRepository::get_by_user_id(&pool, auth.user_id).await { Ok(Some(c)) => c, _ => return (StatusCode::NOT_FOUND, "Customer not found").into_response(), }; if customer.status != "APPROVED" { return (StatusCode::FORBIDDEN, "Customer profile approval is required before submitting requirements").into_response(); } let req = match RequirementRepository::get_by_id(&pool, id).await { Ok(Some(r)) if r.customer_id == customer.id => r, Ok(Some(_)) => return (StatusCode::FORBIDDEN, "Access denied").into_response(), _ => 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(&pool, req.id, "PENDING_APPROVAL").await { Ok(updated) => (StatusCode::OK, Json(updated)).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } }