fix: return JSON errors from create_job and add admin job-slot grant endpoint
All checks were successful
build-and-release / build (customers) (push) Successful in 8s
build-and-release / build (catering-services) (push) Successful in 11s
build-and-release / build (fitness-trainers) (push) Successful in 7s
build-and-release / build (developers) (push) Successful in 12s
build-and-release / build (cron) (push) Successful in 18s
build-and-release / build (employees) (push) Successful in 16s
build-and-release / build (gateway) (push) Successful in 8s
build-and-release / build (job-seekers) (push) Successful in 5s
build-and-release / build (graphic-designers) (push) Successful in 7s
build-and-release / build (jobs) (push) Successful in 7s
build-and-release / build (payments) (push) Successful in 7s
build-and-release / build (makeup-artists) (push) Successful in 6s
build-and-release / build (photographers) (push) Successful in 5s
build-and-release / build (tutors) (push) Successful in 5s
build-and-release / build (social-media-managers) (push) Successful in 5s
build-and-release / build (ugc-content-creators) (push) Successful in 6s
build-and-release / build (users) (push) Successful in 5s
build-and-release / build (video-editors) (push) Successful in 4s
build-and-release / build (companies) (push) Successful in 1m21s

create_job returned plain-text bodies on every failure branch except
quota-exhausted, so the frontend's res.json() silently failed and always
showed a generic "Failed to create job" message regardless of the real
cause (company not approved, quota exhausted, DB error).

Also adds PATCH /api/admin/companies/{id}/job-slots as a manual top-up
for purchased_job_slots, since no self-serve purchase flow exists yet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Ashwin Kumar Sivakumar 2026-08-03 17:04:00 +05:30
parent b281eb5278
commit ddb2110e86
2 changed files with 43 additions and 5 deletions

View file

@ -21,6 +21,7 @@ pub fn router() -> Router<AppState> {
.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<AppState>,
Path(id): Path<Uuid>,
Json(payload): Json<GrantJobSlotsPayload>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
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<AppState>,

View file

@ -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(),
}
}