2026-03-19 00:30:23 +01:00
|
|
|
use crate::AppState;
|
|
|
|
|
use axum::{
|
|
|
|
|
extract::{Path, Query, State},
|
|
|
|
|
http::StatusCode,
|
|
|
|
|
response::IntoResponse,
|
|
|
|
|
routing::{get, post},
|
|
|
|
|
Json, Router,
|
|
|
|
|
};
|
|
|
|
|
use contracts::auth_middleware::{require_admin, AuthUser};
|
|
|
|
|
use db::models::job::JobRepository;
|
2026-03-23 00:34:49 +01:00
|
|
|
use db::models::onboarding_state::OnboardingStateRepository;
|
2026-03-19 00:30:23 +01:00
|
|
|
use db::models::requirement::RequirementRepository;
|
2026-03-23 00:34:49 +01:00
|
|
|
use db::models::role::RoleRepository;
|
|
|
|
|
use db::models::user::UserRepository;
|
2026-03-19 00:30:23 +01:00
|
|
|
use serde::Deserialize;
|
|
|
|
|
use uuid::Uuid;
|
|
|
|
|
|
|
|
|
|
pub fn router() -> Router<AppState> {
|
|
|
|
|
Router::new()
|
|
|
|
|
.route("/", get(list_pending))
|
2026-03-23 00:34:49 +01:00
|
|
|
// Submission viewer: GET /api/admin/approvals/submission/{user_id}?roleKey=PHOTOGRAPHER
|
|
|
|
|
.route("/submission/{user_id}", get(get_submission))
|
2026-03-22 15:55:29 +01:00
|
|
|
.route("/profiles/company/{user_id}/approve", post(approve_company_profile))
|
|
|
|
|
.route("/profiles/company/{user_id}/reject", post(reject_company_profile))
|
|
|
|
|
.route("/profiles/customer/{user_id}/approve", post(approve_customer_profile))
|
|
|
|
|
.route("/profiles/customer/{user_id}/reject", post(reject_customer_profile))
|
|
|
|
|
.route("/profiles/professional/{role_key}/{user_id}/approve", post(approve_professional_profile))
|
|
|
|
|
.route("/profiles/professional/{role_key}/{user_id}/reject", post(reject_professional_profile))
|
|
|
|
|
.route("/jobs/{id}/approve", post(approve_job))
|
|
|
|
|
.route("/jobs/{id}/reject", post(reject_job))
|
|
|
|
|
.route("/requirements/{id}/approve", post(approve_requirement))
|
|
|
|
|
.route("/requirements/{id}/reject", post(reject_requirement))
|
2026-03-19 00:30:23 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-23 00:34:49 +01:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
pub struct RoleKeyQuery {
|
|
|
|
|
#[serde(rename = "roleKey", alias = "role_key")]
|
|
|
|
|
pub role_key: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// GET /api/admin/approvals/submission/{user_id}?roleKey=PHOTOGRAPHER
|
|
|
|
|
/// Returns the user info + their onboarding state (submitted form answers) for admin review.
|
|
|
|
|
async fn get_submission(
|
|
|
|
|
auth: AuthUser,
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path(user_id): Path<Uuid>,
|
|
|
|
|
Query(q): Query<RoleKeyQuery>,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
if let Err(e) = require_admin(&auth) {
|
|
|
|
|
return e.into_response();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Fetch user
|
|
|
|
|
let user = match UserRepository::get_by_id(&state.pool, user_id).await {
|
|
|
|
|
Ok(u) => u,
|
|
|
|
|
Err(_) => return (StatusCode::NOT_FOUND, "User not found").into_response(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Fetch onboarding state (for the given roleKey, or the user's active role)
|
|
|
|
|
let role_key = q.role_key.filter(|k| !k.is_empty());
|
|
|
|
|
let onboarding = if let Some(ref rk) = role_key {
|
|
|
|
|
match RoleRepository::get_by_key(&state.pool, rk).await {
|
|
|
|
|
Ok(role) => OnboardingStateRepository::get(&state.pool, user_id, role.id)
|
|
|
|
|
.await
|
|
|
|
|
.unwrap_or(None),
|
|
|
|
|
Err(_) => None,
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
(
|
|
|
|
|
StatusCode::OK,
|
|
|
|
|
Json(serde_json::json!({
|
|
|
|
|
"user": {
|
|
|
|
|
"id": user.id,
|
|
|
|
|
"name": user.full_name,
|
|
|
|
|
"email": user.email,
|
|
|
|
|
"phone": user.phone,
|
|
|
|
|
"status": user.status,
|
|
|
|
|
"email_verified": user.email_verified,
|
|
|
|
|
"created_at": user.created_at,
|
|
|
|
|
},
|
|
|
|
|
"role_key": role_key,
|
|
|
|
|
"onboarding": onboarding.map(|s| serde_json::json!({
|
|
|
|
|
"status": s.status,
|
|
|
|
|
"progress_json": s.progress_json,
|
|
|
|
|
"completed_at": s.completed_at,
|
|
|
|
|
"updated_at": s.updated_at,
|
|
|
|
|
})),
|
|
|
|
|
})),
|
|
|
|
|
)
|
|
|
|
|
.into_response()
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 00:30:23 +01:00
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
pub struct ListQuery {
|
|
|
|
|
pub page: Option<i64>,
|
|
|
|
|
pub limit: Option<i64>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
|
pub struct RejectPayload {
|
|
|
|
|
pub reason: Option<String>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn list_pending(
|
|
|
|
|
auth: AuthUser,
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Query(q): Query<ListQuery>,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
if let Err(e) = require_admin(&auth) {
|
|
|
|
|
return e.into_response();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let page = q.page.unwrap_or(1);
|
|
|
|
|
let limit = q.limit.unwrap_or(20);
|
|
|
|
|
let offset = (page - 1) * limit;
|
|
|
|
|
|
|
|
|
|
let jobs = sqlx::query_as!(
|
|
|
|
|
db::models::job::Job,
|
|
|
|
|
r#"
|
|
|
|
|
SELECT *
|
|
|
|
|
FROM jobs
|
|
|
|
|
WHERE status = 'PENDING_APPROVAL'
|
|
|
|
|
ORDER BY created_at ASC
|
|
|
|
|
LIMIT $1 OFFSET $2
|
|
|
|
|
"#,
|
|
|
|
|
limit,
|
|
|
|
|
offset
|
|
|
|
|
)
|
|
|
|
|
.fetch_all(&state.pool)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
let requirements = sqlx::query_as!(
|
|
|
|
|
db::models::requirement::Requirement,
|
|
|
|
|
r#"
|
|
|
|
|
SELECT *
|
|
|
|
|
FROM requirements
|
|
|
|
|
WHERE status = 'PENDING_APPROVAL'
|
|
|
|
|
ORDER BY created_at ASC
|
|
|
|
|
LIMIT $1 OFFSET $2
|
|
|
|
|
"#,
|
|
|
|
|
limit,
|
|
|
|
|
offset
|
|
|
|
|
)
|
|
|
|
|
.fetch_all(&state.pool)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
let company_profiles = sqlx::query!(
|
|
|
|
|
r#"
|
|
|
|
|
SELECT user_id, status, updated_at
|
|
|
|
|
FROM company_profiles
|
|
|
|
|
WHERE status = 'PENDING'
|
|
|
|
|
ORDER BY updated_at ASC
|
|
|
|
|
LIMIT $1 OFFSET $2
|
|
|
|
|
"#,
|
|
|
|
|
limit,
|
|
|
|
|
offset
|
|
|
|
|
)
|
|
|
|
|
.fetch_all(&state.pool)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
let customer_profiles = sqlx::query!(
|
|
|
|
|
r#"
|
|
|
|
|
SELECT user_id, status, updated_at
|
|
|
|
|
FROM customer_profiles
|
|
|
|
|
WHERE status = 'PENDING'
|
|
|
|
|
ORDER BY updated_at ASC
|
|
|
|
|
LIMIT $1 OFFSET $2
|
|
|
|
|
"#,
|
|
|
|
|
limit,
|
|
|
|
|
offset
|
|
|
|
|
)
|
|
|
|
|
.fetch_all(&state.pool)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
let photographer_profiles = sqlx::query!(
|
|
|
|
|
r#"
|
|
|
|
|
SELECT user_id, status, updated_at
|
|
|
|
|
FROM photographer_profiles
|
|
|
|
|
WHERE status = 'PENDING'
|
|
|
|
|
ORDER BY updated_at ASC
|
|
|
|
|
LIMIT $1 OFFSET $2
|
|
|
|
|
"#,
|
|
|
|
|
limit,
|
|
|
|
|
offset
|
|
|
|
|
)
|
|
|
|
|
.fetch_all(&state.pool)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
let makeup_profiles = sqlx::query!(
|
|
|
|
|
r#"
|
|
|
|
|
SELECT user_id, status, updated_at
|
|
|
|
|
FROM makeup_artist_profiles
|
|
|
|
|
WHERE status = 'PENDING'
|
|
|
|
|
ORDER BY updated_at ASC
|
|
|
|
|
LIMIT $1 OFFSET $2
|
|
|
|
|
"#,
|
|
|
|
|
limit,
|
|
|
|
|
offset
|
|
|
|
|
)
|
|
|
|
|
.fetch_all(&state.pool)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
let tutor_profiles = sqlx::query!(
|
|
|
|
|
r#"
|
|
|
|
|
SELECT user_id, status, updated_at
|
|
|
|
|
FROM tutor_profiles
|
|
|
|
|
WHERE status = 'PENDING'
|
|
|
|
|
ORDER BY updated_at ASC
|
|
|
|
|
LIMIT $1 OFFSET $2
|
|
|
|
|
"#,
|
|
|
|
|
limit,
|
|
|
|
|
offset
|
|
|
|
|
)
|
|
|
|
|
.fetch_all(&state.pool)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
let developer_profiles = sqlx::query!(
|
|
|
|
|
r#"
|
|
|
|
|
SELECT user_id, status, updated_at
|
|
|
|
|
FROM developer_profiles
|
|
|
|
|
WHERE status = 'PENDING'
|
|
|
|
|
ORDER BY updated_at ASC
|
|
|
|
|
LIMIT $1 OFFSET $2
|
|
|
|
|
"#,
|
|
|
|
|
limit,
|
|
|
|
|
offset
|
|
|
|
|
)
|
|
|
|
|
.fetch_all(&state.pool)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
let video_editor_profiles = sqlx::query!(
|
|
|
|
|
r#"
|
|
|
|
|
SELECT user_id, status, updated_at
|
|
|
|
|
FROM video_editor_profiles
|
|
|
|
|
WHERE status = 'PENDING'
|
|
|
|
|
ORDER BY updated_at ASC
|
|
|
|
|
LIMIT $1 OFFSET $2
|
|
|
|
|
"#,
|
|
|
|
|
limit,
|
|
|
|
|
offset
|
|
|
|
|
)
|
|
|
|
|
.fetch_all(&state.pool)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
let graphic_designer_profiles = sqlx::query!(
|
|
|
|
|
r#"
|
|
|
|
|
SELECT user_id, status, updated_at
|
|
|
|
|
FROM graphic_designer_profiles
|
|
|
|
|
WHERE status = 'PENDING'
|
|
|
|
|
ORDER BY updated_at ASC
|
|
|
|
|
LIMIT $1 OFFSET $2
|
|
|
|
|
"#,
|
|
|
|
|
limit,
|
|
|
|
|
offset
|
|
|
|
|
)
|
|
|
|
|
.fetch_all(&state.pool)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
let social_media_manager_profiles = sqlx::query!(
|
|
|
|
|
r#"
|
|
|
|
|
SELECT user_id, status, updated_at
|
|
|
|
|
FROM social_media_manager_profiles
|
|
|
|
|
WHERE status = 'PENDING'
|
|
|
|
|
ORDER BY updated_at ASC
|
|
|
|
|
LIMIT $1 OFFSET $2
|
|
|
|
|
"#,
|
|
|
|
|
limit,
|
|
|
|
|
offset
|
|
|
|
|
)
|
|
|
|
|
.fetch_all(&state.pool)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
let fitness_trainer_profiles = sqlx::query!(
|
|
|
|
|
r#"
|
|
|
|
|
SELECT user_id, status, updated_at
|
|
|
|
|
FROM fitness_trainer_profiles
|
|
|
|
|
WHERE status = 'PENDING'
|
|
|
|
|
ORDER BY updated_at ASC
|
|
|
|
|
LIMIT $1 OFFSET $2
|
|
|
|
|
"#,
|
|
|
|
|
limit,
|
|
|
|
|
offset
|
|
|
|
|
)
|
|
|
|
|
.fetch_all(&state.pool)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
let catering_profiles = sqlx::query!(
|
|
|
|
|
r#"
|
|
|
|
|
SELECT user_id, status, updated_at
|
|
|
|
|
FROM catering_service_profiles
|
|
|
|
|
WHERE status = 'PENDING'
|
|
|
|
|
ORDER BY updated_at ASC
|
|
|
|
|
LIMIT $1 OFFSET $2
|
|
|
|
|
"#,
|
|
|
|
|
limit,
|
|
|
|
|
offset
|
|
|
|
|
)
|
|
|
|
|
.fetch_all(&state.pool)
|
|
|
|
|
.await;
|
|
|
|
|
|
|
|
|
|
match (
|
|
|
|
|
jobs,
|
|
|
|
|
requirements,
|
|
|
|
|
company_profiles,
|
|
|
|
|
customer_profiles,
|
|
|
|
|
photographer_profiles,
|
|
|
|
|
makeup_profiles,
|
|
|
|
|
tutor_profiles,
|
|
|
|
|
developer_profiles,
|
|
|
|
|
video_editor_profiles,
|
|
|
|
|
graphic_designer_profiles,
|
|
|
|
|
social_media_manager_profiles,
|
|
|
|
|
fitness_trainer_profiles,
|
|
|
|
|
catering_profiles,
|
|
|
|
|
) {
|
|
|
|
|
(
|
|
|
|
|
Ok(jobs),
|
|
|
|
|
Ok(requirements),
|
|
|
|
|
Ok(company_profiles),
|
|
|
|
|
Ok(customer_profiles),
|
|
|
|
|
Ok(photographer_profiles),
|
|
|
|
|
Ok(makeup_profiles),
|
|
|
|
|
Ok(tutor_profiles),
|
|
|
|
|
Ok(developer_profiles),
|
|
|
|
|
Ok(video_editor_profiles),
|
|
|
|
|
Ok(graphic_designer_profiles),
|
|
|
|
|
Ok(social_media_manager_profiles),
|
|
|
|
|
Ok(fitness_trainer_profiles),
|
|
|
|
|
Ok(catering_profiles),
|
|
|
|
|
) => (
|
|
|
|
|
StatusCode::OK,
|
|
|
|
|
Json(serde_json::json!({
|
|
|
|
|
"jobs": jobs,
|
|
|
|
|
"requirements": requirements,
|
2026-03-22 15:55:29 +01:00
|
|
|
"profiles_summary": {
|
|
|
|
|
"company": company_profiles.len(),
|
|
|
|
|
"customer": customer_profiles.len(),
|
|
|
|
|
"photographer": photographer_profiles.len(),
|
|
|
|
|
"makeup_artist": makeup_profiles.len(),
|
|
|
|
|
"tutor": tutor_profiles.len(),
|
|
|
|
|
"developer": developer_profiles.len(),
|
|
|
|
|
"video_editor": video_editor_profiles.len(),
|
|
|
|
|
"graphic_designer": graphic_designer_profiles.len(),
|
|
|
|
|
"social_media_manager": social_media_manager_profiles.len(),
|
|
|
|
|
"fitness_trainer": fitness_trainer_profiles.len(),
|
|
|
|
|
"catering_services": catering_profiles.len()
|
2026-03-19 00:30:23 +01:00
|
|
|
},
|
|
|
|
|
"pagination": { "page": page, "limit": limit }
|
|
|
|
|
})),
|
|
|
|
|
)
|
|
|
|
|
.into_response(),
|
|
|
|
|
(Err(e), _, _, _, _, _, _, _, _, _, _, _, _)
|
|
|
|
|
| (_, Err(e), _, _, _, _, _, _, _, _, _, _, _)
|
|
|
|
|
| (_, _, Err(e), _, _, _, _, _, _, _, _, _, _)
|
|
|
|
|
| (_, _, _, Err(e), _, _, _, _, _, _, _, _, _)
|
|
|
|
|
| (_, _, _, _, Err(e), _, _, _, _, _, _, _, _)
|
|
|
|
|
| (_, _, _, _, _, Err(e), _, _, _, _, _, _, _)
|
|
|
|
|
| (_, _, _, _, _, _, Err(e), _, _, _, _, _, _)
|
|
|
|
|
| (_, _, _, _, _, _, _, Err(e), _, _, _, _, _)
|
|
|
|
|
| (_, _, _, _, _, _, _, _, Err(e), _, _, _, _)
|
|
|
|
|
| (_, _, _, _, _, _, _, _, _, Err(e), _, _, _)
|
|
|
|
|
| (_, _, _, _, _, _, _, _, _, _, Err(e), _, _)
|
|
|
|
|
| (_, _, _, _, _, _, _, _, _, _, _, Err(e), _)
|
|
|
|
|
| (_, _, _, _, _, _, _, _, _, _, _, _, Err(e)) => {
|
|
|
|
|
(StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn approve_company_profile(
|
|
|
|
|
auth: AuthUser,
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path(user_id): Path<Uuid>,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
if let Err(e) = require_admin(&auth) {
|
|
|
|
|
return e.into_response();
|
|
|
|
|
}
|
|
|
|
|
match sqlx::query!(
|
|
|
|
|
"UPDATE company_profiles SET status = 'APPROVED', updated_at = NOW() WHERE user_id = $1",
|
|
|
|
|
user_id
|
|
|
|
|
)
|
|
|
|
|
.execute(&state.pool)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(result) if result.rows_affected() > 0 => {
|
feat(phase1): wire email notifications, shared email crate, AppState for services
- Create crates/email shared Mailer with 18+ templates (auth, approvals, jobs, leads, tracecoins)
- users/mail.rs now re-exports from shared crate (lettre dep removed)
- Wire password changed/reset emails in users auth handlers
- Wire profile approval/rejection emails in users approvals handlers (company, customer, all 9 professional types)
- Wire job approved/rejected emails in users approvals handlers
- Wire requirement approved email in users approvals handlers
- Add AppState (pool + mail) to companies service; wire submit_job and update_application_status emails
- Add AppState (pool + mail) to customers service; wire submit_requirement, approve_request, reject_request emails (incl. contact-exchange on lead acceptance)
- Add AppState (pool + storage) to job_seekers service with resume upload multipart handler
- Wire lead cancellation and accepted-leads handlers in contracts/profession_shared.rs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 01:42:48 +02:00
|
|
|
if let Ok(user) = UserRepository::get_by_id(&state.pool, user_id).await {
|
|
|
|
|
let _ = state.mail.send_approval_approved_email(&user.email, &user.full_name, "Company").await;
|
|
|
|
|
}
|
2026-03-19 00:30:23 +01:00
|
|
|
(StatusCode::OK, Json(serde_json::json!({ "user_id": user_id, "status": "APPROVED" }))).into_response()
|
|
|
|
|
}
|
|
|
|
|
Ok(_) => (StatusCode::NOT_FOUND, "Company profile not found").into_response(),
|
|
|
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn reject_company_profile(
|
|
|
|
|
auth: AuthUser,
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path(user_id): Path<Uuid>,
|
feat(phase1): wire email notifications, shared email crate, AppState for services
- Create crates/email shared Mailer with 18+ templates (auth, approvals, jobs, leads, tracecoins)
- users/mail.rs now re-exports from shared crate (lettre dep removed)
- Wire password changed/reset emails in users auth handlers
- Wire profile approval/rejection emails in users approvals handlers (company, customer, all 9 professional types)
- Wire job approved/rejected emails in users approvals handlers
- Wire requirement approved email in users approvals handlers
- Add AppState (pool + mail) to companies service; wire submit_job and update_application_status emails
- Add AppState (pool + mail) to customers service; wire submit_requirement, approve_request, reject_request emails (incl. contact-exchange on lead acceptance)
- Add AppState (pool + storage) to job_seekers service with resume upload multipart handler
- Wire lead cancellation and accepted-leads handlers in contracts/profession_shared.rs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 01:42:48 +02:00
|
|
|
Json(payload): Json<RejectPayload>,
|
2026-03-19 00:30:23 +01:00
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
if let Err(e) = require_admin(&auth) {
|
|
|
|
|
return e.into_response();
|
|
|
|
|
}
|
feat(phase1): wire email notifications, shared email crate, AppState for services
- Create crates/email shared Mailer with 18+ templates (auth, approvals, jobs, leads, tracecoins)
- users/mail.rs now re-exports from shared crate (lettre dep removed)
- Wire password changed/reset emails in users auth handlers
- Wire profile approval/rejection emails in users approvals handlers (company, customer, all 9 professional types)
- Wire job approved/rejected emails in users approvals handlers
- Wire requirement approved email in users approvals handlers
- Add AppState (pool + mail) to companies service; wire submit_job and update_application_status emails
- Add AppState (pool + mail) to customers service; wire submit_requirement, approve_request, reject_request emails (incl. contact-exchange on lead acceptance)
- Add AppState (pool + storage) to job_seekers service with resume upload multipart handler
- Wire lead cancellation and accepted-leads handlers in contracts/profession_shared.rs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 01:42:48 +02:00
|
|
|
let reason = payload.reason.unwrap_or_else(|| "Profile rejected".to_string());
|
2026-03-19 00:30:23 +01:00
|
|
|
match sqlx::query!(
|
|
|
|
|
"UPDATE company_profiles SET status = 'REJECTED', updated_at = NOW() WHERE user_id = $1",
|
|
|
|
|
user_id
|
|
|
|
|
)
|
|
|
|
|
.execute(&state.pool)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(result) if result.rows_affected() > 0 => {
|
feat(phase1): wire email notifications, shared email crate, AppState for services
- Create crates/email shared Mailer with 18+ templates (auth, approvals, jobs, leads, tracecoins)
- users/mail.rs now re-exports from shared crate (lettre dep removed)
- Wire password changed/reset emails in users auth handlers
- Wire profile approval/rejection emails in users approvals handlers (company, customer, all 9 professional types)
- Wire job approved/rejected emails in users approvals handlers
- Wire requirement approved email in users approvals handlers
- Add AppState (pool + mail) to companies service; wire submit_job and update_application_status emails
- Add AppState (pool + mail) to customers service; wire submit_requirement, approve_request, reject_request emails (incl. contact-exchange on lead acceptance)
- Add AppState (pool + storage) to job_seekers service with resume upload multipart handler
- Wire lead cancellation and accepted-leads handlers in contracts/profession_shared.rs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 01:42:48 +02:00
|
|
|
if let Ok(user) = UserRepository::get_by_id(&state.pool, user_id).await {
|
|
|
|
|
let _ = state.mail.send_approval_rejected_email(&user.email, &user.full_name, "Company", &reason).await;
|
|
|
|
|
}
|
2026-03-19 00:30:23 +01:00
|
|
|
(StatusCode::OK, Json(serde_json::json!({ "user_id": user_id, "status": "REJECTED" }))).into_response()
|
|
|
|
|
}
|
|
|
|
|
Ok(_) => (StatusCode::NOT_FOUND, "Company profile not found").into_response(),
|
|
|
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn approve_customer_profile(
|
|
|
|
|
auth: AuthUser,
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path(user_id): Path<Uuid>,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
if let Err(e) = require_admin(&auth) {
|
|
|
|
|
return e.into_response();
|
|
|
|
|
}
|
|
|
|
|
match sqlx::query!(
|
|
|
|
|
"UPDATE customer_profiles SET status = 'APPROVED', updated_at = NOW() WHERE user_id = $1",
|
|
|
|
|
user_id
|
|
|
|
|
)
|
|
|
|
|
.execute(&state.pool)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(result) if result.rows_affected() > 0 => {
|
feat(phase1): wire email notifications, shared email crate, AppState for services
- Create crates/email shared Mailer with 18+ templates (auth, approvals, jobs, leads, tracecoins)
- users/mail.rs now re-exports from shared crate (lettre dep removed)
- Wire password changed/reset emails in users auth handlers
- Wire profile approval/rejection emails in users approvals handlers (company, customer, all 9 professional types)
- Wire job approved/rejected emails in users approvals handlers
- Wire requirement approved email in users approvals handlers
- Add AppState (pool + mail) to companies service; wire submit_job and update_application_status emails
- Add AppState (pool + mail) to customers service; wire submit_requirement, approve_request, reject_request emails (incl. contact-exchange on lead acceptance)
- Add AppState (pool + storage) to job_seekers service with resume upload multipart handler
- Wire lead cancellation and accepted-leads handlers in contracts/profession_shared.rs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 01:42:48 +02:00
|
|
|
if let Ok(user) = UserRepository::get_by_id(&state.pool, user_id).await {
|
|
|
|
|
let _ = state.mail.send_approval_approved_email(&user.email, &user.full_name, "Customer").await;
|
|
|
|
|
}
|
2026-03-19 00:30:23 +01:00
|
|
|
(StatusCode::OK, Json(serde_json::json!({ "user_id": user_id, "status": "APPROVED" }))).into_response()
|
|
|
|
|
}
|
|
|
|
|
Ok(_) => (StatusCode::NOT_FOUND, "Customer profile not found").into_response(),
|
|
|
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn reject_customer_profile(
|
|
|
|
|
auth: AuthUser,
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path(user_id): Path<Uuid>,
|
feat(phase1): wire email notifications, shared email crate, AppState for services
- Create crates/email shared Mailer with 18+ templates (auth, approvals, jobs, leads, tracecoins)
- users/mail.rs now re-exports from shared crate (lettre dep removed)
- Wire password changed/reset emails in users auth handlers
- Wire profile approval/rejection emails in users approvals handlers (company, customer, all 9 professional types)
- Wire job approved/rejected emails in users approvals handlers
- Wire requirement approved email in users approvals handlers
- Add AppState (pool + mail) to companies service; wire submit_job and update_application_status emails
- Add AppState (pool + mail) to customers service; wire submit_requirement, approve_request, reject_request emails (incl. contact-exchange on lead acceptance)
- Add AppState (pool + storage) to job_seekers service with resume upload multipart handler
- Wire lead cancellation and accepted-leads handlers in contracts/profession_shared.rs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 01:42:48 +02:00
|
|
|
Json(payload): Json<RejectPayload>,
|
2026-03-19 00:30:23 +01:00
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
if let Err(e) = require_admin(&auth) {
|
|
|
|
|
return e.into_response();
|
|
|
|
|
}
|
feat(phase1): wire email notifications, shared email crate, AppState for services
- Create crates/email shared Mailer with 18+ templates (auth, approvals, jobs, leads, tracecoins)
- users/mail.rs now re-exports from shared crate (lettre dep removed)
- Wire password changed/reset emails in users auth handlers
- Wire profile approval/rejection emails in users approvals handlers (company, customer, all 9 professional types)
- Wire job approved/rejected emails in users approvals handlers
- Wire requirement approved email in users approvals handlers
- Add AppState (pool + mail) to companies service; wire submit_job and update_application_status emails
- Add AppState (pool + mail) to customers service; wire submit_requirement, approve_request, reject_request emails (incl. contact-exchange on lead acceptance)
- Add AppState (pool + storage) to job_seekers service with resume upload multipart handler
- Wire lead cancellation and accepted-leads handlers in contracts/profession_shared.rs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 01:42:48 +02:00
|
|
|
let reason = payload.reason.unwrap_or_else(|| "Profile rejected".to_string());
|
2026-03-19 00:30:23 +01:00
|
|
|
match sqlx::query!(
|
|
|
|
|
"UPDATE customer_profiles SET status = 'REJECTED', updated_at = NOW() WHERE user_id = $1",
|
|
|
|
|
user_id
|
|
|
|
|
)
|
|
|
|
|
.execute(&state.pool)
|
|
|
|
|
.await
|
|
|
|
|
{
|
|
|
|
|
Ok(result) if result.rows_affected() > 0 => {
|
feat(phase1): wire email notifications, shared email crate, AppState for services
- Create crates/email shared Mailer with 18+ templates (auth, approvals, jobs, leads, tracecoins)
- users/mail.rs now re-exports from shared crate (lettre dep removed)
- Wire password changed/reset emails in users auth handlers
- Wire profile approval/rejection emails in users approvals handlers (company, customer, all 9 professional types)
- Wire job approved/rejected emails in users approvals handlers
- Wire requirement approved email in users approvals handlers
- Add AppState (pool + mail) to companies service; wire submit_job and update_application_status emails
- Add AppState (pool + mail) to customers service; wire submit_requirement, approve_request, reject_request emails (incl. contact-exchange on lead acceptance)
- Add AppState (pool + storage) to job_seekers service with resume upload multipart handler
- Wire lead cancellation and accepted-leads handlers in contracts/profession_shared.rs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 01:42:48 +02:00
|
|
|
if let Ok(user) = UserRepository::get_by_id(&state.pool, user_id).await {
|
|
|
|
|
let _ = state.mail.send_approval_rejected_email(&user.email, &user.full_name, "Customer", &reason).await;
|
|
|
|
|
}
|
2026-03-19 00:30:23 +01:00
|
|
|
(StatusCode::OK, Json(serde_json::json!({ "user_id": user_id, "status": "REJECTED" }))).into_response()
|
|
|
|
|
}
|
|
|
|
|
Ok(_) => (StatusCode::NOT_FOUND, "Customer profile not found").into_response(),
|
|
|
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn professional_profile_table(role_key: &str) -> Option<&'static str> {
|
|
|
|
|
match role_key {
|
|
|
|
|
"PHOTOGRAPHER" => Some("photographer_profiles"),
|
|
|
|
|
"MAKEUP_ARTIST" => Some("makeup_artist_profiles"),
|
|
|
|
|
"TUTOR" => Some("tutor_profiles"),
|
|
|
|
|
"DEVELOPER" => Some("developer_profiles"),
|
|
|
|
|
"VIDEO_EDITOR" => Some("video_editor_profiles"),
|
|
|
|
|
"GRAPHIC_DESIGNER" => Some("graphic_designer_profiles"),
|
|
|
|
|
"SOCIAL_MEDIA_MANAGER" => Some("social_media_manager_profiles"),
|
|
|
|
|
"FITNESS_TRAINER" => Some("fitness_trainer_profiles"),
|
|
|
|
|
"CATERING_SERVICES" => Some("catering_service_profiles"),
|
|
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat(phase1): wire email notifications, shared email crate, AppState for services
- Create crates/email shared Mailer with 18+ templates (auth, approvals, jobs, leads, tracecoins)
- users/mail.rs now re-exports from shared crate (lettre dep removed)
- Wire password changed/reset emails in users auth handlers
- Wire profile approval/rejection emails in users approvals handlers (company, customer, all 9 professional types)
- Wire job approved/rejected emails in users approvals handlers
- Wire requirement approved email in users approvals handlers
- Add AppState (pool + mail) to companies service; wire submit_job and update_application_status emails
- Add AppState (pool + mail) to customers service; wire submit_requirement, approve_request, reject_request emails (incl. contact-exchange on lead acceptance)
- Add AppState (pool + storage) to job_seekers service with resume upload multipart handler
- Wire lead cancellation and accepted-leads handlers in contracts/profession_shared.rs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 01:42:48 +02:00
|
|
|
fn role_key_to_display(role_key: &str) -> &'static str {
|
|
|
|
|
match role_key {
|
|
|
|
|
"PHOTOGRAPHER" => "Photographer",
|
|
|
|
|
"MAKEUP_ARTIST" => "Makeup Artist",
|
|
|
|
|
"TUTOR" => "Tutor",
|
|
|
|
|
"DEVELOPER" => "Developer",
|
|
|
|
|
"VIDEO_EDITOR" => "Video Editor",
|
|
|
|
|
"GRAPHIC_DESIGNER" => "Graphic Designer",
|
|
|
|
|
"SOCIAL_MEDIA_MANAGER" => "Social Media Manager",
|
|
|
|
|
"FITNESS_TRAINER" => "Fitness Trainer",
|
|
|
|
|
"CATERING_SERVICES" => "Catering Services",
|
|
|
|
|
_ => role_key,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-19 00:30:23 +01:00
|
|
|
async fn approve_professional_profile(
|
|
|
|
|
auth: AuthUser,
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path((role_key, user_id)): Path<(String, Uuid)>,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
if let Err(e) = require_admin(&auth) {
|
|
|
|
|
return e.into_response();
|
|
|
|
|
}
|
|
|
|
|
let role_key = role_key.to_uppercase();
|
|
|
|
|
let Some(table) = professional_profile_table(&role_key) else {
|
|
|
|
|
return (StatusCode::BAD_REQUEST, "Unsupported professional role_key").into_response();
|
|
|
|
|
};
|
|
|
|
|
let query = format!(
|
|
|
|
|
"UPDATE {} SET status = 'APPROVED', rejection_reason = NULL, approved_at = NOW(), updated_at = NOW() WHERE user_id = $1",
|
|
|
|
|
table
|
|
|
|
|
);
|
|
|
|
|
match sqlx::query(&query).bind(user_id).execute(&state.pool).await {
|
feat(phase1): wire email notifications, shared email crate, AppState for services
- Create crates/email shared Mailer with 18+ templates (auth, approvals, jobs, leads, tracecoins)
- users/mail.rs now re-exports from shared crate (lettre dep removed)
- Wire password changed/reset emails in users auth handlers
- Wire profile approval/rejection emails in users approvals handlers (company, customer, all 9 professional types)
- Wire job approved/rejected emails in users approvals handlers
- Wire requirement approved email in users approvals handlers
- Add AppState (pool + mail) to companies service; wire submit_job and update_application_status emails
- Add AppState (pool + mail) to customers service; wire submit_requirement, approve_request, reject_request emails (incl. contact-exchange on lead acceptance)
- Add AppState (pool + storage) to job_seekers service with resume upload multipart handler
- Wire lead cancellation and accepted-leads handlers in contracts/profession_shared.rs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 01:42:48 +02:00
|
|
|
Ok(result) if result.rows_affected() > 0 => {
|
|
|
|
|
let display = role_key_to_display(&role_key);
|
|
|
|
|
if let Ok(user) = UserRepository::get_by_id(&state.pool, user_id).await {
|
|
|
|
|
let _ = state.mail.send_approval_approved_email(&user.email, &user.full_name, display).await;
|
|
|
|
|
}
|
|
|
|
|
(StatusCode::OK, Json(serde_json::json!({ "user_id": user_id, "role_key": role_key, "status": "APPROVED" }))).into_response()
|
|
|
|
|
}
|
2026-03-19 00:30:23 +01:00
|
|
|
Ok(_) => (StatusCode::NOT_FOUND, "Professional profile not found").into_response(),
|
|
|
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn reject_professional_profile(
|
|
|
|
|
auth: AuthUser,
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path((role_key, user_id)): Path<(String, Uuid)>,
|
|
|
|
|
Json(payload): Json<RejectPayload>,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
if let Err(e) = require_admin(&auth) {
|
|
|
|
|
return e.into_response();
|
|
|
|
|
}
|
|
|
|
|
let role_key = role_key.to_uppercase();
|
|
|
|
|
let Some(table) = professional_profile_table(&role_key) else {
|
|
|
|
|
return (StatusCode::BAD_REQUEST, "Unsupported professional role_key").into_response();
|
|
|
|
|
};
|
feat(phase1): wire email notifications, shared email crate, AppState for services
- Create crates/email shared Mailer with 18+ templates (auth, approvals, jobs, leads, tracecoins)
- users/mail.rs now re-exports from shared crate (lettre dep removed)
- Wire password changed/reset emails in users auth handlers
- Wire profile approval/rejection emails in users approvals handlers (company, customer, all 9 professional types)
- Wire job approved/rejected emails in users approvals handlers
- Wire requirement approved email in users approvals handlers
- Add AppState (pool + mail) to companies service; wire submit_job and update_application_status emails
- Add AppState (pool + mail) to customers service; wire submit_requirement, approve_request, reject_request emails (incl. contact-exchange on lead acceptance)
- Add AppState (pool + storage) to job_seekers service with resume upload multipart handler
- Wire lead cancellation and accepted-leads handlers in contracts/profession_shared.rs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 01:42:48 +02:00
|
|
|
let reason = payload.reason.unwrap_or_else(|| "Profile rejected".to_string());
|
2026-03-19 00:30:23 +01:00
|
|
|
let query = format!(
|
|
|
|
|
"UPDATE {} SET status = 'REJECTED', rejection_reason = $2, updated_at = NOW() WHERE user_id = $1",
|
|
|
|
|
table
|
|
|
|
|
);
|
|
|
|
|
match sqlx::query(&query)
|
|
|
|
|
.bind(user_id)
|
feat(phase1): wire email notifications, shared email crate, AppState for services
- Create crates/email shared Mailer with 18+ templates (auth, approvals, jobs, leads, tracecoins)
- users/mail.rs now re-exports from shared crate (lettre dep removed)
- Wire password changed/reset emails in users auth handlers
- Wire profile approval/rejection emails in users approvals handlers (company, customer, all 9 professional types)
- Wire job approved/rejected emails in users approvals handlers
- Wire requirement approved email in users approvals handlers
- Add AppState (pool + mail) to companies service; wire submit_job and update_application_status emails
- Add AppState (pool + mail) to customers service; wire submit_requirement, approve_request, reject_request emails (incl. contact-exchange on lead acceptance)
- Add AppState (pool + storage) to job_seekers service with resume upload multipart handler
- Wire lead cancellation and accepted-leads handlers in contracts/profession_shared.rs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 01:42:48 +02:00
|
|
|
.bind(&reason)
|
2026-03-19 00:30:23 +01:00
|
|
|
.execute(&state.pool)
|
|
|
|
|
.await
|
|
|
|
|
{
|
feat(phase1): wire email notifications, shared email crate, AppState for services
- Create crates/email shared Mailer with 18+ templates (auth, approvals, jobs, leads, tracecoins)
- users/mail.rs now re-exports from shared crate (lettre dep removed)
- Wire password changed/reset emails in users auth handlers
- Wire profile approval/rejection emails in users approvals handlers (company, customer, all 9 professional types)
- Wire job approved/rejected emails in users approvals handlers
- Wire requirement approved email in users approvals handlers
- Add AppState (pool + mail) to companies service; wire submit_job and update_application_status emails
- Add AppState (pool + mail) to customers service; wire submit_requirement, approve_request, reject_request emails (incl. contact-exchange on lead acceptance)
- Add AppState (pool + storage) to job_seekers service with resume upload multipart handler
- Wire lead cancellation and accepted-leads handlers in contracts/profession_shared.rs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 01:42:48 +02:00
|
|
|
Ok(result) if result.rows_affected() > 0 => {
|
|
|
|
|
let display = role_key_to_display(&role_key);
|
|
|
|
|
if let Ok(user) = UserRepository::get_by_id(&state.pool, user_id).await {
|
|
|
|
|
let _ = state.mail.send_approval_rejected_email(&user.email, &user.full_name, display, &reason).await;
|
|
|
|
|
}
|
|
|
|
|
(StatusCode::OK, Json(serde_json::json!({ "user_id": user_id, "role_key": role_key, "status": "REJECTED" }))).into_response()
|
|
|
|
|
}
|
2026-03-19 00:30:23 +01:00
|
|
|
Ok(_) => (StatusCode::NOT_FOUND, "Professional profile not found").into_response(),
|
|
|
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn approve_job(
|
|
|
|
|
auth: AuthUser,
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path(id): Path<Uuid>,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
if let Err(e) = require_admin(&auth) {
|
|
|
|
|
return e.into_response();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let existing = match JobRepository::get_by_id(&state.pool, id).await {
|
|
|
|
|
Ok(Some(job)) => job,
|
|
|
|
|
Ok(None) => return (StatusCode::NOT_FOUND, "Job not found").into_response(),
|
|
|
|
|
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if existing.status != "PENDING_APPROVAL" {
|
|
|
|
|
return (StatusCode::BAD_REQUEST, "Job is not pending approval").into_response();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match JobRepository::approve(&state.pool, id, auth.user_id).await {
|
feat(phase1): wire email notifications, shared email crate, AppState for services
- Create crates/email shared Mailer with 18+ templates (auth, approvals, jobs, leads, tracecoins)
- users/mail.rs now re-exports from shared crate (lettre dep removed)
- Wire password changed/reset emails in users auth handlers
- Wire profile approval/rejection emails in users approvals handlers (company, customer, all 9 professional types)
- Wire job approved/rejected emails in users approvals handlers
- Wire requirement approved email in users approvals handlers
- Add AppState (pool + mail) to companies service; wire submit_job and update_application_status emails
- Add AppState (pool + mail) to customers service; wire submit_requirement, approve_request, reject_request emails (incl. contact-exchange on lead acceptance)
- Add AppState (pool + storage) to job_seekers service with resume upload multipart handler
- Wire lead cancellation and accepted-leads handlers in contracts/profession_shared.rs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 01:42:48 +02:00
|
|
|
Ok(job) => {
|
|
|
|
|
// Notify company user (ignore failures)
|
|
|
|
|
let company_info = sqlx::query_as::<_, (String, String)>(
|
|
|
|
|
"SELECT u.full_name, u.email FROM companies c JOIN users u ON u.id = c.user_id WHERE c.id = $1",
|
|
|
|
|
)
|
|
|
|
|
.bind(existing.company_id)
|
|
|
|
|
.fetch_optional(&state.pool)
|
|
|
|
|
.await;
|
|
|
|
|
if let Ok(Some((name, email))) = company_info {
|
|
|
|
|
let _ = state.mail.send_job_approved_email(&email, &name, &existing.title).await;
|
|
|
|
|
}
|
|
|
|
|
(StatusCode::OK, Json(job)).into_response()
|
|
|
|
|
}
|
2026-03-19 00:30:23 +01:00
|
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn reject_job(
|
|
|
|
|
auth: AuthUser,
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path(id): Path<Uuid>,
|
|
|
|
|
Json(payload): Json<RejectPayload>,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
if let Err(e) = require_admin(&auth) {
|
|
|
|
|
return e.into_response();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let existing = match JobRepository::get_by_id(&state.pool, id).await {
|
|
|
|
|
Ok(Some(job)) => job,
|
|
|
|
|
Ok(None) => return (StatusCode::NOT_FOUND, "Job not found").into_response(),
|
|
|
|
|
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if existing.status != "PENDING_APPROVAL" {
|
|
|
|
|
return (StatusCode::BAD_REQUEST, "Job is not pending approval").into_response();
|
|
|
|
|
}
|
|
|
|
|
|
feat(phase1): wire email notifications, shared email crate, AppState for services
- Create crates/email shared Mailer with 18+ templates (auth, approvals, jobs, leads, tracecoins)
- users/mail.rs now re-exports from shared crate (lettre dep removed)
- Wire password changed/reset emails in users auth handlers
- Wire profile approval/rejection emails in users approvals handlers (company, customer, all 9 professional types)
- Wire job approved/rejected emails in users approvals handlers
- Wire requirement approved email in users approvals handlers
- Add AppState (pool + mail) to companies service; wire submit_job and update_application_status emails
- Add AppState (pool + mail) to customers service; wire submit_requirement, approve_request, reject_request emails (incl. contact-exchange on lead acceptance)
- Add AppState (pool + storage) to job_seekers service with resume upload multipart handler
- Wire lead cancellation and accepted-leads handlers in contracts/profession_shared.rs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 01:42:48 +02:00
|
|
|
let reason = payload.reason.clone();
|
2026-03-19 00:30:23 +01:00
|
|
|
match JobRepository::reject(&state.pool, id, payload.reason).await {
|
feat(phase1): wire email notifications, shared email crate, AppState for services
- Create crates/email shared Mailer with 18+ templates (auth, approvals, jobs, leads, tracecoins)
- users/mail.rs now re-exports from shared crate (lettre dep removed)
- Wire password changed/reset emails in users auth handlers
- Wire profile approval/rejection emails in users approvals handlers (company, customer, all 9 professional types)
- Wire job approved/rejected emails in users approvals handlers
- Wire requirement approved email in users approvals handlers
- Add AppState (pool + mail) to companies service; wire submit_job and update_application_status emails
- Add AppState (pool + mail) to customers service; wire submit_requirement, approve_request, reject_request emails (incl. contact-exchange on lead acceptance)
- Add AppState (pool + storage) to job_seekers service with resume upload multipart handler
- Wire lead cancellation and accepted-leads handlers in contracts/profession_shared.rs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 01:42:48 +02:00
|
|
|
Ok(job) => {
|
|
|
|
|
let company_info = sqlx::query_as::<_, (String, String)>(
|
|
|
|
|
"SELECT u.full_name, u.email FROM companies c JOIN users u ON u.id = c.user_id WHERE c.id = $1",
|
|
|
|
|
)
|
|
|
|
|
.bind(existing.company_id)
|
|
|
|
|
.fetch_optional(&state.pool)
|
|
|
|
|
.await;
|
|
|
|
|
if let Ok(Some((name, email))) = company_info {
|
|
|
|
|
let r = reason.as_deref().unwrap_or("Rejected by admin");
|
|
|
|
|
let _ = state.mail.send_job_rejected_email(&email, &name, &existing.title, r).await;
|
|
|
|
|
}
|
|
|
|
|
(StatusCode::OK, Json(job)).into_response()
|
|
|
|
|
}
|
2026-03-19 00:30:23 +01:00
|
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn approve_requirement(
|
|
|
|
|
auth: AuthUser,
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path(id): Path<Uuid>,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
if let Err(e) = require_admin(&auth) {
|
|
|
|
|
return e.into_response();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let existing = match RequirementRepository::get_by_id(&state.pool, id).await {
|
|
|
|
|
Ok(Some(req)) => req,
|
|
|
|
|
Ok(None) => return (StatusCode::NOT_FOUND, "Requirement not found").into_response(),
|
|
|
|
|
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if existing.status != "PENDING_APPROVAL" {
|
|
|
|
|
return (StatusCode::BAD_REQUEST, "Requirement is not pending approval").into_response();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match RequirementRepository::approve(&state.pool, id, auth.user_id).await {
|
feat(phase1): wire email notifications, shared email crate, AppState for services
- Create crates/email shared Mailer with 18+ templates (auth, approvals, jobs, leads, tracecoins)
- users/mail.rs now re-exports from shared crate (lettre dep removed)
- Wire password changed/reset emails in users auth handlers
- Wire profile approval/rejection emails in users approvals handlers (company, customer, all 9 professional types)
- Wire job approved/rejected emails in users approvals handlers
- Wire requirement approved email in users approvals handlers
- Add AppState (pool + mail) to companies service; wire submit_job and update_application_status emails
- Add AppState (pool + mail) to customers service; wire submit_requirement, approve_request, reject_request emails (incl. contact-exchange on lead acceptance)
- Add AppState (pool + storage) to job_seekers service with resume upload multipart handler
- Wire lead cancellation and accepted-leads handlers in contracts/profession_shared.rs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-02 01:42:48 +02:00
|
|
|
Ok(req) => {
|
|
|
|
|
let customer_info = sqlx::query_as::<_, (String, String)>(
|
|
|
|
|
"SELECT u.full_name, u.email FROM customers c JOIN users u ON u.id = c.user_id WHERE c.id = $1",
|
|
|
|
|
)
|
|
|
|
|
.bind(existing.customer_id)
|
|
|
|
|
.fetch_optional(&state.pool)
|
|
|
|
|
.await;
|
|
|
|
|
if let Ok(Some((name, email))) = customer_info {
|
|
|
|
|
let _ = state.mail.send_requirement_approved_email(&email, &name, &existing.title).await;
|
|
|
|
|
}
|
|
|
|
|
(StatusCode::OK, Json(req)).into_response()
|
|
|
|
|
}
|
2026-03-19 00:30:23 +01:00
|
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async fn reject_requirement(
|
|
|
|
|
auth: AuthUser,
|
|
|
|
|
State(state): State<AppState>,
|
|
|
|
|
Path(id): Path<Uuid>,
|
|
|
|
|
Json(payload): Json<RejectPayload>,
|
|
|
|
|
) -> impl IntoResponse {
|
|
|
|
|
if let Err(e) = require_admin(&auth) {
|
|
|
|
|
return e.into_response();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let existing = match RequirementRepository::get_by_id(&state.pool, id).await {
|
|
|
|
|
Ok(Some(req)) => req,
|
|
|
|
|
Ok(None) => return (StatusCode::NOT_FOUND, "Requirement not found").into_response(),
|
|
|
|
|
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if existing.status != "PENDING_APPROVAL" {
|
|
|
|
|
return (StatusCode::BAD_REQUEST, "Requirement is not pending approval").into_response();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
match RequirementRepository::reject(&state.pool, id, payload.reason).await {
|
|
|
|
|
Ok(req) => (StatusCode::OK, Json(req)).into_response(),
|
|
|
|
|
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
|
|
|
|
}
|
|
|
|
|
}
|