feat: complete backend implementation - payments service, admin endpoints, auth guards, submit-for-verification for all roles

- Add payments service proxying to Beeceptor mock gateway (create-order, verify, status)
- Add companies admin approve/reject/suspend + get detail endpoints
- Apply require_admin auth guards to all employee/department/designation handlers
- Add submit-for-verification endpoint to all 12 roles (10 professions + job seekers + customers + companies)
- Fix port conflict (employees moved from 8085 to 8096)
- Add submit_for_verification methods to all profile repositories
This commit is contained in:
Ashwin Kumar 2026-04-06 06:19:10 +02:00
parent 5cd00b74bc
commit cb53b68f49
16 changed files with 682 additions and 9 deletions

View file

@ -23,7 +23,8 @@ members = [
"crates/cache",
"crates/email",
"apps/cron",
"apps/employees"
"apps/employees",
"apps/payments"
]
[workspace.package]

View file

@ -3,7 +3,7 @@ use axum::{
extract::{Path, Query, State},
http::StatusCode,
response::IntoResponse,
routing::get,
routing::{get, patch},
Json, Router,
};
use contracts::auth_middleware::AuthUser;
@ -13,6 +13,10 @@ use uuid::Uuid;
pub fn router() -> Router<AppState> {
Router::new()
.route("/", get(list_companies))
.route("/{id}", get(get_company))
.route("/{id}/approve", patch(approve_company))
.route("/{id}/reject", patch(reject_company))
.route("/{id}/suspend", patch(suspend_company))
}
#[derive(Deserialize)]
@ -32,6 +36,39 @@ pub struct AdminCompanyRow {
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Serialize)]
pub struct AdminCompanyDetail {
pub id: Uuid,
pub user_id: Uuid,
pub company_name: String,
pub registration_number: Option<String>,
pub industry: Option<String>,
pub website_url: Option<String>,
pub employee_count: Option<i32>,
pub business_type: Option<String>,
pub gst_number: Option<String>,
pub contact_name: Option<String>,
pub contact_email: Option<String>,
pub contact_phone: Option<String>,
pub address_line1: Option<String>,
pub city: Option<String>,
pub state: Option<String>,
pub country: String,
pub postal_code: Option<String>,
pub status: String,
pub free_job_slots: i32,
pub purchased_job_slots: i32,
pub free_contact_views: i32,
pub purchased_contact_views: i32,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Deserialize)]
pub struct ApproveRejectRequest {
pub reason: Option<String>,
}
async fn list_companies(
_auth: AuthUser,
State(state): State<AppState>,
@ -58,3 +95,153 @@ async fn list_companies(
Ok(Json(companies))
}
async fn get_company(
_auth: AuthUser,
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
let company = sqlx::query_as!(
AdminCompanyDetail,
r#"
SELECT
id, user_id, company_name, registration_number, industry,
website_url, employee_count, business_type, gst_number,
contact_name, contact_email, contact_phone, address_line1,
city, state, country, postal_code, status,
free_job_slots, purchased_job_slots, free_contact_views, purchased_contact_views,
created_at, updated_at
FROM company_profiles
WHERE id = $1
"#,
id
)
.fetch_optional(&state.pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
match company {
Some(c) => Ok(Json(c)),
None => Err((StatusCode::NOT_FOUND, "Company not found".to_string())),
}
}
async fn approve_company(
_auth: AuthUser,
State(state): State<AppState>,
Path(id): Path<Uuid>,
Json(_payload): Json<ApproveRejectRequest>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
let company = sqlx::query!(
"SELECT id, user_id, status FROM company_profiles WHERE id = $1",
id
)
.fetch_optional(&state.pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
let company = match company {
Some(c) => c,
None => return Err((StatusCode::NOT_FOUND, "Company not found".to_string())),
};
if company.status == "APPROVED" {
return Err((StatusCode::BAD_REQUEST, "Company is already approved".to_string()));
}
sqlx::query!(
"UPDATE company_profiles SET status = 'APPROVED', updated_at = NOW() WHERE id = $1",
id
)
.execute(&state.pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
Ok(Json(serde_json::json!({
"id": id,
"status": "APPROVED",
"message": "Company approved successfully"
})))
}
async fn reject_company(
_auth: AuthUser,
State(state): State<AppState>,
Path(id): Path<Uuid>,
Json(payload): Json<ApproveRejectRequest>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
let reason = payload.reason.as_deref().unwrap_or("No reason provided");
let company = sqlx::query!(
"SELECT id, user_id, status FROM company_profiles WHERE id = $1",
id
)
.fetch_optional(&state.pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
let company = match company {
Some(c) => c,
None => return Err((StatusCode::NOT_FOUND, "Company not found".to_string())),
};
if company.status == "REJECTED" {
return Err((StatusCode::BAD_REQUEST, "Company is already rejected".to_string()));
}
sqlx::query!(
"UPDATE company_profiles SET status = 'REJECTED', updated_at = NOW() WHERE id = $1",
id
)
.execute(&state.pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
Ok(Json(serde_json::json!({
"id": id,
"status": "REJECTED",
"reason": reason,
"message": "Company rejected"
})))
}
async fn suspend_company(
_auth: AuthUser,
State(state): State<AppState>,
Path(id): Path<Uuid>,
Json(payload): Json<ApproveRejectRequest>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
let reason = payload.reason.as_deref().unwrap_or("No reason provided");
let company = sqlx::query!(
"SELECT id, user_id, status FROM company_profiles WHERE id = $1",
id
)
.fetch_optional(&state.pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
let company = match company {
Some(c) => c,
None => return Err((StatusCode::NOT_FOUND, "Company not found".to_string())),
};
if company.status == "SUSPENDED" {
return Err((StatusCode::BAD_REQUEST, "Company is already suspended".to_string()));
}
sqlx::query!(
"UPDATE company_profiles SET status = 'SUSPENDED', updated_at = NOW() WHERE id = $1",
id
)
.execute(&state.pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
Ok(Json(serde_json::json!({
"id": id,
"status": "SUSPENDED",
"reason": reason,
"message": "Company suspended"
})))
}

View file

@ -18,6 +18,7 @@ use crate::AppState;
pub fn router() -> Router<AppState> {
Router::new()
.route("/profile/me", get(get_profile).patch(update_profile))
.route("/profile/submit", post(submit_for_verification))
.route("/jobs", get(list_jobs).post(create_job))
.route("/jobs/{id}", get(get_job).patch(update_job))
.route("/jobs/{id}/submit", post(submit_job))
@ -74,6 +75,29 @@ async fn update_profile(
}
}
async fn submit_for_verification(
State(state): State<AppState>,
auth: AuthUser,
) -> impl IntoResponse {
let company = match CompanyRepository::get_by_user_id(&state.pool, auth.user_id).await {
Ok(Some(c)) => c,
Ok(None) => return (StatusCode::NOT_FOUND, "Company profile not found").into_response(),
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
};
if company.status == "PENDING_REVIEW" || company.status == "APPROVED" {
return (StatusCode::BAD_REQUEST, format!("Profile is already {}", company.status)).into_response();
}
match CompanyRepository::submit_for_verification(&state.pool, auth.user_id).await {
Ok(profile) => (StatusCode::OK, Json(serde_json::json!({
"status": profile.status,
"message": "Profile submitted for verification"
}))).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
}
}
async fn list_jobs(
State(state): State<AppState>,
auth: AuthUser,

View file

@ -18,6 +18,7 @@ use crate::AppState;
pub fn router() -> Router<AppState> {
Router::new()
.route("/profile/me", get(get_profile).patch(update_profile))
.route("/profile/submit", post(submit_for_verification))
.route("/requirements", get(list_requirements).post(create_requirement))
.route("/requirements/{id}", get(get_requirement).patch(update_requirement))
.route("/requirements/{id}/submit", post(submit_requirement))
@ -70,6 +71,29 @@ async fn update_profile(
}
}
async fn submit_for_verification(
State(state): State<AppState>,
auth: AuthUser,
) -> impl IntoResponse {
let customer = match CustomerRepository::get_by_user_id(&state.pool, auth.user_id).await {
Ok(Some(c)) => c,
Ok(None) => return (StatusCode::NOT_FOUND, "Customer profile not found").into_response(),
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
};
if customer.status == "PENDING_REVIEW" || customer.status == "APPROVED" {
return (StatusCode::BAD_REQUEST, format!("Profile is already {}", customer.status)).into_response();
}
match CustomerRepository::submit_for_verification(&state.pool, auth.user_id).await {
Ok(profile) => (StatusCode::OK, Json(serde_json::json!({
"status": profile.status,
"message": "Profile submitted for verification"
}))).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
}
}
async fn list_requirements(
State(state): State<AppState>,
auth: AuthUser,

View file

@ -21,6 +21,9 @@ async fn list_departments(
auth: AuthUser,
State(state): State<AppState>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Insufficient permissions".to_string()));
}
let departments = DepartmentRepository::list(&state.pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
@ -33,6 +36,9 @@ async fn create_department(
State(state): State<AppState>,
Json(payload): Json<CreateDepartmentPayload>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Insufficient permissions".to_string()));
}
let department = DepartmentRepository::create(&state.pool, payload)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
@ -46,6 +52,9 @@ async fn update_department(
State(state): State<AppState>,
Json(payload): Json<serde_json::Value>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Insufficient permissions".to_string()));
}
let department = DepartmentRepository::update(&state.pool, id, payload)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
@ -58,6 +67,9 @@ async fn delete_department(
Path(id): Path<Uuid>,
State(state): State<AppState>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Insufficient permissions".to_string()));
}
DepartmentRepository::delete(&state.pool, id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

View file

@ -6,7 +6,7 @@ use axum::{
routing::{get, post, patch},
Json, Router,
};
use contracts::auth_middleware::AuthUser;
use contracts::auth_middleware::{AuthUser, require_admin};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use db::models::designation::{DesignationRepository, CreateDesignationPayload};
@ -22,6 +22,9 @@ async fn list_all_designations(
auth: AuthUser,
State(state): State<AppState>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Insufficient permissions".to_string()));
}
let designations = DesignationRepository::list_all(&state.pool)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
@ -34,6 +37,9 @@ async fn list_by_department(
State(state): State<AppState>,
Path(dept_id): Path<Uuid>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Insufficient permissions".to_string()));
}
let designations = DesignationRepository::list_by_department(&state.pool, dept_id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
@ -46,6 +52,9 @@ async fn create_designation(
State(state): State<AppState>,
Json(payload): Json<CreateDesignationPayload>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Insufficient permissions".to_string()));
}
let designation = DesignationRepository::create(&state.pool, payload)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
@ -59,6 +68,9 @@ async fn update_designation(
State(state): State<AppState>,
Json(payload): Json<serde_json::Value>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Insufficient permissions".to_string()));
}
let designation = DesignationRepository::update(&state.pool, id, payload)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
@ -71,6 +83,9 @@ async fn delete_designation(
Path(id): Path<Uuid>,
State(state): State<AppState>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Insufficient permissions".to_string()));
}
DesignationRepository::delete(&state.pool, id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

View file

@ -34,10 +34,13 @@ pub struct EmployeeResponse {
}
async fn list_employees(
_auth: AuthUser,
auth: AuthUser,
State(state): State<AppState>,
Query(q): Query<ListQuery>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Insufficient permissions".to_string()));
}
let employees = EmployeeRepository::list(&state.pool, q.q)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
@ -46,10 +49,13 @@ async fn list_employees(
}
async fn get_employee(
_auth: AuthUser,
auth: AuthUser,
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Insufficient permissions".to_string()));
}
let employee = EmployeeRepository::list(&state.pool, None)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?
@ -61,10 +67,13 @@ async fn get_employee(
}
async fn create_employee(
_auth: AuthUser,
auth: AuthUser,
State(state): State<AppState>,
Json(payload): Json<CreateEmployeePayload>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Insufficient permissions".to_string()));
}
let employee = EmployeeRepository::create(&state.pool, payload)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;
@ -84,11 +93,14 @@ pub struct UpdateEmployeePayload {
}
async fn update_employee(
_auth: AuthUser,
auth: AuthUser,
State(state): State<AppState>,
Path(id): Path<Uuid>,
Json(payload): Json<UpdateEmployeePayload>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Insufficient permissions".to_string()));
}
let employee = EmployeeRepository::update(
&state.pool,
id,
@ -107,10 +119,13 @@ async fn update_employee(
}
async fn delete_employee(
_auth: AuthUser,
auth: AuthUser,
State(state): State<AppState>,
Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, (StatusCode, String)> {
if let Err(_) = require_admin(&auth) {
return Err((StatusCode::FORBIDDEN, "Insufficient permissions".to_string()));
}
EmployeeRepository::delete(&state.pool, id)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?;

View file

@ -68,7 +68,7 @@ impl Services {
payments_url: std::env::var("PAYMENTS_SERVICE_URL")
.unwrap_or_else(|_| "http://localhost:8094".to_string()),
employees_url: std::env::var("EMPLOYEES_SERVICE_URL")
.unwrap_or_else(|_| "http://localhost:8085".to_string()),
.unwrap_or_else(|_| "http://localhost:8096".to_string()),
client: reqwest::Client::new(),
}
}

View file

@ -18,6 +18,7 @@ pub fn router() -> Router<AppState> {
Router::new()
.route("/profile/me", get(get_profile).patch(update_profile))
.route("/profile/resume", post(upload_resume))
.route("/profile/submit", post(submit_for_verification))
.route("/jobs", get(browse_jobs))
.route("/jobs/{id}", get(get_job))
.route("/jobs/{id}/apply", post(apply_to_job))
@ -318,3 +319,26 @@ async fn withdraw_application(
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
}
}
async fn submit_for_verification(
State(state): State<AppState>,
auth: AuthUser,
) -> impl IntoResponse {
let seeker = match JobSeekerRepository::get_by_user_id(&state.pool, auth.user_id).await {
Ok(Some(s)) => s,
Ok(None) => return (StatusCode::NOT_FOUND, "Job seeker profile not found").into_response(),
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
};
if seeker.status == "PENDING_REVIEW" || seeker.status == "APPROVED" {
return (StatusCode::BAD_REQUEST, format!("Profile is already {}", seeker.status)).into_response();
}
match JobSeekerRepository::submit_for_verification(&state.pool, auth.user_id).await {
Ok(profile) => (StatusCode::OK, Json(serde_json::json!({
"status": profile.status,
"message": "Profile submitted for verification"
}))).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
}
}

14
apps/payments/Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "payments"
version = "0.1.0"
edition.workspace = true
[dependencies]
axum.workspace = true
serde.workspace = true
serde_json.workspace = true
tokio.workspace = true
tracing.workspace = true
tracing-subscriber.workspace = true
reqwest = { version = "0.12", features = ["json", "stream"] }
anyhow.workspace = true

230
apps/payments/src/main.rs Normal file
View file

@ -0,0 +1,230 @@
use axum::{
extract::State,
http::StatusCode,
routing::{get, post},
Json, Router,
};
use serde::{Deserialize, Serialize};
use std::net::SocketAddr;
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[derive(Clone)]
struct AppState {
beeceptor_url: String,
client: reqwest::Client,
}
#[derive(Debug, Serialize, Deserialize)]
struct CreateOrderRequest {
amount: u64,
currency: Option<String>,
package_id: Option<String>,
user_id: Option<String>,
}
#[derive(Debug, Serialize)]
struct CreateOrderResponse {
order_id: String,
amount: u64,
currency: String,
status: String,
}
#[derive(Debug, Serialize, Deserialize)]
struct VerifyPaymentRequest {
order_id: String,
payment_id: String,
signature: Option<String>,
}
#[derive(Debug, Serialize)]
struct VerifyPaymentResponse {
verified: bool,
payment_id: String,
status: String,
message: String,
}
#[derive(Debug, Serialize)]
struct PaymentStatusResponse {
payment_id: String,
status: String,
amount: u64,
currency: String,
}
async fn create_order(
State(state): State<AppState>,
Json(payload): Json<CreateOrderRequest>,
) -> Result<Json<CreateOrderResponse>, (StatusCode, String)> {
tracing::info!("Creating payment order: amount={}", payload.amount);
let resp = state
.client
.post(&state.beeceptor_url)
.header("Content-Type", "application/json")
.json(&serde_json::json!({
"amount": payload.amount,
"currency": payload.currency.as_deref().unwrap_or("INR"),
"package_id": payload.package_id,
"user_id": payload.user_id,
}))
.send()
.await
.map_err(|e| (StatusCode::BAD_GATEWAY, format!("Beeceptor error: {}", e)))?;
let status = resp.status();
let body: serde_json::Value = resp
.json()
.await
.map_err(|e| (StatusCode::BAD_GATEWAY, format!("Parse error: {}", e)))?;
if !status.is_success() {
return Err((
StatusCode::BAD_REQUEST,
body.get("message")
.and_then(|m| m.as_str())
.unwrap_or("Order creation failed")
.to_string(),
));
}
let order_id = body
.get("order_id")
.and_then(|v| v.as_str())
.unwrap_or("mock_order_123")
.to_string();
Ok(Json(CreateOrderResponse {
order_id,
amount: payload.amount,
currency: payload.currency.unwrap_or("INR".to_string()),
status: "created".to_string(),
}))
}
async fn verify_payment(
State(state): State<AppState>,
Json(payload): Json<VerifyPaymentRequest>,
) -> Result<Json<VerifyPaymentResponse>, (StatusCode, String)> {
tracing::info!("Verifying payment: order_id={}", payload.order_id);
let verify_url = format!("{}/verify", state.beeceptor_url.trim_end_matches('/'));
let resp = state
.client
.post(&verify_url)
.header("Content-Type", "application/json")
.json(&payload)
.send()
.await
.map_err(|e| (StatusCode::BAD_GATEWAY, format!("Beeceptor error: {}", e)))?;
let status = resp.status();
let body: serde_json::Value = resp
.json()
.await
.map_err(|e| (StatusCode::BAD_GATEWAY, format!("Parse error: {}", e)))?;
if !status.is_success() {
return Err((
StatusCode::BAD_REQUEST,
body.get("message")
.and_then(|m| m.as_str())
.unwrap_or("Verification failed")
.to_string(),
));
}
Ok(Json(VerifyPaymentResponse {
verified: true,
payment_id: payload.payment_id,
status: "success".to_string(),
message: "Payment verified successfully".to_string(),
}))
}
async fn get_payment_status(
State(state): State<AppState>,
axum::extract::Path(payment_id): axum::extract::Path<String>,
) -> Result<Json<PaymentStatusResponse>, (StatusCode, String)> {
tracing::info!("Getting payment status: payment_id={}", payment_id);
let status_url = format!("{}/{}", state.beeceptor_url.trim_end_matches('/'), payment_id);
let resp = state
.client
.get(&status_url)
.send()
.await
.map_err(|e| (StatusCode::BAD_GATEWAY, format!("Beeceptor error: {}", e)))?;
let status = resp.status();
let body: serde_json::Value = resp
.json()
.await
.map_err(|e| (StatusCode::BAD_GATEWAY, format!("Parse error: {}", e)))?;
if !status.is_success() {
return Ok(Json(PaymentStatusResponse {
payment_id,
status: "not_found".to_string(),
amount: 0,
currency: "INR".to_string(),
}));
}
let amount = body.get("amount").and_then(|v| v.as_u64()).unwrap_or(0);
let currency = body
.get("currency")
.and_then(|v| v.as_str())
.unwrap_or("INR")
.to_string();
let status_str = body
.get("status")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
Ok(Json(PaymentStatusResponse {
payment_id,
status: status_str,
amount,
currency,
}))
}
#[tokio::main]
async fn main() {
tracing_subscriber::registry()
.with(tracing_subscriber::EnvFilter::new(
std::env::var("RUST_LOG").unwrap_or_else(|_| "info".into()),
))
.with(tracing_subscriber::fmt::layer())
.init();
let beeceptor_url = std::env::var("BEECEPTOR_URL")
.unwrap_or_else(|_| "https://nxtgauge.free.beeceptor.com".to_string());
let state = AppState {
beeceptor_url,
client: reqwest::Client::new(),
};
let app = Router::new()
.route("/api/payments/create-order", post(create_order))
.route("/api/payments/verify", post(verify_payment))
.route("/api/payments/:id/status", get(get_payment_status))
.with_state(state);
let port: u16 = std::env::var("PORT")
.unwrap_or_else(|_| "8094".to_string())
.parse()
.expect("PORT must be a valid u16");
let addr = SocketAddr::from(([0, 0, 0, 0], port));
tracing::info!("Payments service (mock via Beeceptor) listening on {}", addr);
let listener = tokio::net::TcpListener::bind(&addr).await.unwrap();
axum::serve(listener, app).await.unwrap();
}

View file

@ -35,6 +35,7 @@ pub struct LeadRequestPayload {
/// `profession_key` must be a `'static str` matching the role key, e.g. `"PHOTOGRAPHER"`.
pub fn shared_routes(profession_key: &'static str) -> Router<ProfessionState> {
Router::new()
.route("/profile/submit", post(submit_for_verification))
// ── Marketplace (Redis-cached) ────────────────────────────────────────
.route(
"/marketplace",
@ -774,3 +775,25 @@ async fn wallet_invoice_detail(
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
}
}
async fn submit_for_verification(
State(state): State<ProfessionState>,
auth: AuthUser,
) -> impl IntoResponse {
let prof = match ProfessionalRepository::get_by_user_id(&state.pool, auth.user_id).await {
Ok(p) => p,
Err(_) => return (StatusCode::NOT_FOUND, "Professional profile not found").into_response(),
};
if prof.status == "PENDING_REVIEW" || prof.status == "APPROVED" {
return (StatusCode::BAD_REQUEST, format!("Profile is already {}", prof.status)).into_response();
}
match ProfessionalRepository::submit_for_verification(&state.pool, auth.user_id).await {
Ok(profile) => (StatusCode::OK, Json(serde_json::json!({
"status": profile.status,
"message": "Profile submitted for verification"
}))).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
}
}

View file

@ -140,4 +140,30 @@ impl CompanyRepository {
Ok(profile)
}
pub async fn submit_for_verification(
pool: &PgPool,
user_id: Uuid,
) -> Result<CompanyProfile, sqlx::Error> {
let profile = sqlx::query_as!(
CompanyProfile,
r#"
UPDATE company_profiles
SET status = 'PENDING_REVIEW', updated_at = NOW()
WHERE user_id = $1
RETURNING
id, user_id, company_name, registration_number, industry,
website_url, employee_count, business_type, gst_number,
contact_name, contact_email, contact_phone, address_line1,
city, state, country, postal_code, status,
free_job_slots, purchased_job_slots, free_contact_views, purchased_contact_views,
created_at, updated_at
"#,
user_id
)
.fetch_one(pool)
.await?;
Ok(profile)
}
}

View file

@ -116,4 +116,27 @@ impl CustomerRepository {
.await?;
Ok(())
}
pub async fn submit_for_verification(
pool: &PgPool,
user_id: Uuid,
) -> Result<CustomerProfile, sqlx::Error> {
let profile = sqlx::query_as!(
CustomerProfile,
r#"
UPDATE customer_profiles
SET status = 'PENDING_REVIEW', updated_at = NOW()
WHERE user_id = $1
RETURNING
id, user_id, full_name, phone, city, area, preferred_professions,
active_requirement_count, status, bio, experience_years, custom_data,
created_at, updated_at
"#,
user_id
)
.fetch_one(pool)
.await?;
Ok(profile)
}
}

View file

@ -116,5 +116,28 @@ impl JobSeekerRepository {
.await?;
Ok(())
}
pub async fn submit_for_verification(
pool: &PgPool,
user_id: Uuid,
) -> Result<JobSeekerProfile, sqlx::Error> {
let profile = sqlx::query_as!(
JobSeekerProfile,
r#"
UPDATE job_seeker_profiles
SET status = 'PENDING_REVIEW', updated_at = NOW()
WHERE user_id = $1
RETURNING
id, user_id, full_name, location, summary, experience_years,
skills, resume_url, active_application_count, status, bio, custom_data,
created_at, updated_at
"#,
user_id
)
.fetch_one(pool)
.await?;
Ok(profile)
}
}

View file

@ -561,4 +561,36 @@ impl ProfessionalRepository {
tx.commit().await?;
Ok(true)
}
pub async fn submit_for_verification(
pool: &PgPool,
user_id: Uuid,
) -> Result<Professional, sqlx::Error> {
let prof = sqlx::query_as!(
Professional,
"SELECT * FROM professionals WHERE user_id = $1",
user_id
)
.fetch_one(pool)
.await?;
if prof.status == "PENDING_REVIEW" || prof.status == "APPROVED" {
return Err(sqlx::Error::Protocol(format!("Professional profile is already {}", prof.status).into()));
}
let prof = sqlx::query_as!(
Professional,
r#"
UPDATE professionals
SET status = 'PENDING_REVIEW', updated_at = NOW()
WHERE user_id = $1
RETURNING *
"#,
user_id
)
.fetch_one(pool)
.await?;
Ok(prof)
}
}