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:
parent
5cd00b74bc
commit
cb53b68f49
16 changed files with 682 additions and 9 deletions
|
|
@ -23,7 +23,8 @@ members = [
|
|||
"crates/cache",
|
||||
"crates/email",
|
||||
"apps/cron",
|
||||
"apps/employees"
|
||||
"apps/employees",
|
||||
"apps/payments"
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
})))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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}")))?;
|
||||
|
|
|
|||
|
|
@ -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}")))?;
|
||||
|
|
|
|||
|
|
@ -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}")))?;
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
14
apps/payments/Cargo.toml
Normal 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
230
apps/payments/src/main.rs
Normal 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();
|
||||
}
|
||||
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue