diff --git a/apps/companies/src/handlers/admin.rs b/apps/companies/src/handlers/admin.rs index 4ca227a..54ab2ff 100644 --- a/apps/companies/src/handlers/admin.rs +++ b/apps/companies/src/handlers/admin.rs @@ -21,6 +21,7 @@ pub fn router() -> Router { .route("/{id}/approve", patch(approve_company)) .route("/{id}/reject", patch(reject_company)) .route("/{id}/suspend", patch(suspend_company)) + .route("/{id}/job-slots", patch(grant_job_slots)) .route("/jobs", get(list_jobs)) .route("/jobs/{id}/approve", post(approve_job)) .route("/jobs/{id}/reject", post(reject_job)) @@ -224,6 +225,36 @@ async fn suspend_company( Ok(Json(serde_json::json!({ "status": "SUSPENDED" }))) } +#[derive(Deserialize)] +pub struct GrantJobSlotsPayload { + pub slots: i32, +} + +/// Manual top-up for `purchased_job_slots` while the self-serve purchase flow +/// (TraceCoin/PayU) doesn't exist yet. Support-only unblock path. +async fn grant_job_slots( + _auth: AuthUser, + State(state): State, + Path(id): Path, + Json(payload): Json, +) -> Result { + if payload.slots <= 0 { + return Err((StatusCode::BAD_REQUEST, "slots must be a positive integer".to_string())); + } + + let new_total: i32 = sqlx::query_scalar( + "UPDATE company_profiles SET purchased_job_slots = purchased_job_slots + $1, updated_at = NOW() WHERE id = $2 RETURNING purchased_job_slots" + ) + .bind(payload.slots) + .bind(id) + .fetch_optional(&state.pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))? + .ok_or((StatusCode::NOT_FOUND, "Company not found".to_string()))?; + + Ok(Json(serde_json::json!({ "purchased_job_slots": new_total }))) +} + async fn list_jobs( _auth: AuthUser, State(state): State, diff --git a/apps/companies/src/handlers/mod.rs b/apps/companies/src/handlers/mod.rs index ee3bb7c..6048666 100644 --- a/apps/companies/src/handlers/mod.rs +++ b/apps/companies/src/handlers/mod.rs @@ -185,17 +185,24 @@ async fn create_job( ) -> impl IntoResponse { let company = match CompanyRepository::get_by_user_id(&state.pool, auth.user_id).await { Ok(Some(c)) => c, - _ => return (StatusCode::NOT_FOUND, "Company not found").into_response(), + Ok(None) => return (StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "Company not found" }))).into_response(), + Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))).into_response(), }; if company.status != "APPROVED" { - return (StatusCode::FORBIDDEN, "Company profile approval is required before posting jobs").into_response(); + return ( + StatusCode::FORBIDDEN, + Json(serde_json::json!({ + "error": "Company profile approval is required before posting jobs", + "code": "COMPANY_NOT_APPROVED" + })), + ).into_response(); } // --- New Quota Logic --- let jobs_this_month = match JobRepository::count_by_company_id_this_month(&state.pool, company.id).await { Ok(count) => count, - Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), + Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))).into_response(), }; if jobs_this_month >= 1 { @@ -221,7 +228,7 @@ async fn create_job( if let Err(e) = deduct_result { tracing::error!("Failed to deduct job slot: {}", e); - return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to deduct quota").into_response(); + return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "Failed to deduct quota" }))).into_response(); } } // ----------------------- @@ -251,7 +258,7 @@ async fn create_job( } (StatusCode::CREATED, Json(job)).into_response() } - Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), + Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))).into_response(), } }