nxtgauge-backend-rust/apps/customers/src/handlers.rs
2026-03-25 22:15:07 +01:00

339 lines
13 KiB
Rust

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<PgPool> {
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<i64>,
pub limit: Option<i64>,
}
#[derive(Deserialize)]
pub struct CreateRequirementRequest {
pub profession_key: String,
pub title: String,
pub description: String,
pub location: String,
pub budget: Option<i32>,
pub preferred_date: Option<String>,
pub extra_data_json: Option<serde_json::Value>,
}
#[derive(Deserialize)]
pub struct RejectRequestPayload {
pub reason: Option<String>,
}
async fn get_profile(
State(pool): State<PgPool>,
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<PgPool>,
auth: AuthUser,
Json(payload): Json<UpsertCustomerProfilePayload>,
) -> 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<PgPool>,
auth: AuthUser,
Query(q): Query<PaginationQuery>,
) -> 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<PgPool>,
auth: AuthUser,
Json(payload): Json<CreateRequirementRequest>,
) -> 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<PgPool>,
Path(id): Path<Uuid>,
_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<PgPool>,
Path(id): Path<Uuid>,
auth: AuthUser,
Json(payload): Json<DbUpdateRequirementPayload>,
) -> 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<PgPool>,
Path(id): Path<Uuid>,
auth: AuthUser,
Query(q): Query<PaginationQuery>,
) -> 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<PgPool>,
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<PgPool>,
Path((req_id, lead_id)): Path<(Uuid, Uuid)>,
auth: AuthUser,
Json(_payload): Json<RejectRequestPayload>,
) -> 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<PgPool>,
Path(id): Path<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(),
};
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(),
}
}