180 lines
8.1 KiB
Rust
180 lines
8.1 KiB
Rust
|
|
use axum::{
|
||
|
|
extract::{Path, Query, State},
|
||
|
|
http::StatusCode,
|
||
|
|
response::IntoResponse,
|
||
|
|
routing::{delete, get, patch, post},
|
||
|
|
Json, Router,
|
||
|
|
};
|
||
|
|
use serde::Deserialize;
|
||
|
|
use sqlx::PgPool;
|
||
|
|
use uuid::Uuid;
|
||
|
|
use db::models::professional::ProfessionalRepository;
|
||
|
|
use db::models::requirement::RequirementRepository;
|
||
|
|
use db::models::lead_request::{LeadRequestRepository, CreateLeadRequestPayload};
|
||
|
|
use crate::auth_middleware::AuthUser;
|
||
|
|
|
||
|
|
#[derive(Deserialize)]
|
||
|
|
pub struct PaginationQuery {
|
||
|
|
pub page: Option<i64>,
|
||
|
|
pub limit: Option<i64>,
|
||
|
|
}
|
||
|
|
|
||
|
|
#[derive(Deserialize)]
|
||
|
|
pub struct LeadRequestPayload {
|
||
|
|
pub requirement_id: Uuid,
|
||
|
|
}
|
||
|
|
|
||
|
|
pub fn shared_routes(profession_key: &'static str) -> Router<PgPool> {
|
||
|
|
Router::new()
|
||
|
|
.route(
|
||
|
|
"/marketplace",
|
||
|
|
get(move |state, query| browse_marketplace(state, query, profession_key)),
|
||
|
|
)
|
||
|
|
.route("/marketplace/:id", get(get_requirement))
|
||
|
|
.route("/leads/request", post(send_lead_request))
|
||
|
|
.route("/leads/requests/me", get(my_requests))
|
||
|
|
.route("/leads/requests/:id", delete(cancel_request))
|
||
|
|
.route("/leads/accepted/me", get(accepted_leads))
|
||
|
|
.route("/leads/accepted/:id", get(accepted_lead_detail))
|
||
|
|
.route("/portfolio/me", get(list_portfolio))
|
||
|
|
// ... (other routes remain same for now)
|
||
|
|
}
|
||
|
|
|
||
|
|
async fn browse_marketplace(
|
||
|
|
State(pool): State<PgPool>,
|
||
|
|
Query(q): Query<PaginationQuery>,
|
||
|
|
profession_key: &str,
|
||
|
|
) -> impl IntoResponse {
|
||
|
|
let page = q.page.unwrap_or(1);
|
||
|
|
let limit = q.limit.unwrap_or(20);
|
||
|
|
|
||
|
|
match ProfessionalRepository::get_marketplace(&pool, profession_key, page, limit).await {
|
||
|
|
Ok(items) => (StatusCode::OK, Json(serde_json::json!({
|
||
|
|
"data": items,
|
||
|
|
"pagination": { "page": page, "limit": limit }
|
||
|
|
}))).into_response(),
|
||
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async fn get_requirement(
|
||
|
|
State(pool): State<PgPool>,
|
||
|
|
_auth: AuthUser,
|
||
|
|
Path(id): Path<Uuid>,
|
||
|
|
) -> impl IntoResponse {
|
||
|
|
match RequirementRepository::get_by_id(&pool, id).await {
|
||
|
|
Ok(Some(req)) if req.status == "OPEN" => (StatusCode::OK, Json(req)).into_response(),
|
||
|
|
Ok(Some(_)) => (StatusCode::FORBIDDEN, "Requirement is not open").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 send_lead_request(
|
||
|
|
State(pool): State<PgPool>,
|
||
|
|
auth: AuthUser,
|
||
|
|
Json(payload): Json<LeadRequestPayload>,
|
||
|
|
) -> impl IntoResponse {
|
||
|
|
let prof = match ProfessionalRepository::get_by_user_id(&pool, auth.user_id).await {
|
||
|
|
Ok(p) => p,
|
||
|
|
Err(_) => return (StatusCode::NOT_FOUND, "Professional profile not found").into_response(),
|
||
|
|
};
|
||
|
|
|
||
|
|
let req = match RequirementRepository::get_by_id(&pool, payload.requirement_id).await {
|
||
|
|
Ok(Some(r)) if r.status == "OPEN" => r,
|
||
|
|
Ok(Some(_)) => return (StatusCode::BAD_REQUEST, "Requirement is not open").into_response(),
|
||
|
|
_ => return (StatusCode::NOT_FOUND, "Requirement not found").into_response(),
|
||
|
|
};
|
||
|
|
|
||
|
|
if req.request_count >= 20 {
|
||
|
|
return (StatusCode::CONFLICT, "Requirement reached max requests").into_response();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check wallet balance
|
||
|
|
let wallet = match ProfessionalRepository::get_wallet(&pool, auth.user_id).await {
|
||
|
|
Ok(w) => w,
|
||
|
|
_ => return (StatusCode::BAD_REQUEST, "Wallet not found").into_response(),
|
||
|
|
};
|
||
|
|
|
||
|
|
if wallet.balance < 25 {
|
||
|
|
return (StatusCode::PAYMENT_REQUIRED, "Insufficient Tracecoin balance").into_response();
|
||
|
|
}
|
||
|
|
|
||
|
|
let db_payload = CreateLeadRequestPayload {
|
||
|
|
requirement_id: req.id,
|
||
|
|
professional_id: prof.id,
|
||
|
|
expires_at: Utc::now() + chrono::Duration::hours(24),
|
||
|
|
};
|
||
|
|
|
||
|
|
match LeadRequestRepository::create(&pool, db_payload).await {
|
||
|
|
Ok(lead) => {
|
||
|
|
let _ = RequirementRepository::increment_request_count(&pool, req.id).await;
|
||
|
|
// TODO: Debit/Reserve Tracecoins in wallet ledger
|
||
|
|
(StatusCode::CREATED, Json(lead)).into_response()
|
||
|
|
},
|
||
|
|
Err(e) => {
|
||
|
|
if e.to_string().contains("unique") {
|
||
|
|
(StatusCode::CONFLICT, "Already requested this lead").into_response()
|
||
|
|
} else {
|
||
|
|
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async fn list_portfolio(
|
||
|
|
State(pool): State<PgPool>,
|
||
|
|
auth: AuthUser,
|
||
|
|
) -> impl IntoResponse {
|
||
|
|
match ProfessionalRepository::get_by_user_id(&pool, auth.user_id).await {
|
||
|
|
Ok(prof) => {
|
||
|
|
match ProfessionalRepository::get_portfolio(&pool, prof.id).await {
|
||
|
|
Ok(items) => (StatusCode::OK, Json(serde_json::json!({ "data": items }))).into_response(),
|
||
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||
|
|
}
|
||
|
|
},
|
||
|
|
Err(_) => (StatusCode::NOT_FOUND, "Professional profile not found").into_response(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async fn list_services(
|
||
|
|
State(pool): State<PgPool>,
|
||
|
|
auth: AuthUser,
|
||
|
|
) -> impl IntoResponse {
|
||
|
|
match ProfessionalRepository::get_by_user_id(&pool, auth.user_id).await {
|
||
|
|
Ok(prof) => {
|
||
|
|
match ProfessionalRepository::get_services(&pool, prof.id).await {
|
||
|
|
Ok(items) => (StatusCode::OK, Json(serde_json::json!({ "data": items }))).into_response(),
|
||
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||
|
|
}
|
||
|
|
},
|
||
|
|
Err(_) => (StatusCode::NOT_FOUND, "Professional profile not found").into_response(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async fn wallet_balance(
|
||
|
|
State(pool): State<PgPool>,
|
||
|
|
auth: AuthUser,
|
||
|
|
) -> impl IntoResponse {
|
||
|
|
match ProfessionalRepository::get_wallet(&pool, auth.user_id).await {
|
||
|
|
Ok(w) => (StatusCode::OK, Json(w)).into_response(),
|
||
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Stubs for remaining routes (ledger, invoices, etc.)
|
||
|
|
async fn my_requests(_s: State<PgPool>, _a: AuthUser, _q: Query<PaginationQuery>) -> impl IntoResponse { (StatusCode::OK, Json(serde_json::json!({"data":[]}))) }
|
||
|
|
async fn cancel_request(_s: State<PgPool>, _a: AuthUser, _p: Path<Uuid>) -> impl IntoResponse { (StatusCode::OK, Json(serde_json::json!({"message":"Done"}))) }
|
||
|
|
async fn accepted_leads(_s: State<PgPool>, _a: AuthUser, _q: Query<PaginationQuery>) -> impl IntoResponse { (StatusCode::OK, Json(serde_json::json!({"data":[]}))) }
|
||
|
|
async fn accepted_lead_detail(_s: State<PgPool>, _a: AuthUser, _p: Path<Uuid>) -> impl IntoResponse { (StatusCode::OK, Json(serde_json::json!({"id":_p.to_string()}))) }
|
||
|
|
async fn create_portfolio_item(_s: State<PgPool>, _a: AuthUser, _p: Json<serde_json::Value>) -> impl IntoResponse { (StatusCode::CREATED, Json(serde_json::json!({"id":Uuid::new_v4().to_string()}))) }
|
||
|
|
async fn update_portfolio_item(_s: State<PgPool>, _a: AuthUser, _p: Path<Uuid>, _v: Json<serde_json::Value>) -> impl IntoResponse { (StatusCode::OK, Json(serde_json::json!({"message":"Updated"}))) }
|
||
|
|
async fn delete_portfolio_item(_s: State<PgPool>, _a: AuthUser, _p: Path<Uuid>) -> impl IntoResponse { (StatusCode::OK, Json(serde_json::json!({"message":"Deleted"}))) }
|
||
|
|
async fn create_service(_s: State<PgPool>, _a: AuthUser, _p: Json<serde_json::Value>) -> impl IntoResponse { (StatusCode::CREATED, Json(serde_json::json!({"id":Uuid::new_v4().to_string()}))) }
|
||
|
|
async fn update_service(_s: State<PgPool>, _a: AuthUser, _p: Path<Uuid>, _v: Json<serde_json::Value>) -> impl IntoResponse { (StatusCode::OK, Json(serde_json::json!({"message":"Updated"}))) }
|
||
|
|
async fn delete_service(_s: State<PgPool>, _a: AuthUser, _p: Path<Uuid>) -> impl IntoResponse { (StatusCode::OK, Json(serde_json::json!({"message":"Deleted"}))) }
|
||
|
|
async fn wallet_ledger(_s: State<PgPool>, _a: AuthUser, _q: Query<PaginationQuery>) -> impl IntoResponse { (StatusCode::OK, Json(serde_json::json!({"data":[]}))) }
|
||
|
|
async fn wallet_invoices(_s: State<PgPool>, _a: AuthUser, _q: Query<PaginationQuery>) -> impl IntoResponse { (StatusCode::OK, Json(serde_json::json!({"data":[]}))) }
|
||
|
|
async fn wallet_invoice_detail(_s: State<PgPool>, _a: AuthUser, _p: Path<Uuid>) -> impl IntoResponse { (StatusCode::OK, Json(serde_json::json!({"id":_p.to_string()}))) }
|
||
|
|
|