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::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/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.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 _ = RequirementRepository::increment_accepted_count(&pool, req.id).await; // TODO: Reveal contact to professional + final Tracecoin deduction logic (StatusCode::OK, Json(updated)).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) => { // TODO: Return reserved Tracecoins to professional (StatusCode::OK, Json(updated)).into_response() }, Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } }