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::company::{CompanyRepository, UpsertCompanyProfilePayload}; use db::models::job::{JobRepository, CreateJobPayload as DbCreateJobPayload, UpdateJobPayload as DbUpdateJobPayload}; use db::models::application::ApplicationRepository; use contracts::auth_middleware::AuthUser; pub fn router() -> Router { Router::new() .route("/profile/me", get(get_profile).patch(update_profile)) .route("/jobs", get(list_jobs).post(create_job)) .route("/jobs/:id", get(get_job).patch(update_job)) .route("/jobs/:id/submit", post(submit_job)) .route("/jobs/:id/close", post(close_job)) .route("/jobs/:id/applications", get(list_applications)) .route("/applications/:id/status", patch(update_application_status)) .route("/applications/:id/contact", get(view_contact)) } #[derive(Deserialize)] pub struct PaginationQuery { pub page: Option, pub limit: Option, pub status: Option, } #[derive(Deserialize)] pub struct CreateJobRequest { pub title: String, pub description: String, pub location: String, pub job_type: Option, pub salary_min: Option, pub salary_max: Option, pub experience_years: Option, pub skills: Option>, pub category: Option, } #[derive(Deserialize)] pub struct UpdateApplicationStatusPayload { pub status: String, } async fn get_profile( State(pool): State, auth: AuthUser, ) -> impl IntoResponse { match CompanyRepository::get_by_user_id(&pool, auth.user_id).await { Ok(Some(profile)) => (StatusCode::OK, Json(profile)).into_response(), Ok(None) => (StatusCode::NOT_FOUND, "Company 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 CompanyRepository::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_jobs( State(pool): State, auth: AuthUser, Query(q): Query, ) -> impl IntoResponse { let company = match CompanyRepository::get_by_user_id(&pool, auth.user_id).await { Ok(Some(c)) => c, _ => return (StatusCode::NOT_FOUND, "Company not found").into_response(), }; let page = q.page.unwrap_or(1); let limit = q.limit.unwrap_or(20); match JobRepository::list_by_company_id(&pool, company.id, q.status, page, limit).await { Ok(jobs) => (StatusCode::OK, Json(serde_json::json!({ "data": jobs, "pagination": { "page": page, "limit": limit } }))).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } async fn create_job( State(pool): State, auth: AuthUser, Json(payload): Json, ) -> impl IntoResponse { let company = match CompanyRepository::get_by_user_id(&pool, auth.user_id).await { Ok(Some(c)) => c, _ => return (StatusCode::NOT_FOUND, "Company not found").into_response(), }; if company.status != "APPROVED" { return (StatusCode::FORBIDDEN, "Company profile approval is required before posting jobs").into_response(); } let db_payload = DbCreateJobPayload { company_id: company.id, title: payload.title, category: payload.category, description: payload.description, location: payload.location, job_type: payload.job_type, salary_min: payload.salary_min, salary_max: payload.salary_max, experience_years: payload.experience_years, skills: payload.skills, }; match JobRepository::create(&pool, db_payload).await { Ok(job) => (StatusCode::CREATED, Json(job)).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } async fn get_job( State(pool): State, Path(id): Path, _auth: AuthUser, ) -> impl IntoResponse { match JobRepository::get_by_id(&pool, id).await { Ok(Some(job)) => (StatusCode::OK, Json(job)).into_response(), Ok(None) => (StatusCode::NOT_FOUND, "Job not found").into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } async fn update_job( State(pool): State, Path(id): Path, auth: AuthUser, Json(payload): Json, ) -> impl IntoResponse { // Basic verification: does job belong to auth user's company? let company = match CompanyRepository::get_by_user_id(&pool, auth.user_id).await { Ok(Some(c)) => c, _ => return (StatusCode::NOT_FOUND, "Company not found").into_response(), }; if company.status != "APPROVED" { return (StatusCode::FORBIDDEN, "Company profile approval is required before submitting jobs").into_response(); } let job = match JobRepository::get_by_id(&pool, id).await { Ok(Some(j)) if j.company_id == company.id => j, Ok(Some(_)) => return (StatusCode::FORBIDDEN, "Access denied").into_response(), _ => return (StatusCode::NOT_FOUND, "Job not found").into_response(), }; match JobRepository::update(&pool, job.id, payload).await { Ok(updated) => (StatusCode::OK, Json(updated)).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } async fn submit_job( State(pool): State, Path(id): Path, auth: AuthUser, ) -> impl IntoResponse { let company = match CompanyRepository::get_by_user_id(&pool, auth.user_id).await { Ok(Some(c)) => c, _ => return (StatusCode::NOT_FOUND, "Company not found").into_response(), }; let job = match JobRepository::get_by_id(&pool, id).await { Ok(Some(j)) if j.company_id == company.id => j, Ok(Some(_)) => return (StatusCode::FORBIDDEN, "Access denied").into_response(), _ => return (StatusCode::NOT_FOUND, "Job not found").into_response(), }; if job.status != "DRAFT" { return (StatusCode::BAD_REQUEST, "Job already submitted or live").into_response(); } match JobRepository::update_status(&pool, job.id, "PENDING_APPROVAL").await { Ok(updated) => (StatusCode::OK, Json(updated)).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } async fn close_job( State(pool): State, Path(id): Path, auth: AuthUser, ) -> impl IntoResponse { let company = match CompanyRepository::get_by_user_id(&pool, auth.user_id).await { Ok(Some(c)) => c, _ => return (StatusCode::NOT_FOUND, "Company not found").into_response(), }; let job = match JobRepository::get_by_id(&pool, id).await { Ok(Some(j)) if j.company_id == company.id => j, Ok(Some(_)) => return (StatusCode::FORBIDDEN, "Access denied").into_response(), _ => return (StatusCode::NOT_FOUND, "Job not found").into_response(), }; match JobRepository::update_status(&pool, job.id, "CLOSED").await { Ok(updated) => (StatusCode::OK, Json(updated)).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } async fn list_applications( State(pool): State, Path(id): Path, auth: AuthUser, Query(q): Query, ) -> impl IntoResponse { let company = match CompanyRepository::get_by_user_id(&pool, auth.user_id).await { Ok(Some(c)) => c, _ => return (StatusCode::NOT_FOUND, "Company not found").into_response(), }; let job = match JobRepository::get_by_id(&pool, id).await { Ok(Some(j)) if j.company_id == company.id => j, Ok(Some(_)) => return (StatusCode::FORBIDDEN, "Access denied").into_response(), _ => return (StatusCode::NOT_FOUND, "Job not found").into_response(), }; let page = q.page.unwrap_or(1); let limit = q.limit.unwrap_or(20); match ApplicationRepository::list_by_job_id(&pool, job.id, q.status, page, limit).await { Ok(apps) => (StatusCode::OK, Json(serde_json::json!({ "data": apps, "pagination": { "page": page, "limit": limit } }))).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } async fn update_application_status( State(pool): State, Path(id): Path, auth: AuthUser, Json(payload): Json, ) -> impl IntoResponse { let app = match ApplicationRepository::get_by_id(&pool, id).await { Ok(Some(a)) => a, _ => return (StatusCode::NOT_FOUND, "Application not found").into_response(), }; let job = match JobRepository::get_by_id(&pool, app.job_id).await { Ok(Some(j)) => j, _ => return (StatusCode::INTERNAL_SERVER_ERROR, "Job lost").into_response(), }; let company = match CompanyRepository::get_by_user_id(&pool, auth.user_id).await { Ok(Some(c)) => c, _ => return (StatusCode::FORBIDDEN, "Access denied").into_response(), }; if job.company_id != company.id { return (StatusCode::FORBIDDEN, "Access denied").into_response(); } match ApplicationRepository::update_status(&pool, app.id, &payload.status).await { Ok(updated) => (StatusCode::OK, Json(updated)).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } async fn view_contact( State(pool): State, Path(id): Path, auth: AuthUser, ) -> impl IntoResponse { let app = match ApplicationRepository::get_by_id(&pool, id).await { Ok(Some(a)) => a, _ => return (StatusCode::NOT_FOUND, "Application not found").into_response(), }; let job = match JobRepository::get_by_id(&pool, app.job_id).await { Ok(Some(j)) => j, _ => return (StatusCode::INTERNAL_SERVER_ERROR, "Job lost").into_response(), }; let company = match CompanyRepository::get_by_user_id(&pool, auth.user_id).await { Ok(Some(c)) => c, _ => return (StatusCode::FORBIDDEN, "Access denied").into_response(), }; if job.company_id != company.id { return (StatusCode::FORBIDDEN, "Access denied").into_response(); } // TODO: logic to deduct quota + fetch job seeker contact info from users table // For now, just mark viewed and return placeholder let _ = ApplicationRepository::mark_contact_viewed(&pool, app.id).await; (StatusCode::OK, Json(serde_json::json!({ "application_id": id.to_string(), "full_name": "Applicant Contact Info Locked", "email": "hidden@example.com", "phone": "+91 0000000000", "message": "Contact revealed" }))).into_response() }