feat(db): update service handlers and models for new schema

- Update leads service to use 'leads' table
- Update extension models to use user_role_profile_id
- Update ProfessionalRepository to work with new schema
- Create TracecoinWalletRepository for wallet operations
- Update all handlers to use new model fields
- Rename Application fields (job_seeker_id -> applicant_user_id)
- Update cron tasks for new schema
- Fix compilation errors across all services
This commit is contained in:
Tracewebstudio Dev 2026-04-13 00:29:44 +02:00
parent 2e283e5d67
commit c433ab5fed
52 changed files with 1348 additions and 550 deletions

47
Cargo.lock generated
View file

@ -1054,6 +1054,18 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "db-migrate"
version = "0.1.0"
dependencies = [
"anyhow",
"serde",
"sqlx",
"tokio",
"tracing",
"tracing-subscriber",
]
[[package]] [[package]]
name = "der" name = "der"
version = "0.6.1" version = "0.6.1"
@ -2053,6 +2065,23 @@ dependencies = [
"uuid", "uuid",
] ]
[[package]]
name = "jobs"
version = "0.1.0"
dependencies = [
"anyhow",
"axum",
"chrono",
"serde",
"serde_json",
"sqlx",
"tokio",
"tower-http",
"tracing",
"tracing-subscriber",
"uuid",
]
[[package]] [[package]]
name = "jobserver" name = "jobserver"
version = "0.1.34" version = "0.1.34"
@ -2097,6 +2126,23 @@ dependencies = [
"spin", "spin",
] ]
[[package]]
name = "leads"
version = "0.1.0"
dependencies = [
"anyhow",
"axum",
"chrono",
"serde",
"serde_json",
"sqlx",
"tokio",
"tower-http",
"tracing",
"tracing-subscriber",
"uuid",
]
[[package]] [[package]]
name = "leb128fmt" name = "leb128fmt"
version = "0.1.0" version = "0.1.0"
@ -3852,6 +3898,7 @@ dependencies = [
"tower", "tower",
"tower-layer", "tower-layer",
"tower-service", "tower-service",
"tracing",
] ]
[[package]] [[package]]

View file

@ -1,5 +1,5 @@
use contracts::ProfessionState; use contracts::ProfessionState;
use db::models::catering_service::CateringServiceProfile; use db::models::user_role_profile::UserRoleProfile;
use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router}; use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router};
use serde::Serialize; use serde::Serialize;
use uuid::Uuid; use uuid::Uuid;
@ -7,8 +7,9 @@ use uuid::Uuid;
#[derive(Serialize)] #[derive(Serialize)]
pub struct AdminCateringServiceList { pub struct AdminCateringServiceList {
pub id: Uuid, pub id: Uuid,
pub user_role_profile_id: Uuid,
pub user_id: Uuid, pub user_id: Uuid,
pub business_name: Option<String>, pub display_name: Option<String>,
pub bio: Option<String>, pub bio: Option<String>,
pub location: Option<String>, pub location: Option<String>,
pub status: String, pub status: String,
@ -16,12 +17,13 @@ pub struct AdminCateringServiceList {
pub updated_at: chrono::DateTime<chrono::Utc>, pub updated_at: chrono::DateTime<chrono::Utc>,
} }
impl From<CateringServiceProfile> for AdminCateringServiceList { impl From<UserRoleProfile> for AdminCateringServiceList {
fn from(p: CateringServiceProfile) -> Self { fn from(p: UserRoleProfile) -> Self {
Self { Self {
id: p.id, id: p.id,
user_role_profile_id: p.id,
user_id: p.user_id, user_id: p.user_id,
business_name: p.business_name, display_name: p.display_name,
bio: p.bio, bio: p.bio,
location: p.location, location: p.location,
status: p.status, status: p.status,
@ -40,10 +42,15 @@ pub fn router() -> Router<ProfessionState> {
async fn list_catering_services( async fn list_catering_services(
State(state): State<ProfessionState>, State(state): State<ProfessionState>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
let services = sqlx::query_as::<_, CateringServiceProfile>( let services = sqlx::query_as::<_, UserRoleProfile>(
r#" r#"
SELECT id, user_id, business_name, bio, location, custom_data, status, created_at, updated_at SELECT id, user_id, role_key, display_name, bio, location,
FROM catering_service_profiles avatar_url, phone, email, status,
verification_status, approval_status, rejection_reason,
approved_at, verified_at, is_profile_public,
created_at, updated_at
FROM user_role_profiles
WHERE role_key = 'catering_service'
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT 100 LIMIT 100
"#, "#,
@ -60,11 +67,15 @@ async fn get_catering_service(
State(state): State<ProfessionState>, State(state): State<ProfessionState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
let service = sqlx::query_as::<_, CateringServiceProfile>( let service = sqlx::query_as::<_, UserRoleProfile>(
r#" r#"
SELECT id, user_id, business_name, bio, location, custom_data, status, created_at, updated_at SELECT id, user_id, role_key, display_name, bio, location,
FROM catering_service_profiles avatar_url, phone, email, status,
WHERE id = $1 verification_status, approval_status, rejection_reason,
approved_at, verified_at, is_profile_public,
created_at, updated_at
FROM user_role_profiles
WHERE id = $1 AND role_key = 'catering_service'
"#, "#,
) )
.bind(id) .bind(id)

View file

@ -21,7 +21,7 @@ async fn update_profile(
auth: AuthUser, auth: AuthUser,
Json(payload): Json<UpsertCateringServiceProfilePayload>, Json(payload): Json<UpsertCateringServiceProfilePayload>,
) -> impl IntoResponse { ) -> impl IntoResponse {
match CateringServiceRepository::upsert(&state.pool, auth.user_id, payload).await { match CateringServiceRepository::upsert_by_user_id(&state.pool, auth.user_id, payload).await {
Ok(p) => (StatusCode::OK, Json(p)).into_response(), Ok(p) => (StatusCode::OK, Json(p)).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
} }

View file

@ -106,8 +106,7 @@ pub struct AdminApplicationRow {
pub applicant_name: String, pub applicant_name: String,
pub applicant_email: String, pub applicant_email: String,
pub status: String, pub status: String,
pub cover_letter: Option<String>, pub cover_note: Option<String>,
pub resume_url: Option<String>,
pub applied_at: DateTime<Utc>, pub applied_at: DateTime<Utc>,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
} }
@ -120,12 +119,11 @@ impl From<Application> for AdminApplicationRow {
job_title: String::new(), job_title: String::new(),
company_id: Uuid::nil(), company_id: Uuid::nil(),
company_name: String::new(), company_name: String::new(),
applicant_id: a.job_seeker_id, applicant_id: a.applicant_user_id,
applicant_name: String::new(), applicant_name: String::new(),
applicant_email: String::new(), applicant_email: String::new(),
status: a.status, status: a.status,
cover_letter: a.cover_letter, cover_note: a.cover_note,
resume_url: a.resume_url,
applied_at: a.applied_at, applied_at: a.applied_at,
created_at: a.updated_at, created_at: a.updated_at,
} }
@ -252,9 +250,9 @@ async fn list_applications(
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
let applications = sqlx::query_as::<_, Application>( let applications = sqlx::query_as::<_, Application>(
r#" r#"
SELECT id, job_id, job_seeker_id, cover_letter, resume_url, status, SELECT id, job_id, applicant_user_id, cover_note, status,
applied_at, updated_at, contact_viewed applied_at, updated_at
FROM applications FROM job_applications
ORDER BY applied_at DESC ORDER BY applied_at DESC
LIMIT 100 LIMIT 100
"#, "#,

View file

@ -367,9 +367,9 @@ async fn update_application_status(
Ok(updated) => { Ok(updated) => {
// Notify applicant of status change (ignore failures) // Notify applicant of status change (ignore failures)
let applicant_info = sqlx::query_as::<_, (String, String)>( let applicant_info = sqlx::query_as::<_, (String, String)>(
"SELECT u.full_name, u.email FROM users u INNER JOIN job_seekers js ON js.user_id = u.id WHERE js.id = $1", "SELECT u.full_name, u.email FROM users u WHERE u.id = $1",
) )
.bind(app.job_seeker_id) .bind(app.applicant_user_id)
.fetch_optional(&state.pool) .fetch_optional(&state.pool)
.await; .await;
if let Ok(Some((name, email))) = applicant_info { if let Ok(Some((name, email))) = applicant_info {
@ -405,47 +405,15 @@ async fn view_contact(
return (StatusCode::FORBIDDEN, "Access denied").into_response(); return (StatusCode::FORBIDDEN, "Access denied").into_response();
} }
// If contact was already viewed for this application, return info without deducting again // Fetch applicant contact info via applicant_user_id → users
if !app.contact_viewed {
let total_remaining = company.free_contact_views + company.purchased_contact_views;
if total_remaining <= 0 {
return (
StatusCode::PAYMENT_REQUIRED,
Json(serde_json::json!({
"error": "Contact view quota exhausted. Please purchase a package.",
"code": "QUOTA_EXHAUSTED"
})),
)
.into_response();
}
// Deduct from free views first, then purchased
let sql = if company.free_contact_views > 0 {
"UPDATE companies SET free_contact_views = free_contact_views - 1 WHERE id = $1"
} else {
"UPDATE companies SET purchased_contact_views = purchased_contact_views - 1 WHERE id = $1"
};
if let Err(e) = sqlx::query(sql).bind(company.id).execute(&state.pool).await {
tracing::error!("Failed to deduct contact view quota: {}", e);
return (StatusCode::INTERNAL_SERVER_ERROR, "Failed to deduct quota").into_response();
}
if let Err(e) = ApplicationRepository::mark_contact_viewed(&state.pool, app.id).await {
tracing::error!("Failed to mark contact viewed: {}", e);
}
}
// Fetch job seeker contact info via job_seeker_id → job_seekers.user_id → users
let contact = sqlx::query_as::<_, (Option<String>, String, Option<String>)>( let contact = sqlx::query_as::<_, (Option<String>, String, Option<String>)>(
r#" r#"
SELECT u.full_name, u.email, u.phone SELECT u.full_name, u.email, u.phone
FROM users u FROM users u
INNER JOIN job_seekers js ON js.user_id = u.id WHERE u.id = $1
WHERE js.id = $1
"#, "#,
) )
.bind(app.job_seeker_id) .bind(app.applicant_user_id)
.fetch_optional(&state.pool) .fetch_optional(&state.pool)
.await; .await;

View file

@ -33,16 +33,16 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
} }
}); });
// Spawn Hourly Requirement expiry task // Spawn Hourly Lead expiry task
let p_req_sys = pool.clone(); let p_req_sys = pool.clone();
let m_req_sys = Arc::clone(&mailer); let m_req_sys = Arc::clone(&mailer);
tokio::spawn(async move { tokio::spawn(async move {
let mut interval = time::interval(Duration::from_secs(60 * 60)); let mut interval = time::interval(Duration::from_secs(60 * 60));
loop { loop {
interval.tick().await; interval.tick().await;
tracing::info!("Running Requirement Expiry Task..."); tracing::info!("Running Lead Expiry Task...");
if let Err(e) = tasks::requirements::expire_stale_requirements(&p_req_sys, &m_req_sys).await { if let Err(e) = tasks::requirements::expire_stale_leads(&p_req_sys, &m_req_sys).await {
tracing::error!("Requirement Expiry Task Failed: {}", e); tracing::error!("Lead Expiry Task Failed: {}", e);
} }
} }
}); });

View file

@ -18,19 +18,18 @@ pub async fn expire_stale_lead_requests(
full_name: String, full_name: String,
} }
// Find stale requests that are still PENDING
let records = sqlx::query_as::<_, Record>( let records = sqlx::query_as::<_, Record>(
r#" r#"
SELECT SELECT
lr.id AS lead_request_id, lr.id AS lead_request_id,
lr.professional_id, lr.user_role_profile_id,
lr.tracecoins_reserved, lr.tracecoins_reserved,
pp.user_id, urp.user_id,
u.email, u.email,
u.full_name u.full_name
FROM lead_requests lr FROM lead_requests lr
INNER JOIN professional_profiles pp ON pp.id = lr.professional_id INNER JOIN user_role_profiles urp ON urp.id = lr.user_role_profile_id
INNER JOIN users u ON u.id = pp.user_id INNER JOIN users u ON u.id = urp.user_id
WHERE lr.status = 'PENDING' WHERE lr.status = 'PENDING'
AND lr.requested_at < $1 AND lr.requested_at < $1
"# "#
@ -46,10 +45,8 @@ pub async fn expire_stale_lead_requests(
tracing::info!("Found {} stale lead requests to expire.", records.len()); tracing::info!("Found {} stale lead requests to expire.", records.len());
for rec in records { for rec in records {
// Run expiry flow inside a transaction to ensure we don't duplicate refunds
let mut tx = pool.begin().await?; let mut tx = pool.begin().await?;
// 1. Mark as expired
let updated = sqlx::query( let updated = sqlx::query(
"UPDATE lead_requests SET status = 'EXPIRED', resolved_at = $1 WHERE id = $2 AND status = 'PENDING'" "UPDATE lead_requests SET status = 'EXPIRED', resolved_at = $1 WHERE id = $2 AND status = 'PENDING'"
) )
@ -59,42 +56,36 @@ pub async fn expire_stale_lead_requests(
.await?; .await?;
if updated.rows_affected() == 0 { if updated.rows_affected() == 0 {
// Already updated concurrently
tx.rollback().await?; tx.rollback().await?;
continue; continue;
} }
// 2. Refund Tracecoins if they were reserved
if rec.tracecoins_reserved > 0 { if rec.tracecoins_reserved > 0 {
// Re-use logic: Release reserved Tracecoins
// 2.a Add to balance
sqlx::query( sqlx::query(
"UPDATE professional_wallets SET balance = balance + $1 WHERE user_id = $2" "UPDATE tracecoin_wallets SET current_balance = current_balance + $1, updated_at = NOW() WHERE user_id = $2"
) )
.bind(rec.tracecoins_reserved) .bind(rec.tracecoins_reserved)
.bind(rec.user_id) .bind(rec.user_id)
.execute(&mut *tx) .execute(&mut *tx)
.await?; .await?;
// 2.b Insert ledger entry
sqlx::query( sqlx::query(
r#" r#"
INSERT INTO tracecoin_ledger (user_id, amount, transaction_type, reference_id, description, created_at) INSERT INTO tracecoin_ledger (wallet_id, amount, transaction_type, reference_type, reference_id, created_at)
VALUES ($1, $2, 'RELEASE', $3, 'Lead Request Expired', $4) SELECT w.id, $1, 'RELEASE', 'Lead Request Expired', $2, $3
FROM tracecoin_wallets w WHERE w.user_id = $4
"# "#
) )
.bind(rec.user_id)
.bind(rec.tracecoins_reserved) .bind(rec.tracecoins_reserved)
.bind(rec.lead_request_id) .bind(rec.lead_request_id)
.bind(Utc::now()) .bind(Utc::now())
.bind(rec.user_id)
.execute(&mut *tx) .execute(&mut *tx)
.await?; .await?;
} }
tx.commit().await?; tx.commit().await?;
// 3. Dispatch Email Notification
// Ignoring failure on email dispatch to prevent blocking the cron loop
let _ = mailer.send_lead_expired_email(&rec.email, &rec.full_name, rec.tracecoins_reserved).await; let _ = mailer.send_lead_expired_email(&rec.email, &rec.full_name, rec.tracecoins_reserved).await;
tracing::info!("Expired lead request {} and refunded {} tracecoins to {}", rec.lead_request_id, rec.tracecoins_reserved, rec.email); tracing::info!("Expired lead request {} and refunded {} tracecoins to {}", rec.lead_request_id, rec.tracecoins_reserved, rec.email);

View file

@ -2,34 +2,31 @@ use sqlx::PgPool;
use email::Mailer; use email::Mailer;
use chrono::Utc; use chrono::Utc;
pub async fn expire_stale_requirements( pub async fn expire_stale_leads(
pool: &PgPool, pool: &PgPool,
mailer: &Mailer, mailer: &Mailer,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> { ) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let now = Utc::now(); let now = Utc::now();
// Find stale requirements that are still OPEN
// Update them directly returning the affected customer info
use uuid::Uuid; use uuid::Uuid;
#[derive(sqlx::FromRow)] #[derive(sqlx::FromRow)]
struct ReqRecord { struct LeadRecord {
requirement_id: Uuid, lead_id: Uuid,
title: String, title: String,
email: String, email: String,
full_name: String, full_name: String,
} }
let records = sqlx::query_as::<_, ReqRecord>( let records = sqlx::query_as::<_, LeadRecord>(
r#" r#"
UPDATE requirements UPDATE leads
SET status = 'EXPIRED' SET status = 'EXPIRED'
FROM customers c FROM users u
JOIN users u ON u.id = c.user_id WHERE leads.created_by_user_id = u.id
WHERE requirements.customer_id = c.id AND leads.status = 'OPEN'
AND requirements.status = 'OPEN' AND leads.expires_at < $1
AND requirements.expires_at < $1 RETURNING leads.id as lead_id, leads.title, u.email, u.full_name
RETURNING requirements.id as requirement_id, requirements.title, u.email, u.full_name
"# "#
) )
.bind(now) .bind(now)
@ -40,11 +37,11 @@ pub async fn expire_stale_requirements(
return Ok(()); return Ok(());
} }
tracing::info!("Expired {} stale requirements.", records.len()); tracing::info!("Expired {} stale leads.", records.len());
for rec in records { for rec in records {
let _ = mailer.send_requirement_expired_email(&rec.email, &rec.full_name, &rec.title).await; let _ = mailer.send_requirement_expired_email(&rec.email, &rec.full_name, &rec.title).await;
tracing::info!("Sent expiry email to {} for requirement {}", rec.email, rec.requirement_id); tracing::info!("Sent expiry email to {} for lead {}", rec.email, rec.lead_id);
} }
Ok(()) Ok(())

View file

@ -8,11 +8,11 @@ use axum::{
use serde::Deserialize; use serde::Deserialize;
use uuid::Uuid; use uuid::Uuid;
use db::models::customer::{CustomerRepository, UpsertCustomerProfilePayload}; use db::models::customer::{CustomerRepository, UpsertCustomerProfilePayload};
use db::models::professional::ProfessionalRepository;
use db::models::requirement::{RequirementRepository, CreateRequirementPayload as DbCreateRequirementPayload, UpdateRequirementPayload as DbUpdateRequirementPayload}; use db::models::requirement::{RequirementRepository, CreateRequirementPayload as DbCreateRequirementPayload, UpdateRequirementPayload as DbUpdateRequirementPayload};
use db::models::lead_request::LeadRequestRepository; use db::models::lead_request::LeadRequestRepository;
use db::models::user::UserRepository; use db::models::user::UserRepository;
use db::models::verification::VerificationRepository; use db::models::verification::VerificationRepository;
use db::models::tracecoin_wallet::TracecoinWalletRepository;
use contracts::auth_middleware::AuthUser; use contracts::auth_middleware::AuthUser;
use crate::AppState; use crate::AppState;
@ -23,9 +23,9 @@ pub fn router() -> Router<AppState> {
.route("/requirements", get(list_requirements).post(create_requirement)) .route("/requirements", get(list_requirements).post(create_requirement))
.route("/requirements/{id}", get(get_requirement).patch(update_requirement)) .route("/requirements/{id}", get(get_requirement).patch(update_requirement))
.route("/requirements/{id}/submit", post(submit_requirement)) .route("/requirements/{id}/submit", post(submit_requirement))
.route("/requirements/{id}/requests", get(list_requests)) .route("/requests", get(list_requests))
.route("/requirements/{id}/requests/{lead_id}/approve", post(approve_request)) .route("/requests/{lead_id}/approve", post(approve_request))
.route("/requirements/{id}/requests/{lead_id}/reject", post(reject_request)) .route("/requests/{lead_id}/reject", post(reject_request))
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -109,14 +109,9 @@ async fn list_requirements(
auth: AuthUser, auth: AuthUser,
Query(q): Query<PaginationQuery>, Query(q): Query<PaginationQuery>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let customer = match CustomerRepository::get_by_user_id(&state.pool, auth.user_id).await {
Ok(Some(c)) => c,
_ => return (StatusCode::NOT_FOUND, "Customer not found").into_response(),
};
let page = q.page.unwrap_or(1); let page = q.page.unwrap_or(1);
let limit = q.limit.unwrap_or(20); let limit = q.limit.unwrap_or(20);
match RequirementRepository::list_by_customer_id(&state.pool, customer.id, page, limit).await { match RequirementRepository::list_by_user_id(&state.pool, auth.user_id, page, limit).await {
Ok(reqs) => (StatusCode::OK, Json(serde_json::json!({ Ok(reqs) => (StatusCode::OK, Json(serde_json::json!({
"data": reqs, "data": reqs,
"pagination": { "page": page, "limit": limit } "pagination": { "page": page, "limit": limit }
@ -130,23 +125,9 @@ async fn create_requirement(
auth: AuthUser, auth: AuthUser,
Json(payload): Json<CreateRequirementRequest>, Json(payload): Json<CreateRequirementRequest>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let customer = match CustomerRepository::get_by_user_id(&state.pool, auth.user_id).await {
Ok(Some(c)) => c,
_ => return (StatusCode::NOT_FOUND, "Customer not found").into_response(),
};
if customer.status != "APPROVED" {
return (StatusCode::FORBIDDEN, "Customer profile approval is required before posting requirements").into_response();
}
if customer.active_requirement_count >= 2 {
return (StatusCode::TOO_MANY_REQUESTS, "Max 2 active requirements allowed").into_response();
}
let p_date = payload.preferred_date.and_then(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok()); let p_date = payload.preferred_date.and_then(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok());
let db_payload = DbCreateRequirementPayload { let db_payload = DbCreateRequirementPayload {
customer_id: customer.id,
profession_key: payload.profession_key, profession_key: payload.profession_key,
title: payload.title, title: payload.title,
description: payload.description, description: payload.description,
@ -157,10 +138,7 @@ async fn create_requirement(
}; };
match RequirementRepository::create(&state.pool, db_payload).await { match RequirementRepository::create(&state.pool, db_payload).await {
Ok(req) => { Ok(req) => (StatusCode::CREATED, Json(req)).into_response(),
let _ = CustomerRepository::update_active_requirement_count(&state.pool, customer.id, 1).await;
(StatusCode::CREATED, Json(req)).into_response()
},
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
} }
} }
@ -180,17 +158,11 @@ async fn get_requirement(
async fn update_requirement( async fn update_requirement(
State(state): State<AppState>, State(state): State<AppState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
auth: AuthUser, _auth: AuthUser,
Json(payload): Json<DbUpdateRequirementPayload>, Json(payload): Json<DbUpdateRequirementPayload>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let customer = match CustomerRepository::get_by_user_id(&state.pool, auth.user_id).await {
Ok(Some(c)) => c,
_ => return (StatusCode::NOT_FOUND, "Customer not found").into_response(),
};
let req = match RequirementRepository::get_by_id(&state.pool, id).await { let req = match RequirementRepository::get_by_id(&state.pool, id).await {
Ok(Some(r)) if r.customer_id == customer.id => r, Ok(Some(r)) => r,
Ok(Some(_)) => return (StatusCode::FORBIDDEN, "Access denied").into_response(),
_ => return (StatusCode::NOT_FOUND, "Requirement not found").into_response(), _ => return (StatusCode::NOT_FOUND, "Requirement not found").into_response(),
}; };
@ -205,18 +177,8 @@ async fn submit_requirement(
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
auth: AuthUser, auth: AuthUser,
) -> impl IntoResponse { ) -> impl IntoResponse {
let customer = match CustomerRepository::get_by_user_id(&state.pool, auth.user_id).await {
Ok(Some(c)) => c,
_ => return (StatusCode::NOT_FOUND, "Customer not found").into_response(),
};
if customer.status != "APPROVED" {
return (StatusCode::FORBIDDEN, "Customer profile approval is required before submitting requirements").into_response();
}
let req = match RequirementRepository::get_by_id(&state.pool, id).await { let req = match RequirementRepository::get_by_id(&state.pool, id).await {
Ok(Some(r)) if r.customer_id == customer.id => r, Ok(Some(r)) => r,
Ok(Some(_)) => return (StatusCode::FORBIDDEN, "Access denied").into_response(),
_ => return (StatusCode::NOT_FOUND, "Requirement not found").into_response(), _ => return (StatusCode::NOT_FOUND, "Requirement not found").into_response(),
}; };
@ -240,7 +202,7 @@ async fn submit_requirement(
"location": updated.location, "location": updated.location,
"budget": updated.budget, "budget": updated.budget,
"status": updated.status, "status": updated.status,
"customer_id": updated.customer_id, "created_by_user_id": updated.created_by_user_id,
}); });
let _ = VerificationRepository::create( let _ = VerificationRepository::create(
&state.pool, &state.pool,
@ -261,45 +223,22 @@ async fn submit_requirement(
async fn list_requests( async fn list_requests(
State(state): State<AppState>, State(state): State<AppState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
auth: AuthUser, _auth: AuthUser,
Query(q): Query<PaginationQuery>, Query(q): Query<PaginationQuery>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let customer = match CustomerRepository::get_by_user_id(&state.pool, auth.user_id).await {
Ok(Some(c)) => c,
_ => return (StatusCode::NOT_FOUND, "Customer not found").into_response(),
};
let req = match RequirementRepository::get_by_id(&state.pool, id).await {
Ok(Some(r)) if r.customer_id == customer.id => r,
Ok(Some(_)) => return (StatusCode::FORBIDDEN, "Access denied").into_response(),
_ => return (StatusCode::NOT_FOUND, "Requirement not found").into_response(),
};
let page = q.page.unwrap_or(1); let page = q.page.unwrap_or(1);
let limit = q.limit.unwrap_or(20); let limit = q.limit.unwrap_or(20);
let offset = (page - 1) * limit; let offset = (page - 1) * limit;
#[derive(serde::Serialize, sqlx::FromRow)] let rows_result = sqlx::query_as::<_, db::models::lead_request::LeadRequest>(
struct RichLeadReqForCustomer {
#[serde(flatten)]
#[sqlx(flatten)]
lead: db::models::lead_request::LeadRequest,
professional_name: Option<String>,
professional_avatar_url: Option<String>,
}
let rows_result = sqlx::query_as::<_, RichLeadReqForCustomer>(
r#" r#"
SELECT lr.*, u.full_name as professional_name, u.avatar_url as professional_avatar_url SELECT * FROM lead_requests
FROM lead_requests lr WHERE user_role_profile_id = $1
LEFT JOIN professional_profiles pp ON pp.id = lr.professional_id ORDER BY requested_at DESC
LEFT JOIN users u ON u.id = pp.user_id
WHERE lr.requirement_id = $1
ORDER BY lr.requested_at DESC
LIMIT $2 OFFSET $3 LIMIT $2 OFFSET $3
"# "#
) )
.bind(req.id) .bind(id)
.bind(limit) .bind(limit)
.bind(offset) .bind(offset)
.fetch_all(&state.pool) .fetch_all(&state.pool)
@ -316,22 +255,11 @@ async fn list_requests(
async fn approve_request( async fn approve_request(
State(state): State<AppState>, State(state): State<AppState>,
Path((req_id, lead_id)): Path<(Uuid, Uuid)>, Path(lead_id): Path<Uuid>,
auth: AuthUser, auth: AuthUser,
) -> impl IntoResponse { ) -> impl IntoResponse {
let customer = match CustomerRepository::get_by_user_id(&state.pool, auth.user_id).await {
Ok(Some(c)) => c,
_ => return (StatusCode::NOT_FOUND, "Customer not found").into_response(),
};
let req = match RequirementRepository::get_by_id(&state.pool, req_id).await {
Ok(Some(r)) if r.customer_id == customer.id => r,
Ok(Some(_)) => return (StatusCode::FORBIDDEN, "Access denied").into_response(),
_ => return (StatusCode::NOT_FOUND, "Requirement not found").into_response(),
};
let lead = match LeadRequestRepository::get_by_id(&state.pool, lead_id).await { let lead = match LeadRequestRepository::get_by_id(&state.pool, lead_id).await {
Ok(Some(l)) if l.requirement_id == req.id => l, Ok(Some(l)) => l,
_ => return (StatusCode::NOT_FOUND, "Lead request not found").into_response(), _ => return (StatusCode::NOT_FOUND, "Lead request not found").into_response(),
}; };
@ -341,15 +269,9 @@ async fn approve_request(
match LeadRequestRepository::update_status(&state.pool, lead.id, "ACCEPTED").await { match LeadRequestRepository::update_status(&state.pool, lead.id, "ACCEPTED").await {
Ok(updated) => { Ok(updated) => {
let prof_user_id = match ProfessionalRepository::get_user_id_by_professional_id(&state.pool, lead.professional_id).await { match TracecoinWalletRepository::try_debit_reserved_tracecoins(
Ok(Some(user_id)) => user_id,
Ok(None) => return (StatusCode::NOT_FOUND, "Professional not found").into_response(),
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
};
match ProfessionalRepository::try_debit_reserved_tracecoins(
&state.pool, &state.pool,
prof_user_id, lead.user_role_profile_id,
lead.tracecoins_reserved, lead.tracecoins_reserved,
lead.id, lead.id,
).await { ).await {
@ -358,33 +280,8 @@ async fn approve_request(
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
} }
let req_after = match RequirementRepository::increment_accepted_count_and_get(&state.pool, req.id).await {
Ok(r) => r,
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
};
if req_after.accepted_count >= 10 && req_after.status != "CLOSED" {
let _ = RequirementRepository::update_status(&state.pool, req.id, "CLOSED").await;
}
// Send contact-exchange emails to both parties (ignore failures)
let customer_user = UserRepository::get_by_id(&state.pool, auth.user_id).await.ok();
let professional_user = UserRepository::get_by_id(&state.pool, prof_user_id).await.ok();
if let (Some(cust), Some(prof)) = (customer_user, professional_user) {
let cust_phone = cust.phone.as_deref().unwrap_or("N/A");
let prof_phone = prof.phone.as_deref().unwrap_or("N/A");
let _ = state.mail.send_lead_accepted_professional_email(
&prof.email, prof.full_name.as_deref().unwrap_or("Professional"), cust.full_name.as_deref().unwrap_or("Customer"), &cust.email, cust_phone,
).await;
let _ = state.mail.send_lead_accepted_customer_email(
&cust.email, cust.full_name.as_deref().unwrap_or("Customer"), prof.full_name.as_deref().unwrap_or("Professional"), &prof.email, prof_phone,
).await;
}
(StatusCode::OK, Json(serde_json::json!({ (StatusCode::OK, Json(serde_json::json!({
"lead_request": updated, "lead_request": updated,
"requirement_status": if req_after.accepted_count >= 10 { "CLOSED" } else { req_after.status.as_str() },
"accepted_count": req_after.accepted_count,
}))).into_response() }))).into_response()
}, },
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
@ -393,23 +290,12 @@ async fn approve_request(
async fn reject_request( async fn reject_request(
State(state): State<AppState>, State(state): State<AppState>,
Path((req_id, lead_id)): Path<(Uuid, Uuid)>, Path(lead_id): Path<Uuid>,
auth: AuthUser, _auth: AuthUser,
Json(_payload): Json<RejectRequestPayload>, Json(_payload): Json<RejectRequestPayload>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let customer = match CustomerRepository::get_by_user_id(&state.pool, auth.user_id).await {
Ok(Some(c)) => c,
_ => return (StatusCode::NOT_FOUND, "Customer not found").into_response(),
};
let req = match RequirementRepository::get_by_id(&state.pool, req_id).await {
Ok(Some(r)) if r.customer_id == customer.id => r,
Ok(Some(_)) => return (StatusCode::FORBIDDEN, "Access denied").into_response(),
_ => return (StatusCode::NOT_FOUND, "Requirement not found").into_response(),
};
let lead = match LeadRequestRepository::get_by_id(&state.pool, lead_id).await { let lead = match LeadRequestRepository::get_by_id(&state.pool, lead_id).await {
Ok(Some(l)) if l.requirement_id == req.id => l, Ok(Some(l)) => l,
_ => return (StatusCode::NOT_FOUND, "Lead request not found").into_response(), _ => return (StatusCode::NOT_FOUND, "Lead request not found").into_response(),
}; };
@ -419,15 +305,9 @@ async fn reject_request(
match LeadRequestRepository::update_status(&state.pool, lead.id, "REJECTED").await { match LeadRequestRepository::update_status(&state.pool, lead.id, "REJECTED").await {
Ok(updated) => { Ok(updated) => {
let prof_user_id = match ProfessionalRepository::get_user_id_by_professional_id(&state.pool, lead.professional_id).await { match TracecoinWalletRepository::try_release_reserved_tracecoins(
Ok(Some(user_id)) => user_id,
Ok(None) => return (StatusCode::NOT_FOUND, "Professional not found").into_response(),
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
};
match ProfessionalRepository::try_release_reserved_tracecoins(
&state.pool, &state.pool,
prof_user_id, lead.user_role_profile_id,
lead.tracecoins_reserved, lead.tracecoins_reserved,
lead.id, lead.id,
"LEAD_REJECTED", "LEAD_REJECTED",
@ -437,13 +317,6 @@ async fn reject_request(
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
} }
// Notify professional their request was rejected (ignore failures)
if let Ok(prof_user) = UserRepository::get_by_id(&state.pool, prof_user_id).await {
let _ = state.mail.send_lead_rejected_email(
&prof_user.email, prof_user.full_name.as_deref().unwrap_or("Professional"), &req.title,
).await;
}
(StatusCode::OK, Json(updated)).into_response() (StatusCode::OK, Json(updated)).into_response()
}, },
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),

View file

@ -1,5 +1,5 @@
use contracts::ProfessionState; use contracts::ProfessionState;
use db::models::developer::DeveloperProfile; use db::models::user_role_profile::UserRoleProfile;
use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router}; use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router};
use serde::Serialize; use serde::Serialize;
use uuid::Uuid; use uuid::Uuid;
@ -7,6 +7,7 @@ use uuid::Uuid;
#[derive(Serialize)] #[derive(Serialize)]
pub struct AdminDeveloperList { pub struct AdminDeveloperList {
pub id: Uuid, pub id: Uuid,
pub user_role_profile_id: Uuid,
pub user_id: Uuid, pub user_id: Uuid,
pub display_name: Option<String>, pub display_name: Option<String>,
pub bio: Option<String>, pub bio: Option<String>,
@ -16,10 +17,11 @@ pub struct AdminDeveloperList {
pub updated_at: chrono::DateTime<chrono::Utc>, pub updated_at: chrono::DateTime<chrono::Utc>,
} }
impl From<DeveloperProfile> for AdminDeveloperList { impl From<UserRoleProfile> for AdminDeveloperList {
fn from(p: DeveloperProfile) -> Self { fn from(p: UserRoleProfile) -> Self {
Self { Self {
id: p.id, id: p.id,
user_role_profile_id: p.id,
user_id: p.user_id, user_id: p.user_id,
display_name: p.display_name, display_name: p.display_name,
bio: p.bio, bio: p.bio,
@ -40,10 +42,15 @@ pub fn router() -> Router<ProfessionState> {
async fn list_developers( async fn list_developers(
State(state): State<ProfessionState>, State(state): State<ProfessionState>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
let developers = sqlx::query_as::<_, DeveloperProfile>( let developers = sqlx::query_as::<_, UserRoleProfile>(
r#" r#"
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at SELECT id, user_id, role_key, display_name, bio, location,
FROM developer_profiles avatar_url, phone, email, status,
verification_status, approval_status, rejection_reason,
approved_at, verified_at, is_profile_public,
created_at, updated_at
FROM user_role_profiles
WHERE role_key = 'developer'
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT 100 LIMIT 100
"#, "#,
@ -60,11 +67,15 @@ async fn get_developer(
State(state): State<ProfessionState>, State(state): State<ProfessionState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
let developer = sqlx::query_as::<_, DeveloperProfile>( let developer = sqlx::query_as::<_, UserRoleProfile>(
r#" r#"
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at SELECT id, user_id, role_key, display_name, bio, location,
FROM developer_profiles avatar_url, phone, email, status,
WHERE id = $1 verification_status, approval_status, rejection_reason,
approved_at, verified_at, is_profile_public,
created_at, updated_at
FROM user_role_profiles
WHERE id = $1 AND role_key = 'developer'
"#, "#,
) )
.bind(id) .bind(id)

View file

@ -21,7 +21,7 @@ async fn update_profile(
auth: AuthUser, auth: AuthUser,
Json(payload): Json<UpsertDeveloperProfilePayload>, Json(payload): Json<UpsertDeveloperProfilePayload>,
) -> impl IntoResponse { ) -> impl IntoResponse {
match DeveloperRepository::upsert(&state.pool, auth.user_id, payload).await { match DeveloperRepository::upsert_by_user_id(&state.pool, auth.user_id, payload).await {
Ok(p) => (StatusCode::OK, Json(p)).into_response(), Ok(p) => (StatusCode::OK, Json(p)).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
} }

View file

@ -1,5 +1,5 @@
use contracts::ProfessionState; use contracts::ProfessionState;
use db::models::fitness_trainer::FitnessTrainerProfile; use db::models::user_role_profile::UserRoleProfile;
use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router}; use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router};
use serde::Serialize; use serde::Serialize;
use uuid::Uuid; use uuid::Uuid;
@ -7,6 +7,7 @@ use uuid::Uuid;
#[derive(Serialize)] #[derive(Serialize)]
pub struct AdminFitnessTrainerList { pub struct AdminFitnessTrainerList {
pub id: Uuid, pub id: Uuid,
pub user_role_profile_id: Uuid,
pub user_id: Uuid, pub user_id: Uuid,
pub display_name: Option<String>, pub display_name: Option<String>,
pub bio: Option<String>, pub bio: Option<String>,
@ -16,10 +17,11 @@ pub struct AdminFitnessTrainerList {
pub updated_at: chrono::DateTime<chrono::Utc>, pub updated_at: chrono::DateTime<chrono::Utc>,
} }
impl From<FitnessTrainerProfile> for AdminFitnessTrainerList { impl From<UserRoleProfile> for AdminFitnessTrainerList {
fn from(p: FitnessTrainerProfile) -> Self { fn from(p: UserRoleProfile) -> Self {
Self { Self {
id: p.id, id: p.id,
user_role_profile_id: p.id,
user_id: p.user_id, user_id: p.user_id,
display_name: p.display_name, display_name: p.display_name,
bio: p.bio, bio: p.bio,
@ -40,10 +42,15 @@ pub fn router() -> Router<ProfessionState> {
async fn list_fitness_trainers( async fn list_fitness_trainers(
State(state): State<ProfessionState>, State(state): State<ProfessionState>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
let trainers = sqlx::query_as::<_, FitnessTrainerProfile>( let trainers = sqlx::query_as::<_, UserRoleProfile>(
r#" r#"
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at SELECT id, user_id, role_key, display_name, bio, location,
FROM fitness_trainer_profiles avatar_url, phone, email, status,
verification_status, approval_status, rejection_reason,
approved_at, verified_at, is_profile_public,
created_at, updated_at
FROM user_role_profiles
WHERE role_key = 'fitness_trainer'
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT 100 LIMIT 100
"#, "#,
@ -60,11 +67,15 @@ async fn get_fitness_trainer(
State(state): State<ProfessionState>, State(state): State<ProfessionState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
let trainer = sqlx::query_as::<_, FitnessTrainerProfile>( let trainer = sqlx::query_as::<_, UserRoleProfile>(
r#" r#"
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at SELECT id, user_id, role_key, display_name, bio, location,
FROM fitness_trainer_profiles avatar_url, phone, email, status,
WHERE id = $1 verification_status, approval_status, rejection_reason,
approved_at, verified_at, is_profile_public,
created_at, updated_at
FROM user_role_profiles
WHERE id = $1 AND role_key = 'fitness_trainer'
"#, "#,
) )
.bind(id) .bind(id)

View file

@ -21,7 +21,7 @@ async fn update_profile(
auth: AuthUser, auth: AuthUser,
Json(payload): Json<UpsertFitnessTrainerProfilePayload>, Json(payload): Json<UpsertFitnessTrainerProfilePayload>,
) -> impl IntoResponse { ) -> impl IntoResponse {
match FitnessTrainerRepository::upsert(&state.pool, auth.user_id, payload).await { match FitnessTrainerRepository::upsert_by_user_id(&state.pool, auth.user_id, payload).await {
Ok(p) => (StatusCode::OK, Json(p)).into_response(), Ok(p) => (StatusCode::OK, Json(p)).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
} }

View file

@ -1,5 +1,5 @@
use contracts::ProfessionState; use contracts::ProfessionState;
use db::models::graphic_designer::GraphicDesignerProfile; use db::models::user_role_profile::UserRoleProfile;
use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router}; use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router};
use serde::Serialize; use serde::Serialize;
use uuid::Uuid; use uuid::Uuid;
@ -7,6 +7,7 @@ use uuid::Uuid;
#[derive(Serialize)] #[derive(Serialize)]
pub struct AdminGraphicDesignerList { pub struct AdminGraphicDesignerList {
pub id: Uuid, pub id: Uuid,
pub user_role_profile_id: Uuid,
pub user_id: Uuid, pub user_id: Uuid,
pub display_name: Option<String>, pub display_name: Option<String>,
pub bio: Option<String>, pub bio: Option<String>,
@ -16,10 +17,11 @@ pub struct AdminGraphicDesignerList {
pub updated_at: chrono::DateTime<chrono::Utc>, pub updated_at: chrono::DateTime<chrono::Utc>,
} }
impl From<GraphicDesignerProfile> for AdminGraphicDesignerList { impl From<UserRoleProfile> for AdminGraphicDesignerList {
fn from(p: GraphicDesignerProfile) -> Self { fn from(p: UserRoleProfile) -> Self {
Self { Self {
id: p.id, id: p.id,
user_role_profile_id: p.id,
user_id: p.user_id, user_id: p.user_id,
display_name: p.display_name, display_name: p.display_name,
bio: p.bio, bio: p.bio,
@ -40,10 +42,15 @@ pub fn router() -> Router<ProfessionState> {
async fn list_graphic_designers( async fn list_graphic_designers(
State(state): State<ProfessionState>, State(state): State<ProfessionState>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
let designers = sqlx::query_as::<_, GraphicDesignerProfile>( let designers = sqlx::query_as::<_, UserRoleProfile>(
r#" r#"
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at SELECT id, user_id, role_key, display_name, bio, location,
FROM graphic_designer_profiles avatar_url, phone, email, status,
verification_status, approval_status, rejection_reason,
approved_at, verified_at, is_profile_public,
created_at, updated_at
FROM user_role_profiles
WHERE role_key = 'graphic_designer'
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT 100 LIMIT 100
"#, "#,
@ -60,11 +67,15 @@ async fn get_graphic_designer(
State(state): State<ProfessionState>, State(state): State<ProfessionState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
let designer = sqlx::query_as::<_, GraphicDesignerProfile>( let designer = sqlx::query_as::<_, UserRoleProfile>(
r#" r#"
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at SELECT id, user_id, role_key, display_name, bio, location,
FROM graphic_designer_profiles avatar_url, phone, email, status,
WHERE id = $1 verification_status, approval_status, rejection_reason,
approved_at, verified_at, is_profile_public,
created_at, updated_at
FROM user_role_profiles
WHERE id = $1 AND role_key = 'graphic_designer'
"#, "#,
) )
.bind(id) .bind(id)

View file

@ -21,7 +21,7 @@ async fn update_profile(
auth: AuthUser, auth: AuthUser,
Json(payload): Json<UpsertGraphicDesignerProfilePayload>, Json(payload): Json<UpsertGraphicDesignerProfilePayload>,
) -> impl IntoResponse { ) -> impl IntoResponse {
match GraphicDesignerRepository::upsert(&state.pool, auth.user_id, payload).await { match GraphicDesignerRepository::upsert_by_user_id(&state.pool, auth.user_id, payload).await {
Ok(p) => (StatusCode::OK, Json(p)).into_response(), Ok(p) => (StatusCode::OK, Json(p)).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
} }

View file

@ -38,7 +38,7 @@ pub struct JobBrowseQuery {
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct ApplyRequest { pub struct ApplyRequest {
pub cover_letter: Option<String>, pub cover_note: Option<String>,
pub resume_url: Option<String>, pub resume_url: Option<String>,
} }
@ -234,9 +234,8 @@ async fn apply_to_job(
let db_payload = CreateApplicationPayload { let db_payload = CreateApplicationPayload {
job_id: job.id, job_id: job.id,
job_seeker_id: seeker.id, applicant_user_id: auth.user_id,
cover_letter: payload.cover_letter, cover_note: payload.cover_note,
resume_url: payload.resume_url.or(seeker.resume_url),
}; };
match ApplicationRepository::create(&state.pool, db_payload).await { match ApplicationRepository::create(&state.pool, db_payload).await {
@ -287,7 +286,7 @@ async fn list_my_applications(
let page = q.page.unwrap_or(1); let page = q.page.unwrap_or(1);
let limit = q.limit.unwrap_or(20); let limit = q.limit.unwrap_or(20);
match ApplicationRepository::list_by_job_seeker_id(&state.pool, seeker.id, page, limit).await { match ApplicationRepository::list_by_user_id(&state.pool, auth.user_id, page, limit).await {
Ok(apps) => (StatusCode::OK, Json(serde_json::json!({ Ok(apps) => (StatusCode::OK, Json(serde_json::json!({
"data": apps, "data": apps,
"pagination": { "page": page, "limit": limit } "pagination": { "page": page, "limit": limit }
@ -307,7 +306,7 @@ async fn get_my_application(
}; };
match ApplicationRepository::get_by_id(&state.pool, id).await { match ApplicationRepository::get_by_id(&state.pool, id).await {
Ok(Some(app)) if app.job_seeker_id == seeker.id => (StatusCode::OK, Json(app)).into_response(), Ok(Some(app)) if app.applicant_user_id == auth.user_id => (StatusCode::OK, Json(app)).into_response(),
Ok(Some(_)) => (StatusCode::FORBIDDEN, "Access denied").into_response(), Ok(Some(_)) => (StatusCode::FORBIDDEN, "Access denied").into_response(),
Ok(None) => (StatusCode::NOT_FOUND, "Application not found").into_response(), Ok(None) => (StatusCode::NOT_FOUND, "Application not found").into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
@ -325,7 +324,7 @@ async fn withdraw_application(
}; };
let app = match ApplicationRepository::get_by_id(&state.pool, id).await { let app = match ApplicationRepository::get_by_id(&state.pool, id).await {
Ok(Some(a)) if a.job_seeker_id == seeker.id => a, Ok(Some(a)) if a.applicant_user_id == auth.user_id => a,
Ok(Some(_)) => return (StatusCode::FORBIDDEN, "Access denied").into_response(), Ok(Some(_)) => return (StatusCode::FORBIDDEN, "Access denied").into_response(),
_ => return (StatusCode::NOT_FOUND, "Application not found").into_response(), _ => return (StatusCode::NOT_FOUND, "Application not found").into_response(),
}; };

View file

@ -1,7 +1,7 @@
use axum::{ use axum::{
extract::State, extract::State,
http::StatusCode, http::StatusCode,
routing::{get, post, put, delete}, routing::{get, post},
Json, Router, Json, Router,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -16,7 +16,7 @@ pub struct AppState {
pub pool: PgPool, pub pool: PgPool,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
pub struct Job { pub struct Job {
pub id: uuid::Uuid, pub id: uuid::Uuid,
pub title: String, pub title: String,

View file

@ -1,7 +1,7 @@
use axum::{ use axum::{
extract::State, extract::State,
http::StatusCode, http::StatusCode,
routing::{get, post, put, delete}, routing::{get, post},
Json, Router, Json, Router,
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -16,7 +16,7 @@ pub struct AppState {
pub pool: PgPool, pub pool: PgPool,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
pub struct Lead { pub struct Lead {
pub id: uuid::Uuid, pub id: uuid::Uuid,
pub title: String, pub title: String,
@ -37,7 +37,7 @@ pub struct CreateLead {
async fn list_leads(State(state): State<Arc<AppState>>) -> Result<Json<Vec<Lead>>, StatusCode> { async fn list_leads(State(state): State<Arc<AppState>>) -> Result<Json<Vec<Lead>>, StatusCode> {
let leads = sqlx::query_as::<_, Lead>( let leads = sqlx::query_as::<_, Lead>(
"SELECT id, title, description, location, profession_key, status, created_at FROM requirements ORDER BY created_at DESC" "SELECT id, title, description, location, profession_key, status, created_at FROM leads ORDER BY created_at DESC"
) )
.fetch_all(&state.pool) .fetch_all(&state.pool)
.await .await
@ -52,7 +52,7 @@ async fn create_lead(
) -> Result<Json<Lead>, StatusCode> { ) -> Result<Json<Lead>, StatusCode> {
let lead = sqlx::query_as::<_, Lead>( let lead = sqlx::query_as::<_, Lead>(
r#" r#"
INSERT INTO requirements (title, description, location, profession_key) INSERT INTO leads (title, description, location, profession_key)
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3, $4)
RETURNING id, title, description, location, profession_key, status, created_at RETURNING id, title, description, location, profession_key, status, created_at
"#, "#,
@ -73,7 +73,7 @@ async fn get_lead(
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>, axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
) -> Result<Json<Lead>, StatusCode> { ) -> Result<Json<Lead>, StatusCode> {
let lead = sqlx::query_as::<_, Lead>( let lead = sqlx::query_as::<_, Lead>(
"SELECT id, title, description, location, profession_key, status, created_at FROM requirements WHERE id = $1" "SELECT id, title, description, location, profession_key, status, created_at FROM leads WHERE id = $1"
) )
.bind(id) .bind(id)
.fetch_optional(&state.pool) .fetch_optional(&state.pool)

View file

@ -1,5 +1,5 @@
use contracts::ProfessionState; use contracts::ProfessionState;
use db::models::makeup_artist::MakeupArtistProfile; use db::models::user_role_profile::UserRoleProfile;
use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router}; use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router};
use serde::Serialize; use serde::Serialize;
use uuid::Uuid; use uuid::Uuid;
@ -7,6 +7,7 @@ use uuid::Uuid;
#[derive(Serialize)] #[derive(Serialize)]
pub struct AdminMakeupArtistList { pub struct AdminMakeupArtistList {
pub id: Uuid, pub id: Uuid,
pub user_role_profile_id: Uuid,
pub user_id: Uuid, pub user_id: Uuid,
pub display_name: Option<String>, pub display_name: Option<String>,
pub bio: Option<String>, pub bio: Option<String>,
@ -16,10 +17,11 @@ pub struct AdminMakeupArtistList {
pub updated_at: chrono::DateTime<chrono::Utc>, pub updated_at: chrono::DateTime<chrono::Utc>,
} }
impl From<MakeupArtistProfile> for AdminMakeupArtistList { impl From<UserRoleProfile> for AdminMakeupArtistList {
fn from(p: MakeupArtistProfile) -> Self { fn from(p: UserRoleProfile) -> Self {
Self { Self {
id: p.id, id: p.id,
user_role_profile_id: p.id,
user_id: p.user_id, user_id: p.user_id,
display_name: p.display_name, display_name: p.display_name,
bio: p.bio, bio: p.bio,
@ -40,10 +42,15 @@ pub fn router() -> Router<ProfessionState> {
async fn list_makeup_artists( async fn list_makeup_artists(
State(state): State<ProfessionState>, State(state): State<ProfessionState>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
let artists = sqlx::query_as::<_, MakeupArtistProfile>( let artists = sqlx::query_as::<_, UserRoleProfile>(
r#" r#"
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at SELECT id, user_id, role_key, display_name, bio, location,
FROM makeup_artist_profiles avatar_url, phone, email, status,
verification_status, approval_status, rejection_reason,
approved_at, verified_at, is_profile_public,
created_at, updated_at
FROM user_role_profiles
WHERE role_key = 'makeup_artist'
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT 100 LIMIT 100
"#, "#,
@ -60,11 +67,15 @@ async fn get_makeup_artist(
State(state): State<ProfessionState>, State(state): State<ProfessionState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
let artist = sqlx::query_as::<_, MakeupArtistProfile>( let artist = sqlx::query_as::<_, UserRoleProfile>(
r#" r#"
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at SELECT id, user_id, role_key, display_name, bio, location,
FROM makeup_artist_profiles avatar_url, phone, email, status,
WHERE id = $1 verification_status, approval_status, rejection_reason,
approved_at, verified_at, is_profile_public,
created_at, updated_at
FROM user_role_profiles
WHERE id = $1 AND role_key = 'makeup_artist'
"#, "#,
) )
.bind(id) .bind(id)

View file

@ -21,7 +21,7 @@ async fn update_profile(
auth: AuthUser, auth: AuthUser,
Json(payload): Json<UpsertMakeupArtistProfilePayload>, Json(payload): Json<UpsertMakeupArtistProfilePayload>,
) -> impl IntoResponse { ) -> impl IntoResponse {
match MakeupArtistRepository::upsert(&state.pool, auth.user_id, payload).await { match MakeupArtistRepository::upsert_by_user_id(&state.pool, auth.user_id, payload).await {
Ok(p) => (StatusCode::OK, Json(p)).into_response(), Ok(p) => (StatusCode::OK, Json(p)).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
} }

View file

@ -1,5 +1,5 @@
use contracts::ProfessionState; use contracts::ProfessionState;
use db::models::photographer::PhotographerProfile; use db::models::user_role_profile::UserRoleProfile;
use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router}; use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router};
use serde::Serialize; use serde::Serialize;
use uuid::Uuid; use uuid::Uuid;
@ -7,6 +7,7 @@ use uuid::Uuid;
#[derive(Serialize)] #[derive(Serialize)]
pub struct AdminPhotographerList { pub struct AdminPhotographerList {
pub id: Uuid, pub id: Uuid,
pub user_role_profile_id: Uuid,
pub user_id: Uuid, pub user_id: Uuid,
pub display_name: Option<String>, pub display_name: Option<String>,
pub bio: Option<String>, pub bio: Option<String>,
@ -16,10 +17,11 @@ pub struct AdminPhotographerList {
pub updated_at: chrono::DateTime<chrono::Utc>, pub updated_at: chrono::DateTime<chrono::Utc>,
} }
impl From<PhotographerProfile> for AdminPhotographerList { impl From<UserRoleProfile> for AdminPhotographerList {
fn from(p: PhotographerProfile) -> Self { fn from(p: UserRoleProfile) -> Self {
Self { Self {
id: p.id, id: p.id,
user_role_profile_id: p.id,
user_id: p.user_id, user_id: p.user_id,
display_name: p.display_name, display_name: p.display_name,
bio: p.bio, bio: p.bio,
@ -40,10 +42,15 @@ pub fn router() -> Router<ProfessionState> {
async fn list_photographers( async fn list_photographers(
State(state): State<ProfessionState>, State(state): State<ProfessionState>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
let photographers = sqlx::query_as::<_, PhotographerProfile>( let photographers = sqlx::query_as::<_, UserRoleProfile>(
r#" r#"
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at SELECT id, user_id, role_key, display_name, bio, location,
FROM photographer_profiles avatar_url, phone, email, status,
verification_status, approval_status, rejection_reason,
approved_at, verified_at, is_profile_public,
created_at, updated_at
FROM user_role_profiles
WHERE role_key = 'photographer'
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT 100 LIMIT 100
"#, "#,
@ -60,11 +67,15 @@ async fn get_photographer(
State(state): State<ProfessionState>, State(state): State<ProfessionState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
let photographer = sqlx::query_as::<_, PhotographerProfile>( let photographer = sqlx::query_as::<_, UserRoleProfile>(
r#" r#"
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at SELECT id, user_id, role_key, display_name, bio, location,
FROM photographer_profiles avatar_url, phone, email, status,
WHERE id = $1 verification_status, approval_status, rejection_reason,
approved_at, verified_at, is_profile_public,
created_at, updated_at
FROM user_role_profiles
WHERE id = $1 AND role_key = 'photographer'
"#, "#,
) )
.bind(id) .bind(id)

View file

@ -21,7 +21,7 @@ async fn update_profile(
auth: AuthUser, auth: AuthUser,
Json(payload): Json<UpsertPhotographerProfilePayload>, Json(payload): Json<UpsertPhotographerProfilePayload>,
) -> impl IntoResponse { ) -> impl IntoResponse {
match PhotographerRepository::upsert(&state.pool, auth.user_id, payload).await { match PhotographerRepository::upsert_by_user_id(&state.pool, auth.user_id, payload).await {
Ok(p) => (StatusCode::OK, Json(p)).into_response(), Ok(p) => (StatusCode::OK, Json(p)).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
} }

View file

@ -1,5 +1,5 @@
use contracts::ProfessionState; use contracts::ProfessionState;
use db::models::social_media_manager::SocialMediaManagerProfile; use db::models::user_role_profile::UserRoleProfile;
use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router}; use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router};
use serde::Serialize; use serde::Serialize;
use uuid::Uuid; use uuid::Uuid;
@ -7,6 +7,7 @@ use uuid::Uuid;
#[derive(Serialize)] #[derive(Serialize)]
pub struct AdminSocialMediaManagerList { pub struct AdminSocialMediaManagerList {
pub id: Uuid, pub id: Uuid,
pub user_role_profile_id: Uuid,
pub user_id: Uuid, pub user_id: Uuid,
pub display_name: Option<String>, pub display_name: Option<String>,
pub bio: Option<String>, pub bio: Option<String>,
@ -16,10 +17,11 @@ pub struct AdminSocialMediaManagerList {
pub updated_at: chrono::DateTime<chrono::Utc>, pub updated_at: chrono::DateTime<chrono::Utc>,
} }
impl From<SocialMediaManagerProfile> for AdminSocialMediaManagerList { impl From<UserRoleProfile> for AdminSocialMediaManagerList {
fn from(p: SocialMediaManagerProfile) -> Self { fn from(p: UserRoleProfile) -> Self {
Self { Self {
id: p.id, id: p.id,
user_role_profile_id: p.id,
user_id: p.user_id, user_id: p.user_id,
display_name: p.display_name, display_name: p.display_name,
bio: p.bio, bio: p.bio,
@ -40,10 +42,15 @@ pub fn router() -> Router<ProfessionState> {
async fn list_social_media_managers( async fn list_social_media_managers(
State(state): State<ProfessionState>, State(state): State<ProfessionState>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
let managers = sqlx::query_as::<_, SocialMediaManagerProfile>( let managers = sqlx::query_as::<_, UserRoleProfile>(
r#" r#"
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at SELECT id, user_id, role_key, display_name, bio, location,
FROM social_media_manager_profiles avatar_url, phone, email, status,
verification_status, approval_status, rejection_reason,
approved_at, verified_at, is_profile_public,
created_at, updated_at
FROM user_role_profiles
WHERE role_key = 'social_media_manager'
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT 100 LIMIT 100
"#, "#,
@ -60,11 +67,15 @@ async fn get_social_media_manager(
State(state): State<ProfessionState>, State(state): State<ProfessionState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
let manager = sqlx::query_as::<_, SocialMediaManagerProfile>( let manager = sqlx::query_as::<_, UserRoleProfile>(
r#" r#"
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at SELECT id, user_id, role_key, display_name, bio, location,
FROM social_media_manager_profiles avatar_url, phone, email, status,
WHERE id = $1 verification_status, approval_status, rejection_reason,
approved_at, verified_at, is_profile_public,
created_at, updated_at
FROM user_role_profiles
WHERE id = $1 AND role_key = 'social_media_manager'
"#, "#,
) )
.bind(id) .bind(id)

View file

@ -21,7 +21,7 @@ async fn update_profile(
auth: AuthUser, auth: AuthUser,
Json(payload): Json<UpsertSocialMediaManagerProfilePayload>, Json(payload): Json<UpsertSocialMediaManagerProfilePayload>,
) -> impl IntoResponse { ) -> impl IntoResponse {
match SocialMediaManagerRepository::upsert(&state.pool, auth.user_id, payload).await { match SocialMediaManagerRepository::upsert_by_user_id(&state.pool, auth.user_id, payload).await {
Ok(p) => (StatusCode::OK, Json(p)).into_response(), Ok(p) => (StatusCode::OK, Json(p)).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
} }

View file

@ -1,5 +1,5 @@
use contracts::ProfessionState; use contracts::ProfessionState;
use db::models::tutor::TutorProfile; use db::models::user_role_profile::UserRoleProfile;
use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router}; use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router};
use serde::Serialize; use serde::Serialize;
use uuid::Uuid; use uuid::Uuid;
@ -7,6 +7,7 @@ use uuid::Uuid;
#[derive(Serialize)] #[derive(Serialize)]
pub struct AdminTutorList { pub struct AdminTutorList {
pub id: Uuid, pub id: Uuid,
pub user_role_profile_id: Uuid,
pub user_id: Uuid, pub user_id: Uuid,
pub display_name: Option<String>, pub display_name: Option<String>,
pub bio: Option<String>, pub bio: Option<String>,
@ -16,10 +17,11 @@ pub struct AdminTutorList {
pub updated_at: chrono::DateTime<chrono::Utc>, pub updated_at: chrono::DateTime<chrono::Utc>,
} }
impl From<TutorProfile> for AdminTutorList { impl From<UserRoleProfile> for AdminTutorList {
fn from(p: TutorProfile) -> Self { fn from(p: UserRoleProfile) -> Self {
Self { Self {
id: p.id, id: p.id,
user_role_profile_id: p.id,
user_id: p.user_id, user_id: p.user_id,
display_name: p.display_name, display_name: p.display_name,
bio: p.bio, bio: p.bio,
@ -43,10 +45,15 @@ pub fn router() -> Router<ProfessionState> {
async fn list_tutors( async fn list_tutors(
State(state): State<ProfessionState>, State(state): State<ProfessionState>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
let tutors = sqlx::query_as::<_, TutorProfile>( let tutors = sqlx::query_as::<_, UserRoleProfile>(
r#" r#"
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at SELECT id, user_id, role_key, display_name, bio, location,
FROM tutor_profiles avatar_url, phone, email, status,
verification_status, approval_status, rejection_reason,
approved_at, verified_at, is_profile_public,
created_at, updated_at
FROM user_role_profiles
WHERE role_key = 'tutor'
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT 100 LIMIT 100
"#, "#,
@ -63,11 +70,15 @@ async fn get_tutor(
State(state): State<ProfessionState>, State(state): State<ProfessionState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
let tutor = sqlx::query_as::<_, TutorProfile>( let tutor = sqlx::query_as::<_, UserRoleProfile>(
r#" r#"
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at SELECT id, user_id, role_key, display_name, bio, location,
FROM tutor_profiles avatar_url, phone, email, status,
WHERE id = $1 verification_status, approval_status, rejection_reason,
approved_at, verified_at, is_profile_public,
created_at, updated_at
FROM user_role_profiles
WHERE id = $1 AND role_key = 'tutor'
"#, "#,
) )
.bind(id) .bind(id)

View file

@ -21,7 +21,7 @@ async fn update_profile(
auth: AuthUser, auth: AuthUser,
Json(payload): Json<UpsertTutorProfilePayload>, Json(payload): Json<UpsertTutorProfilePayload>,
) -> impl IntoResponse { ) -> impl IntoResponse {
match TutorRepository::upsert(&state.pool, auth.user_id, payload).await { match TutorRepository::upsert_by_user_id(&state.pool, auth.user_id, payload).await {
Ok(p) => (StatusCode::OK, Json(p)).into_response(), Ok(p) => (StatusCode::OK, Json(p)).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
} }

View file

@ -21,7 +21,7 @@ async fn update_profile(
auth: AuthUser, auth: AuthUser,
Json(payload): Json<UpsertUgcContentCreatorProfilePayload>, Json(payload): Json<UpsertUgcContentCreatorProfilePayload>,
) -> impl IntoResponse { ) -> impl IntoResponse {
match UgcContentCreatorRepository::upsert(&state.pool, auth.user_id, payload).await { match UgcContentCreatorRepository::upsert_by_user_id(&state.pool, auth.user_id, payload).await {
Ok(p) => (StatusCode::OK, Json(p)).into_response(), Ok(p) => (StatusCode::OK, Json(p)).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
} }

View file

@ -205,11 +205,23 @@ async fn activate_profile_after_final_approval(
_ => return Ok(()), _ => return Ok(()),
}; };
let user_role_profile_id = match sqlx::query_scalar::<_, Uuid>(
"SELECT id FROM user_role_profiles WHERE user_id = $1 AND role_key = $2",
)
.bind(user_id)
.bind(&role_key)
.fetch_optional(&state.pool)
.await?
{
Some(id) => id,
None => return Ok(()),
};
let query = format!( let query = format!(
"UPDATE {} SET status = 'APPROVED', updated_at = NOW() WHERE user_id = $1", "UPDATE {} SET verification_status = 'APPROVED', updated_at = NOW() WHERE id = $1",
table table
); );
sqlx::query(&query).bind(user_id).execute(&state.pool).await?; sqlx::query(&query).bind(user_role_profile_id).execute(&state.pool).await?;
sqlx::query( sqlx::query(
"UPDATE users SET status = 'ACTIVE', updated_at = NOW() WHERE id = $1 AND status = 'PENDING'", "UPDATE users SET status = 'ACTIVE', updated_at = NOW() WHERE id = $1 AND status = 'PENDING'",
@ -267,11 +279,23 @@ async fn reject_profile_after_final_approval(
_ => return Ok(()), _ => return Ok(()),
}; };
let user_role_profile_id = match sqlx::query_scalar::<_, Uuid>(
"SELECT id FROM user_role_profiles WHERE user_id = $1 AND role_key = $2",
)
.bind(user_id)
.bind(&role_key)
.fetch_optional(&state.pool)
.await?
{
Some(id) => id,
None => return Ok(()),
};
let query = format!( let query = format!(
"UPDATE {} SET status = 'REJECTED', updated_at = NOW() WHERE user_id = $1", "UPDATE {} SET verification_status = 'REJECTED', updated_at = NOW() WHERE id = $1",
table table
); );
sqlx::query(&query).bind(user_id).execute(&state.pool).await?; sqlx::query(&query).bind(user_role_profile_id).execute(&state.pool).await?;
if let Ok(user) = UserRepository::get_by_id(&state.pool, user_id).await { if let Ok(user) = UserRepository::get_by_id(&state.pool, user_id).await {
let display = role_key_to_display(&role_key); let display = role_key_to_display(&role_key);

View file

@ -28,7 +28,7 @@ async fn get_metrics(State(state): State<crate::AppState>) -> Json<DashboardMetr
.unwrap_or(0); .unwrap_or(0);
let open_leads: i64 = sqlx::query_scalar::<_, i64>( let open_leads: i64 = sqlx::query_scalar::<_, i64>(
"SELECT COUNT(*) FROM requirements WHERE status = 'PENDING_APPROVAL' OR status = 'APPROVED'", "SELECT COUNT(*) FROM leads WHERE status = 'PENDING_APPROVAL' OR status = 'APPROVED'",
) )
.fetch_one(&state.pool) .fetch_one(&state.pool)
.await .await
@ -37,13 +37,7 @@ async fn get_metrics(State(state): State<crate::AppState>) -> Json<DashboardMetr
let pending_approvals: i64 = sqlx::query_scalar::<_, i64>( let pending_approvals: i64 = sqlx::query_scalar::<_, i64>(
r#" r#"
SELECT COUNT(*) FROM ( SELECT COUNT(*) FROM (
SELECT id FROM company_profiles WHERE status = 'PENDING_APPROVAL' SELECT id FROM user_role_profiles WHERE status = 'PENDING_APPROVAL'
UNION ALL
SELECT id FROM customer_profiles WHERE status = 'PENDING_APPROVAL'
UNION ALL
SELECT id FROM job_seeker_profiles WHERE status = 'PENDING_APPROVAL'
UNION ALL
SELECT id FROM professionals WHERE status = 'PENDING_APPROVAL'
) sub ) sub
"#, "#,
) )
@ -132,9 +126,8 @@ async fn get_metrics(State(state): State<crate::AppState>) -> Json<DashboardMetr
r#" r#"
SELECT r.id, r.title, r.status, r.created_at, SELECT r.id, r.title, r.status, r.created_at,
u.full_name AS requester_name u.full_name AS requester_name
FROM requirements r FROM leads r
LEFT JOIN customer_profiles cp ON cp.id = r.customer_id LEFT JOIN users u ON u.id = r.created_by_user_id
LEFT JOIN users u ON u.id = cp.user_id
WHERE r.status IN ('PENDING_APPROVAL', 'APPROVED') WHERE r.status IN ('PENDING_APPROVAL', 'APPROVED')
ORDER BY r.created_at DESC ORDER BY r.created_at DESC
LIMIT 5 LIMIT 5

View file

@ -162,11 +162,20 @@ async fn submit(
}; };
if let Some(tbl) = table_name { if let Some(tbl) = table_name {
let user_role_profile_id = get_or_create_user_role_profile_id(
&state.pool,
auth.user_id,
&role_key,
role.id,
)
.await
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to create user role profile: {}", e)))?;
let query = format!( let query = format!(
r#" r#"
INSERT INTO {} (user_id, "profileData", verification_status, submitted_at, updated_at) INSERT INTO {} (id, "profileData", verification_status, submitted_at, updated_at)
VALUES ($1, $2, 'PENDING', NOW(), NOW()) VALUES ($1, $2, 'PENDING', NOW(), NOW())
ON CONFLICT (user_id) DO UPDATE SET ON CONFLICT (id) DO UPDATE SET
"profileData" = EXCLUDED."profileData", "profileData" = EXCLUDED."profileData",
verification_status = 'PENDING', verification_status = 'PENDING',
submitted_at = NOW(), submitted_at = NOW(),
@ -176,7 +185,7 @@ async fn submit(
); );
sqlx::query(&query) sqlx::query(&query)
.bind(auth.user_id) .bind(user_role_profile_id)
.bind(&progress) .bind(&progress)
.execute(&state.pool) .execute(&state.pool)
.await .await
@ -254,3 +263,35 @@ async fn profile_status(
}), }),
)) ))
} }
async fn get_or_create_user_role_profile_id(
pool: &sqlx::PgPool,
user_id: uuid::Uuid,
role_key: &str,
role_id: uuid::Uuid,
) -> Result<uuid::Uuid, sqlx::Error> {
if let Some(id) = sqlx::query_scalar::<_, uuid::Uuid>(
r#"SELECT id FROM user_role_profiles WHERE user_id = $1 AND role_key = $2"#,
)
.bind(user_id)
.bind(role_key)
.fetch_optional(pool)
.await?
{
return Ok(id);
}
sqlx::query_scalar::<_, uuid::Uuid>(
r#"
INSERT INTO user_role_profiles (user_id, role_key, role_id, status)
VALUES ($1, $2, $3, 'DRAFT')
ON CONFLICT (user_id, role_key) DO UPDATE SET updated_at = NOW()
RETURNING id
"#,
)
.bind(user_id)
.bind(role_key)
.bind(role_id)
.fetch_one(pool)
.await
}

View file

@ -161,12 +161,28 @@ async fn get_profile(
}; };
let query = format!( let query = format!(
r#"SELECT "profileData", verification_status FROM {} WHERE user_id = $1"#, r#"SELECT "profileData", verification_status FROM {} WHERE id = $1"#,
table table
); );
let user_role_profile_id = match get_user_role_profile_id(&state.pool, auth.user_id, &role_key).await {
Ok(Some(id)) => id,
Ok(None) => {
return (
StatusCode::OK,
Json(serde_json::json!({
"role_key": role_key,
"profile_data": null,
"verification_status": "NOT_STARTED",
})),
)
.into_response();
}
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
};
match sqlx::query(&query) match sqlx::query(&query)
.bind(auth.user_id) .bind(user_role_profile_id)
.fetch_optional(&state.pool) .fetch_optional(&state.pool)
.await .await
{ {
@ -252,16 +268,21 @@ async fn save_profile(
let query = format!( let query = format!(
r#" r#"
INSERT INTO {table} (user_id, "profileData", verification_status, updated_at) INSERT INTO {table} (id, "profileData", verification_status, updated_at)
VALUES ($1, $2, 'DRAFT', NOW()) VALUES ($1, $2, 'DRAFT', NOW())
ON CONFLICT (user_id) DO UPDATE SET ON CONFLICT (id) DO UPDATE SET
"profileData" = EXCLUDED."profileData", "profileData" = EXCLUDED."profileData",
updated_at = NOW() updated_at = NOW()
"# "#
); );
let user_role_profile_id = match get_or_create_user_role_profile_id(&state.pool, auth.user_id, &role_key).await {
Ok(id) => id,
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
};
match sqlx::query(&query) match sqlx::query(&query)
.bind(auth.user_id) .bind(user_role_profile_id)
.bind(&input.profile_data) .bind(&input.profile_data)
.execute(&state.pool) .execute(&state.pool)
.await .await
@ -434,18 +455,8 @@ async fn fetch_saved_profile(
}; };
} }
if let Some(table) = role_to_table(role_key) { if let Some(urp_id) = get_user_role_profile_id(&state.pool, user_id, role_key).await.ok().flatten() {
let q = format!(r#"SELECT "profileData" FROM {} WHERE user_id = $1"#, table); return fetch_saved_profile_by_urp_id(state, urp_id, role_key).await;
if let Ok(Some(row)) = sqlx::query(&q)
.bind(user_id)
.fetch_optional(&state.pool)
.await
{
use sqlx::Row;
return row
.try_get::<serde_json::Value, _>("profileData")
.unwrap_or(serde_json::Value::Object(Default::default()));
}
} }
serde_json::Value::Object(Default::default()) serde_json::Value::Object(Default::default())
@ -464,16 +475,86 @@ async fn set_profile_status(state: &AppState, user_id: Uuid, role_key: &str, sta
return; return;
} }
let user_role_profile_id = match get_user_role_profile_id(&state.pool, user_id, role_key).await {
Ok(Some(id)) => id,
Ok(None) => return,
Err(_) => return,
};
if let Some(table) = role_to_table(role_key) { if let Some(table) = role_to_table(role_key) {
let q = format!( let q = format!(
"UPDATE {} SET verification_status = $1, submitted_at = NOW(), updated_at = NOW() WHERE user_id = $2", "UPDATE {} SET verification_status = $1, submitted_at = NOW(), updated_at = NOW() WHERE id = $2",
table table
); );
sqlx::query(&q) sqlx::query(&q)
.bind(status) .bind(status)
.bind(user_id) .bind(user_role_profile_id)
.execute(&state.pool) .execute(&state.pool)
.await .await
.ok(); .ok();
} }
} }
async fn get_user_role_profile_id(
pool: &sqlx::PgPool,
user_id: Uuid,
role_key: &str,
) -> Result<Option<Uuid>, sqlx::Error> {
sqlx::query_scalar::<_, Uuid>(
r#"
SELECT id FROM user_role_profiles
WHERE user_id = $1 AND role_key = $2
"#,
)
.bind(user_id)
.bind(role_key)
.fetch_optional(pool)
.await
}
async fn get_or_create_user_role_profile_id(
pool: &sqlx::PgPool,
user_id: Uuid,
role_key: &str,
) -> Result<Uuid, sqlx::Error> {
if let Some(id) = get_user_role_profile_id(pool, user_id, role_key).await? {
return Ok(id);
}
let role = RoleRepository::get_by_key(pool, role_key).await?;
sqlx::query_scalar::<_, Uuid>(
r#"
INSERT INTO user_role_profiles (user_id, role_key, role_id, status)
VALUES ($1, $2, $3, 'DRAFT')
ON CONFLICT (user_id, role_key) DO UPDATE SET updated_at = NOW()
RETURNING id
"#,
)
.bind(user_id)
.bind(role_key)
.bind(role.id)
.fetch_one(pool)
.await
}
async fn fetch_saved_profile_by_urp_id(
state: &AppState,
user_role_profile_id: Uuid,
role_key: &str,
) -> serde_json::Value {
if let Some(table) = role_to_table(role_key) {
let q = format!(r#"SELECT "profileData" FROM {} WHERE id = $1"#, table);
if let Ok(Some(row)) = sqlx::query(&q)
.bind(user_role_profile_id)
.fetch_optional(&state.pool)
.await
{
use sqlx::Row;
return row
.try_get::<serde_json::Value, _>("profileData")
.unwrap_or(serde_json::Value::Object(Default::default()));
}
}
serde_json::Value::Object(Default::default())
}

View file

@ -123,11 +123,23 @@ async fn trigger_rejection(
_ => return Ok(()), _ => return Ok(()),
}; };
let user_role_profile_id = match sqlx::query_scalar::<_, Uuid>(
"SELECT id FROM user_role_profiles WHERE user_id = $1 AND role_key = $2",
)
.bind(user_id)
.bind(&role_key)
.fetch_optional(&state.pool)
.await?
{
Some(id) => id,
None => return Ok(()),
};
let query = format!( let query = format!(
"UPDATE {} SET status = 'REJECTED', updated_at = NOW() WHERE user_id = $1", "UPDATE {} SET verification_status = 'REJECTED', updated_at = NOW() WHERE id = $1",
table table
); );
sqlx::query(&query).bind(user_id).execute(&state.pool).await?; sqlx::query(&query).bind(user_role_profile_id).execute(&state.pool).await?;
// Send Email // Send Email
if let Ok(user) = db::models::user::UserRepository::get_by_id(&state.pool, user_id).await { if let Ok(user) = db::models::user::UserRepository::get_by_id(&state.pool, user_id).await {

View file

@ -1,5 +1,5 @@
use contracts::ProfessionState; use contracts::ProfessionState;
use db::models::video_editor::VideoEditorProfile; use db::models::user_role_profile::UserRoleProfile;
use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router}; use axum::{extract::{Path, State}, http::StatusCode, response::IntoResponse, routing::get, Json, Router};
use serde::Serialize; use serde::Serialize;
use uuid::Uuid; use uuid::Uuid;
@ -7,6 +7,7 @@ use uuid::Uuid;
#[derive(Serialize)] #[derive(Serialize)]
pub struct AdminVideoEditorList { pub struct AdminVideoEditorList {
pub id: Uuid, pub id: Uuid,
pub user_role_profile_id: Uuid,
pub user_id: Uuid, pub user_id: Uuid,
pub display_name: Option<String>, pub display_name: Option<String>,
pub bio: Option<String>, pub bio: Option<String>,
@ -16,10 +17,11 @@ pub struct AdminVideoEditorList {
pub updated_at: chrono::DateTime<chrono::Utc>, pub updated_at: chrono::DateTime<chrono::Utc>,
} }
impl From<VideoEditorProfile> for AdminVideoEditorList { impl From<UserRoleProfile> for AdminVideoEditorList {
fn from(p: VideoEditorProfile) -> Self { fn from(p: UserRoleProfile) -> Self {
Self { Self {
id: p.id, id: p.id,
user_role_profile_id: p.id,
user_id: p.user_id, user_id: p.user_id,
display_name: p.display_name, display_name: p.display_name,
bio: p.bio, bio: p.bio,
@ -40,10 +42,15 @@ pub fn router() -> Router<ProfessionState> {
async fn list_video_editors( async fn list_video_editors(
State(state): State<ProfessionState>, State(state): State<ProfessionState>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
let editors = sqlx::query_as::<_, VideoEditorProfile>( let editors = sqlx::query_as::<_, UserRoleProfile>(
r#" r#"
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at SELECT id, user_id, role_key, display_name, bio, location,
FROM video_editor_profiles avatar_url, phone, email, status,
verification_status, approval_status, rejection_reason,
approved_at, verified_at, is_profile_public,
created_at, updated_at
FROM user_role_profiles
WHERE role_key = 'video_editor'
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT 100 LIMIT 100
"#, "#,
@ -60,11 +67,15 @@ async fn get_video_editor(
State(state): State<ProfessionState>, State(state): State<ProfessionState>,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> Result<impl IntoResponse, (StatusCode, String)> { ) -> Result<impl IntoResponse, (StatusCode, String)> {
let editor = sqlx::query_as::<_, VideoEditorProfile>( let editor = sqlx::query_as::<_, UserRoleProfile>(
r#" r#"
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at SELECT id, user_id, role_key, display_name, bio, location,
FROM video_editor_profiles avatar_url, phone, email, status,
WHERE id = $1 verification_status, approval_status, rejection_reason,
approved_at, verified_at, is_profile_public,
created_at, updated_at
FROM user_role_profiles
WHERE id = $1 AND role_key = 'video_editor'
"#, "#,
) )
.bind(id) .bind(id)

View file

@ -21,7 +21,7 @@ async fn update_profile(
auth: AuthUser, auth: AuthUser,
Json(payload): Json<UpsertVideoEditorProfilePayload>, Json(payload): Json<UpsertVideoEditorProfilePayload>,
) -> impl IntoResponse { ) -> impl IntoResponse {
match VideoEditorRepository::upsert(&state.pool, auth.user_id, payload).await { match VideoEditorRepository::upsert_by_user_id(&state.pool, auth.user_id, payload).await {
Ok(p) => (StatusCode::OK, Json(p)).into_response(), Ok(p) => (StatusCode::OK, Json(p)).into_response(),
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
} }

View file

@ -9,6 +9,8 @@ use chrono::Utc;
use serde::Deserialize; use serde::Deserialize;
use uuid::Uuid; use uuid::Uuid;
use db::models::lead_request::{CreateLeadRequestPayload, LeadRequestRepository}; use db::models::lead_request::{CreateLeadRequestPayload, LeadRequestRepository};
use db::models::tracecoin_wallet::TracecoinWalletRepository;
use db::models::requirement::RequirementRepository;
use db::models::professional::{ use db::models::professional::{
CreatePortfolioItemPayload, CreatePortfolioItemPayload,
CreateServicePayload, CreateServicePayload,
@ -16,7 +18,7 @@ use db::models::professional::{
UpdatePortfolioItemPayload, UpdatePortfolioItemPayload,
UpdateServicePayload, UpdateServicePayload,
}; };
use db::models::requirement::RequirementRepository; use db::models::user_role_profile::UserRoleProfileRepository;
use crate::auth_middleware::AuthUser; use crate::auth_middleware::AuthUser;
use crate::ProfessionState; use crate::ProfessionState;
@ -35,7 +37,10 @@ pub struct LeadRequestPayload {
/// `profession_key` must be a `'static str` matching the role key, e.g. `"PHOTOGRAPHER"`. /// `profession_key` must be a `'static str` matching the role key, e.g. `"PHOTOGRAPHER"`.
pub fn shared_routes(profession_key: &'static str) -> Router<ProfessionState> { pub fn shared_routes(profession_key: &'static str) -> Router<ProfessionState> {
Router::new() Router::new()
.route("/profile/submit", post(submit_for_verification)) .route("/profile/submit", post({
let pk = profession_key;
move |state, auth| submit_for_verification(state, auth, pk)
}))
// ── Marketplace (Redis-cached) ──────────────────────────────────────── // ── Marketplace (Redis-cached) ────────────────────────────────────────
.route( .route(
"/marketplace", "/marketplace",
@ -129,9 +134,10 @@ async fn send_lead_request(
return (StatusCode::TOO_MANY_REQUESTS, "Too many lead requests. Try again later.").into_response(); return (StatusCode::TOO_MANY_REQUESTS, "Too many lead requests. Try again later.").into_response();
} }
let prof = match ProfessionalRepository::get_by_user_id(&state.pool, auth.user_id).await { let user_role_profile = match UserRoleProfileRepository::get_by_user_and_role(&state.pool, auth.user_id, profession_key).await {
Ok(p) => p, Ok(Some(p)) => p,
Err(_) => return (StatusCode::NOT_FOUND, "Professional profile not found").into_response(), Ok(None) => return (StatusCode::NOT_FOUND, "Professional profile not found").into_response(),
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
}; };
match is_professional_profile_approved(&state.pool, auth.user_id, profession_key).await { match is_professional_profile_approved(&state.pool, auth.user_id, profession_key).await {
@ -152,7 +158,7 @@ async fn send_lead_request(
// ── Deduplication: one lead per requirement per professional (24 h) ──────── // ── Deduplication: one lead per requirement per professional (24 h) ────────
let duplicate = cache::lead::is_duplicate( let duplicate = cache::lead::is_duplicate(
&mut redis, &mut redis,
&prof.id.to_string(), &user_role_profile.id.to_string(),
&payload.requirement_id.to_string(), &payload.requirement_id.to_string(),
) )
.await .await
@ -172,24 +178,23 @@ async fn send_lead_request(
return (StatusCode::CONFLICT, "Requirement reached max requests").into_response(); return (StatusCode::CONFLICT, "Requirement reached max requests").into_response();
} }
let wallet = match ProfessionalRepository::get_wallet(&state.pool, auth.user_id).await { let wallet = match TracecoinWalletRepository::get_by_user_id(&state.pool, auth.user_id).await {
Ok(w) => w, Ok(w) => w,
Err(_) => return (StatusCode::BAD_REQUEST, "Wallet not found").into_response(), Err(_) => return (StatusCode::BAD_REQUEST, "Wallet not found").into_response(),
}; };
if wallet.balance < 25 { if wallet.current_balance < 25 {
return (StatusCode::PAYMENT_REQUIRED, "Insufficient Tracecoin balance").into_response(); return (StatusCode::PAYMENT_REQUIRED, "Insufficient Tracecoin balance").into_response();
} }
let db_payload = CreateLeadRequestPayload { let db_payload = CreateLeadRequestPayload {
requirement_id: req.id, user_role_profile_id: user_role_profile.id,
professional_id: prof.id,
expires_at: Utc::now() + chrono::Duration::hours(24), expires_at: Utc::now() + chrono::Duration::hours(24),
}; };
match LeadRequestRepository::create(&state.pool, db_payload).await { match LeadRequestRepository::create(&state.pool, db_payload).await {
Ok(lead) => { Ok(lead) => {
let reserved = ProfessionalRepository::try_reserve_tracecoins( let reserved = TracecoinWalletRepository::try_reserve_tracecoins(
&state.pool, &state.pool,
auth.user_id, auth.user_id,
lead.tracecoins_reserved, lead.tracecoins_reserved,
@ -213,7 +218,7 @@ async fn send_lead_request(
// Mark dedup in Redis so this professional can't spam the same requirement // Mark dedup in Redis so this professional can't spam the same requirement
let _ = cache::lead::mark_sent( let _ = cache::lead::mark_sent(
&mut redis, &mut redis,
&prof.id.to_string(), &user_role_profile.id.to_string(),
&payload.requirement_id.to_string(), &payload.requirement_id.to_string(),
) )
.await; .await;
@ -427,9 +432,10 @@ async fn cancel_request(
auth: AuthUser, auth: AuthUser,
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let prof = match ProfessionalRepository::get_by_user_id(&state.pool, auth.user_id).await { let user_role_profile = match UserRoleProfileRepository::get_by_user_and_role(&state.pool, auth.user_id, "PHOTOGRAPHER").await {
Ok(p) => p, Ok(Some(p)) => p,
Err(_) => return (StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "Professional profile not found" }))).into_response(), Ok(None) => return (StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "Professional profile not found" }))).into_response(),
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))).into_response(),
}; };
let lead = match LeadRequestRepository::get_by_id(&state.pool, id).await { let lead = match LeadRequestRepository::get_by_id(&state.pool, id).await {
@ -438,7 +444,7 @@ async fn cancel_request(
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))).into_response(), Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))).into_response(),
}; };
if lead.professional_id != prof.id { if lead.user_role_profile_id != user_role_profile.id {
return (StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "Access denied" }))).into_response(); return (StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "Access denied" }))).into_response();
} }
@ -450,7 +456,7 @@ async fn cancel_request(
// Release reserved Tracecoins back to balance // Release reserved Tracecoins back to balance
if lead.tracecoins_reserved > 0 { if lead.tracecoins_reserved > 0 {
let _ = ProfessionalRepository::try_release_reserved_tracecoins( let _ = TracecoinWalletRepository::try_release_reserved_tracecoins(
&state.pool, &state.pool,
auth.user_id, auth.user_id,
lead.tracecoins_reserved, lead.tracecoins_reserved,
@ -470,51 +476,42 @@ async fn accepted_leads(
auth: AuthUser, auth: AuthUser,
Query(q): Query<PaginationQuery>, Query(q): Query<PaginationQuery>,
) -> impl IntoResponse { ) -> impl IntoResponse {
let prof = match ProfessionalRepository::get_by_user_id(&state.pool, auth.user_id).await { let user_role_profile = match UserRoleProfileRepository::get_by_user_and_role(&state.pool, auth.user_id, "PHOTOGRAPHER").await {
Ok(p) => p, Ok(Some(p)) => p,
Err(_) => return (StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "Professional profile not found" }))).into_response(), Ok(None) => return (StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "Professional profile not found" }))).into_response(),
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))).into_response(),
}; };
let page = q.page.unwrap_or(1).max(1); let page = q.page.unwrap_or(1).max(1);
let limit = q.limit.unwrap_or(20).clamp(1, 100); let limit = q.limit.unwrap_or(20).clamp(1, 100);
let offset = (page - 1) * limit; let offset = (page - 1) * limit;
// Join lead_requests → requirements → customers → users to get full contact info
let rows = sqlx::query( let rows = sqlx::query(
r#" r#"
SELECT SELECT
lr.id AS lead_id, lr.id AS lead_request_id,
lr.status, lr.status,
lr.requested_at, lr.requested_at,
lr.resolved_at, lr.resolved_at,
r.id AS requirement_id, lr.tracecoins_reserved,
r.title AS requirement_title, lr.user_role_profile_id
r.description AS requirement_description,
r.location AS requirement_location,
r.profession_key,
u.full_name AS customer_name,
u.email AS customer_email,
u.phone AS customer_phone
FROM lead_requests lr FROM lead_requests lr
INNER JOIN requirements r ON r.id = lr.requirement_id WHERE lr.user_role_profile_id = $1
INNER JOIN customers c ON c.id = r.customer_id
INNER JOIN users u ON u.id = c.user_id
WHERE lr.professional_id = $1
AND lr.status = 'ACCEPTED' AND lr.status = 'ACCEPTED'
ORDER BY lr.resolved_at DESC ORDER BY lr.resolved_at DESC
LIMIT $2 OFFSET $3 LIMIT $2 OFFSET $3
"# "#
) )
.bind(prof.id) .bind(user_role_profile.id)
.bind(limit) .bind(limit)
.bind(offset) .bind(offset)
.fetch_all(&state.pool) .fetch_all(&state.pool)
.await; .await;
let total: i64 = sqlx::query_scalar( let total: i64 = sqlx::query_scalar(
"SELECT COUNT(*) FROM lead_requests WHERE professional_id = $1 AND status = 'ACCEPTED'" "SELECT COUNT(*) FROM lead_requests WHERE user_role_profile_id = $1 AND status = 'ACCEPTED'"
) )
.bind(prof.id) .bind(user_role_profile.id)
.fetch_one(&state.pool) .fetch_one(&state.pool)
.await .await
.unwrap_or(0); .unwrap_or(0);
@ -524,20 +521,12 @@ async fn accepted_leads(
use sqlx::Row; use sqlx::Row;
let data: Vec<serde_json::Value> = rows.iter().map(|row| { let data: Vec<serde_json::Value> = rows.iter().map(|row| {
serde_json::json!({ serde_json::json!({
"lead_id": row.get::<Uuid, _>("lead_id"), "lead_request_id": row.get::<Uuid, _>("lead_request_id"),
"status": row.get::<String, _>("status"), "status": row.get::<String, _>("status"),
"requested_at": row.get::<chrono::DateTime<chrono::Utc>, _>("requested_at"), "requested_at": row.get::<chrono::DateTime<chrono::Utc>, _>("requested_at"),
"resolved_at": row.try_get::<chrono::DateTime<chrono::Utc>, _>("resolved_at").ok(), "resolved_at": row.try_get::<chrono::DateTime<chrono::Utc>, _>("resolved_at").ok(),
"requirement_id": row.get::<Uuid, _>("requirement_id"), "tracecoins_reserved": row.get::<i32, _>("tracecoins_reserved"),
"requirement_title": row.get::<String, _>("requirement_title"), "user_role_profile_id": row.get::<Uuid, _>("user_role_profile_id"),
"requirement_description": row.try_get::<String, _>("requirement_description").ok(),
"requirement_location": row.try_get::<String, _>("requirement_location").ok(),
"profession_key": row.get::<String, _>("profession_key"),
"customer": {
"name": row.try_get::<String, _>("customer_name").ok(),
"email": row.get::<String, _>("customer_email"),
"phone": row.try_get::<String, _>("customer_phone").ok(),
}
}) })
}).collect(); }).collect();
@ -779,6 +768,7 @@ async fn wallet_invoice_detail(
async fn submit_for_verification( async fn submit_for_verification(
State(state): State<ProfessionState>, State(state): State<ProfessionState>,
auth: AuthUser, auth: AuthUser,
profession_key: &'static str,
) -> impl IntoResponse { ) -> impl IntoResponse {
let prof = match ProfessionalRepository::get_by_user_id(&state.pool, auth.user_id).await { let prof = match ProfessionalRepository::get_by_user_id(&state.pool, auth.user_id).await {
Ok(p) => p, Ok(p) => p,
@ -789,7 +779,7 @@ async fn submit_for_verification(
return (StatusCode::BAD_REQUEST, format!("Profile is already {}", prof.status)).into_response(); return (StatusCode::BAD_REQUEST, format!("Profile is already {}", prof.status)).into_response();
} }
match ProfessionalRepository::submit_for_verification(&state.pool, auth.user_id).await { match ProfessionalRepository::submit_for_verification(&state.pool, auth.user_id, profession_key).await {
Ok(profile) => (StatusCode::OK, Json(serde_json::json!({ Ok(profile) => (StatusCode::OK, Json(serde_json::json!({
"status": profile.status, "status": profile.status,
"message": "Profile submitted for verification" "message": "Profile submitted for verification"

View file

@ -1,5 +1,6 @@
use std::path::Path; use std::path::Path;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {

View file

@ -7,21 +7,18 @@ use uuid::Uuid;
pub struct Application { pub struct Application {
pub id: Uuid, pub id: Uuid,
pub job_id: Uuid, pub job_id: Uuid,
pub job_seeker_id: Uuid, pub applicant_user_id: Uuid,
pub cover_letter: Option<String>, pub cover_note: Option<String>,
pub resume_url: Option<String>, pub status: String,
pub status: String, // APPLIED, SHORTLISTED, INTERVIEW, OFFERED, HIRED, REJECTED, WITHDRAWN
pub applied_at: DateTime<Utc>, pub applied_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>, pub updated_at: DateTime<Utc>,
pub contact_viewed: bool,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct CreateApplicationPayload { pub struct CreateApplicationPayload {
pub job_id: Uuid, pub job_id: Uuid,
pub job_seeker_id: Uuid, pub applicant_user_id: Uuid,
pub cover_letter: Option<String>, pub cover_note: Option<String>,
pub resume_url: Option<String>,
} }
pub struct ApplicationRepository; pub struct ApplicationRepository;
@ -33,15 +30,14 @@ impl ApplicationRepository {
) -> Result<Application, sqlx::Error> { ) -> Result<Application, sqlx::Error> {
let app = sqlx::query_as::<_, Application>( let app = sqlx::query_as::<_, Application>(
r#" r#"
INSERT INTO applications (job_id, job_seeker_id, cover_letter, resume_url) INSERT INTO job_applications (job_id, applicant_user_id, cover_note)
VALUES ($1, $2, $3, $4) VALUES ($1, $2, $3)
RETURNING * RETURNING *
"#, "#,
) )
.bind(payload.job_id) .bind(payload.job_id)
.bind(payload.job_seeker_id) .bind(payload.applicant_user_id)
.bind(payload.cover_letter) .bind(payload.cover_note)
.bind(payload.resume_url)
.fetch_one(pool) .fetch_one(pool)
.await?; .await?;
@ -49,7 +45,7 @@ impl ApplicationRepository {
} }
pub async fn get_by_id(pool: &PgPool, id: Uuid) -> Result<Option<Application>, sqlx::Error> { pub async fn get_by_id(pool: &PgPool, id: Uuid) -> Result<Option<Application>, sqlx::Error> {
sqlx::query_as::<_, Application>("SELECT * FROM applications WHERE id = $1") sqlx::query_as::<_, Application>("SELECT * FROM job_applications WHERE id = $1")
.bind(id) .bind(id)
.fetch_optional(pool) .fetch_optional(pool)
.await .await
@ -65,7 +61,7 @@ impl ApplicationRepository {
let offset = (page - 1) * limit; let offset = (page - 1) * limit;
let apps = sqlx::query_as::<_, Application>( let apps = sqlx::query_as::<_, Application>(
r#" r#"
SELECT * FROM applications SELECT * FROM job_applications
WHERE job_id = $1 AND ($2::VARCHAR IS NULL OR status = $2) WHERE job_id = $1 AND ($2::VARCHAR IS NULL OR status = $2)
ORDER BY applied_at DESC ORDER BY applied_at DESC
LIMIT $3 OFFSET $4 LIMIT $3 OFFSET $4
@ -81,22 +77,22 @@ impl ApplicationRepository {
Ok(apps) Ok(apps)
} }
pub async fn list_by_job_seeker_id( pub async fn list_by_user_id(
pool: &PgPool, pool: &PgPool,
job_seeker_id: Uuid, applicant_user_id: Uuid,
page: i64, page: i64,
limit: i64, limit: i64,
) -> Result<Vec<Application>, sqlx::Error> { ) -> Result<Vec<Application>, sqlx::Error> {
let offset = (page - 1) * limit; let offset = (page - 1) * limit;
let apps = sqlx::query_as::<_, Application>( let apps = sqlx::query_as::<_, Application>(
r#" r#"
SELECT * FROM applications SELECT * FROM job_applications
WHERE job_seeker_id = $1 WHERE applicant_user_id = $1
ORDER BY applied_at DESC ORDER BY applied_at DESC
LIMIT $2 OFFSET $3 LIMIT $2 OFFSET $3
"#, "#,
) )
.bind(job_seeker_id) .bind(applicant_user_id)
.bind(limit) .bind(limit)
.bind(offset) .bind(offset)
.fetch_all(pool) .fetch_all(pool)
@ -110,7 +106,7 @@ impl ApplicationRepository {
status: &str, status: &str,
) -> Result<Application, sqlx::Error> { ) -> Result<Application, sqlx::Error> {
let app = sqlx::query_as::<_, Application>( let app = sqlx::query_as::<_, Application>(
"UPDATE applications SET status = $1, updated_at = NOW() WHERE id = $2 RETURNING *", "UPDATE job_applications SET status = $1, updated_at = NOW() WHERE id = $2 RETURNING *",
) )
.bind(status) .bind(status)
.bind(id) .bind(id)
@ -118,14 +114,4 @@ impl ApplicationRepository {
.await?; .await?;
Ok(app) Ok(app)
} }
pub async fn mark_contact_viewed(pool: &PgPool, id: Uuid) -> Result<(), sqlx::Error> {
sqlx::query(
"UPDATE applications SET contact_viewed = true WHERE id = $1",
)
.bind(id)
.execute(pool)
.await?;
Ok(())
}
} }

View file

@ -34,6 +34,60 @@ pub struct UpsertCateringServiceProfilePayload {
pub struct CateringServiceRepository; pub struct CateringServiceRepository;
impl CateringServiceRepository { impl CateringServiceRepository {
pub async fn get_by_user_id(pool: &PgPool, user_id: Uuid) -> Result<Option<CateringServiceProfile>, sqlx::Error> {
sqlx::query_as::<_, CateringServiceProfile>(
r#"SELECT csp.id, csp.user_role_profile_id, csp.business_name, csp.cuisine_types, csp.event_types,
csp.min_guests, csp.max_guests, csp.has_setup_team, csp.has_serving_staff,
csp.price_per_head_inr, csp.created_at, csp.updated_at
FROM catering_service_profiles csp
INNER JOIN user_role_profiles urp ON urp.id = csp.user_role_profile_id
WHERE urp.user_id = $1 AND urp.role_key = 'catering_service'"#,
)
.bind(user_id)
.fetch_optional(pool)
.await
}
pub async fn upsert_by_user_id(pool: &PgPool, user_id: Uuid, p: UpsertCateringServiceProfilePayload) -> Result<CateringServiceProfile, sqlx::Error> {
let user_role_profile = sqlx::query_as::<_, (Uuid,)>(
r#"SELECT id FROM user_role_profiles WHERE user_id = $1 AND role_key = 'catering_service'"#,
)
.bind(user_id)
.fetch_optional(pool)
.await?
.ok_or(sqlx::Error::RowNotFound)?;
sqlx::query_as::<_, CateringServiceProfile>(
r#"INSERT INTO catering_service_profiles (user_role_profile_id, business_name, cuisine_types, event_types,
min_guests, max_guests, has_setup_team, has_serving_staff, price_per_head_inr)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
ON CONFLICT (user_role_profile_id) DO UPDATE SET
business_name = EXCLUDED.business_name,
cuisine_types = COALESCE(EXCLUDED.cuisine_types, catering_service_profiles.cuisine_types),
event_types = COALESCE(EXCLUDED.event_types, catering_service_profiles.event_types),
min_guests = EXCLUDED.min_guests,
max_guests = EXCLUDED.max_guests,
has_setup_team = EXCLUDED.has_setup_team,
has_serving_staff = EXCLUDED.has_serving_staff,
price_per_head_inr = EXCLUDED.price_per_head_inr,
updated_at = NOW()
RETURNING id, user_role_profile_id, business_name, cuisine_types, event_types,
min_guests, max_guests, has_setup_team, has_serving_staff,
price_per_head_inr, created_at, updated_at"#,
)
.bind(user_role_profile.0)
.bind(&p.business_name)
.bind(&p.cuisine_types)
.bind(&p.event_types)
.bind(p.min_guests)
.bind(p.max_guests)
.bind(p.has_setup_team)
.bind(p.has_serving_staff)
.bind(p.price_per_head_inr)
.fetch_one(pool)
.await
}
pub async fn get_by_user_role_id(pool: &PgPool, user_role_profile_id: Uuid) -> Result<Option<CateringServiceProfile>, sqlx::Error> { pub async fn get_by_user_role_id(pool: &PgPool, user_role_profile_id: Uuid) -> Result<Option<CateringServiceProfile>, sqlx::Error> {
sqlx::query_as::<_, CateringServiceProfile>( sqlx::query_as::<_, CateringServiceProfile>(
r#"SELECT id, user_role_profile_id, business_name, cuisine_types, event_types, r#"SELECT id, user_role_profile_id, business_name, cuisine_types, event_types,

View file

@ -28,6 +28,53 @@ pub struct UpsertDeveloperProfilePayload {
pub struct DeveloperRepository; pub struct DeveloperRepository;
impl DeveloperRepository { impl DeveloperRepository {
pub async fn get_by_user_id(pool: &PgPool, user_id: Uuid) -> Result<Option<DeveloperProfile>, sqlx::Error> {
sqlx::query_as::<_, DeveloperProfile>(
r#"SELECT dp.id, dp.user_role_profile_id, dp.tech_stack, dp.experience_years, dp.availability,
dp.hourly_rate_inr, dp.remote_ok,
dp.created_at, dp.updated_at
FROM developer_profiles dp
INNER JOIN user_role_profiles urp ON urp.id = dp.user_role_profile_id
WHERE urp.user_id = $1 AND urp.role_key = 'developer'"#,
)
.bind(user_id)
.fetch_optional(pool)
.await
}
pub async fn upsert_by_user_id(pool: &PgPool, user_id: Uuid, p: UpsertDeveloperProfilePayload) -> Result<DeveloperProfile, sqlx::Error> {
let user_role_profile = sqlx::query_as::<_, (Uuid,)>(
r#"SELECT id FROM user_role_profiles WHERE user_id = $1 AND role_key = 'developer'"#,
)
.bind(user_id)
.fetch_optional(pool)
.await?
.ok_or(sqlx::Error::RowNotFound)?;
sqlx::query_as::<_, DeveloperProfile>(
r#"INSERT INTO developer_profiles (user_role_profile_id, tech_stack, experience_years,
availability, hourly_rate_inr, remote_ok)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (user_role_profile_id) DO UPDATE SET
tech_stack = COALESCE(EXCLUDED.tech_stack, developer_profiles.tech_stack),
experience_years = EXCLUDED.experience_years,
availability = EXCLUDED.availability,
hourly_rate_inr = EXCLUDED.hourly_rate_inr,
remote_ok = EXCLUDED.remote_ok,
updated_at = NOW()
RETURNING id, user_role_profile_id, tech_stack, experience_years, availability,
hourly_rate_inr, remote_ok, created_at, updated_at"#,
)
.bind(user_role_profile.0)
.bind(&p.tech_stack)
.bind(p.experience_years)
.bind(&p.availability)
.bind(p.hourly_rate_inr)
.bind(p.remote_ok)
.fetch_one(pool)
.await
}
pub async fn get_by_user_role_id(pool: &PgPool, user_role_profile_id: Uuid) -> Result<Option<DeveloperProfile>, sqlx::Error> { pub async fn get_by_user_role_id(pool: &PgPool, user_role_profile_id: Uuid) -> Result<Option<DeveloperProfile>, sqlx::Error> {
sqlx::query_as::<_, DeveloperProfile>( sqlx::query_as::<_, DeveloperProfile>(
r#"SELECT id, user_role_profile_id, tech_stack, experience_years, availability, r#"SELECT id, user_role_profile_id, tech_stack, experience_years, availability,

View file

@ -30,6 +30,55 @@ pub struct UpsertFitnessTrainerProfilePayload {
pub struct FitnessTrainerRepository; pub struct FitnessTrainerRepository;
impl FitnessTrainerRepository { impl FitnessTrainerRepository {
pub async fn get_by_user_id(pool: &PgPool, user_id: Uuid) -> Result<Option<FitnessTrainerProfile>, sqlx::Error> {
sqlx::query_as::<_, FitnessTrainerProfile>(
r#"SELECT ftp.id, ftp.user_role_profile_id, ftp.disciplines, ftp.certifications, ftp.online_sessions,
ftp.home_visits, ftp.gym_based, ftp.per_session_rate_inr,
ftp.created_at, ftp.updated_at
FROM fitness_trainer_profiles ftp
INNER JOIN user_role_profiles urp ON urp.id = ftp.user_role_profile_id
WHERE urp.user_id = $1 AND urp.role_key = 'fitness_trainer'"#,
)
.bind(user_id)
.fetch_optional(pool)
.await
}
pub async fn upsert_by_user_id(pool: &PgPool, user_id: Uuid, p: UpsertFitnessTrainerProfilePayload) -> Result<FitnessTrainerProfile, sqlx::Error> {
let user_role_profile = sqlx::query_as::<_, (Uuid,)>(
r#"SELECT id FROM user_role_profiles WHERE user_id = $1 AND role_key = 'fitness_trainer'"#,
)
.bind(user_id)
.fetch_optional(pool)
.await?
.ok_or(sqlx::Error::RowNotFound)?;
sqlx::query_as::<_, FitnessTrainerProfile>(
r#"INSERT INTO fitness_trainer_profiles (user_role_profile_id, disciplines, certifications,
online_sessions, home_visits, gym_based, per_session_rate_inr)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (user_role_profile_id) DO UPDATE SET
disciplines = COALESCE(EXCLUDED.disciplines, fitness_trainer_profiles.disciplines),
certifications = COALESCE(EXCLUDED.certifications, fitness_trainer_profiles.certifications),
online_sessions = EXCLUDED.online_sessions,
home_visits = EXCLUDED.home_visits,
gym_based = EXCLUDED.gym_based,
per_session_rate_inr = EXCLUDED.per_session_rate_inr,
updated_at = NOW()
RETURNING id, user_role_profile_id, disciplines, certifications, online_sessions,
home_visits, gym_based, per_session_rate_inr, created_at, updated_at"#,
)
.bind(user_role_profile.0)
.bind(&p.disciplines)
.bind(&p.certifications)
.bind(p.online_sessions)
.bind(p.home_visits)
.bind(p.gym_based)
.bind(p.per_session_rate_inr)
.fetch_one(pool)
.await
}
pub async fn get_by_user_role_id(pool: &PgPool, user_role_profile_id: Uuid) -> Result<Option<FitnessTrainerProfile>, sqlx::Error> { pub async fn get_by_user_role_id(pool: &PgPool, user_role_profile_id: Uuid) -> Result<Option<FitnessTrainerProfile>, sqlx::Error> {
sqlx::query_as::<_, FitnessTrainerProfile>( sqlx::query_as::<_, FitnessTrainerProfile>(
r#"SELECT id, user_role_profile_id, disciplines, certifications, online_sessions, r#"SELECT id, user_role_profile_id, disciplines, certifications, online_sessions,

View file

@ -26,6 +26,51 @@ pub struct UpsertGraphicDesignerProfilePayload {
pub struct GraphicDesignerRepository; pub struct GraphicDesignerRepository;
impl GraphicDesignerRepository { impl GraphicDesignerRepository {
pub async fn get_by_user_id(pool: &PgPool, user_id: Uuid) -> Result<Option<GraphicDesignerProfile>, sqlx::Error> {
sqlx::query_as::<_, GraphicDesignerProfile>(
r#"SELECT gdp.id, gdp.user_role_profile_id, gdp.design_tools, gdp.style_tags,
gdp.brand_experience, gdp.starting_price_inr,
gdp.created_at, gdp.updated_at
FROM graphic_designer_profiles gdp
INNER JOIN user_role_profiles urp ON urp.id = gdp.user_role_profile_id
WHERE urp.user_id = $1 AND urp.role_key = 'graphic_designer'"#,
)
.bind(user_id)
.fetch_optional(pool)
.await
}
pub async fn upsert_by_user_id(pool: &PgPool, user_id: Uuid, p: UpsertGraphicDesignerProfilePayload) -> Result<GraphicDesignerProfile, sqlx::Error> {
let user_role_profile = sqlx::query_as::<_, (Uuid,)>(
r#"SELECT id FROM user_role_profiles WHERE user_id = $1 AND role_key = 'graphic_designer'"#,
)
.bind(user_id)
.fetch_optional(pool)
.await?
.ok_or(sqlx::Error::RowNotFound)?;
sqlx::query_as::<_, GraphicDesignerProfile>(
r#"INSERT INTO graphic_designer_profiles (user_role_profile_id, design_tools, style_tags,
brand_experience, starting_price_inr)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (user_role_profile_id) DO UPDATE SET
design_tools = COALESCE(EXCLUDED.design_tools, graphic_designer_profiles.design_tools),
style_tags = COALESCE(EXCLUDED.style_tags, graphic_designer_profiles.style_tags),
brand_experience = EXCLUDED.brand_experience,
starting_price_inr = EXCLUDED.starting_price_inr,
updated_at = NOW()
RETURNING id, user_role_profile_id, design_tools, style_tags, brand_experience,
starting_price_inr, created_at, updated_at"#,
)
.bind(user_role_profile.0)
.bind(&p.design_tools)
.bind(&p.style_tags)
.bind(p.brand_experience)
.bind(p.starting_price_inr)
.fetch_one(pool)
.await
}
pub async fn get_by_user_role_id(pool: &PgPool, user_role_profile_id: Uuid) -> Result<Option<GraphicDesignerProfile>, sqlx::Error> { pub async fn get_by_user_role_id(pool: &PgPool, user_role_profile_id: Uuid) -> Result<Option<GraphicDesignerProfile>, sqlx::Error> {
sqlx::query_as::<_, GraphicDesignerProfile>( sqlx::query_as::<_, GraphicDesignerProfile>(
r#"SELECT id, user_role_profile_id, design_tools, style_tags, brand_experience, r#"SELECT id, user_role_profile_id, design_tools, style_tags, brand_experience,

View file

@ -6,21 +6,19 @@ use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize, FromRow)] #[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct LeadRequest { pub struct LeadRequest {
pub id: Uuid, pub id: Uuid,
pub requirement_id: Uuid, pub user_role_profile_id: Uuid,
pub professional_id: Uuid, pub status: String,
pub status: String, // PENDING, ACCEPTED, REJECTED, EXPIRED, CANCELLED
pub tracecoins_reserved: i32, pub tracecoins_reserved: i32,
pub expires_at: DateTime<Utc>, pub expires_at: DateTime<Utc>,
pub requested_at: DateTime<Utc>, pub requested_at: DateTime<Utc>,
pub resolved_at: Option<DateTime<Utc>>, pub resolved_at: Option<DateTime<Utc>>,
pub professional_user_id: Option<Uuid>, pub remarks: Option<String>,
pub updated_at: DateTime<Utc>, pub updated_at: DateTime<Utc>,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct CreateLeadRequestPayload { pub struct CreateLeadRequestPayload {
pub requirement_id: Uuid, pub user_role_profile_id: Uuid,
pub professional_id: Uuid,
pub expires_at: DateTime<Utc>, pub expires_at: DateTime<Utc>,
} }
@ -33,13 +31,12 @@ impl LeadRequestRepository {
) -> Result<LeadRequest, sqlx::Error> { ) -> Result<LeadRequest, sqlx::Error> {
let req = sqlx::query_as::<_, LeadRequest>( let req = sqlx::query_as::<_, LeadRequest>(
r#" r#"
INSERT INTO lead_requests (requirement_id, professional_id, expires_at) INSERT INTO lead_requests (user_role_profile_id, expires_at)
VALUES ($1, $2, $3) VALUES ($1, $2)
RETURNING * RETURNING *
"#, "#,
) )
.bind(payload.requirement_id) .bind(payload.user_role_profile_id)
.bind(payload.professional_id)
.bind(payload.expires_at) .bind(payload.expires_at)
.fetch_one(pool) .fetch_one(pool)
.await?; .await?;
@ -54,9 +51,9 @@ impl LeadRequestRepository {
.await .await
} }
pub async fn list_by_requirement_id( pub async fn list_by_user_role_profile_id(
pool: &PgPool, pool: &PgPool,
requirement_id: Uuid, user_role_profile_id: Uuid,
page: i64, page: i64,
limit: i64, limit: i64,
) -> Result<Vec<LeadRequest>, sqlx::Error> { ) -> Result<Vec<LeadRequest>, sqlx::Error> {
@ -64,12 +61,12 @@ impl LeadRequestRepository {
let reqs = sqlx::query_as::<_, LeadRequest>( let reqs = sqlx::query_as::<_, LeadRequest>(
r#" r#"
SELECT * FROM lead_requests SELECT * FROM lead_requests
WHERE requirement_id = $1 WHERE user_role_profile_id = $1
ORDER BY requested_at DESC ORDER BY requested_at DESC
LIMIT $2 OFFSET $3 LIMIT $2 OFFSET $3
"#, "#,
) )
.bind(requirement_id) .bind(user_role_profile_id)
.bind(limit) .bind(limit)
.bind(offset) .bind(offset)
.fetch_all(pool) .fetch_all(pool)

View file

@ -28,6 +28,54 @@ pub struct UpsertMakeupArtistProfilePayload {
pub struct MakeupArtistRepository; pub struct MakeupArtistRepository;
impl MakeupArtistRepository { impl MakeupArtistRepository {
pub async fn get_by_user_id(pool: &PgPool, user_id: Uuid) -> Result<Option<MakeupArtistProfile>, sqlx::Error> {
sqlx::query_as::<_, MakeupArtistProfile>(
r#"SELECT map.id, map.user_role_profile_id, map.specializations, map.kit_brands,
map.home_service, map.studio_available, map.starting_price_inr,
map.created_at, map.updated_at
FROM makeup_artist_profiles map
INNER JOIN user_role_profiles urp ON urp.id = map.user_role_profile_id
WHERE urp.user_id = $1 AND urp.role_key = 'makeup_artist'"#,
)
.bind(user_id)
.fetch_optional(pool)
.await
}
pub async fn upsert_by_user_id(pool: &PgPool, user_id: Uuid, p: UpsertMakeupArtistProfilePayload) -> Result<MakeupArtistProfile, sqlx::Error> {
let user_role_profile = sqlx::query_as::<_, (Uuid,)>(
r#"SELECT id FROM user_role_profiles WHERE user_id = $1 AND role_key = 'makeup_artist'"#,
)
.bind(user_id)
.fetch_optional(pool)
.await?
.ok_or(sqlx::Error::RowNotFound)?;
sqlx::query_as::<_, MakeupArtistProfile>(
r#"INSERT INTO makeup_artist_profiles (user_role_profile_id, specializations, kit_brands,
home_service, studio_available, starting_price_inr)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (user_role_profile_id) DO UPDATE SET
specializations = COALESCE(EXCLUDED.specializations, makeup_artist_profiles.specializations),
kit_brands = COALESCE(EXCLUDED.kit_brands, makeup_artist_profiles.kit_brands),
home_service = EXCLUDED.home_service,
studio_available = EXCLUDED.studio_available,
starting_price_inr = EXCLUDED.starting_price_inr,
updated_at = NOW()
RETURNING id, user_role_profile_id, specializations, kit_brands,
home_service, studio_available, starting_price_inr,
created_at, updated_at"#,
)
.bind(user_role_profile.0)
.bind(&p.specializations)
.bind(&p.kit_brands)
.bind(p.home_service)
.bind(p.studio_available)
.bind(p.starting_price_inr)
.fetch_one(pool)
.await
}
pub async fn get_by_user_role_id(pool: &PgPool, user_role_profile_id: Uuid) -> Result<Option<MakeupArtistProfile>, sqlx::Error> { pub async fn get_by_user_role_id(pool: &PgPool, user_role_profile_id: Uuid) -> Result<Option<MakeupArtistProfile>, sqlx::Error> {
sqlx::query_as::<_, MakeupArtistProfile>( sqlx::query_as::<_, MakeupArtistProfile>(
r#"SELECT id, user_role_profile_id, specializations, kit_brands, r#"SELECT id, user_role_profile_id, specializations, kit_brands,

View file

@ -26,4 +26,5 @@ pub mod department;
pub mod designation; pub mod designation;
pub mod verification; pub mod verification;
pub mod user_role_profile; pub mod user_role_profile;
pub mod tracecoin_wallet;

View file

@ -30,6 +30,57 @@ pub struct UpsertPhotographerProfilePayload {
pub struct PhotographerRepository; pub struct PhotographerRepository;
impl PhotographerRepository { impl PhotographerRepository {
pub async fn get_by_user_id(pool: &PgPool, user_id: Uuid) -> Result<Option<PhotographerProfile>, sqlx::Error> {
sqlx::query_as::<_, PhotographerProfile>(
r#"SELECT pp.id, pp.user_role_profile_id, pp.specialties, pp.camera_brands,
pp.studio_available, pp.outdoor_shoots, pp.travel_radius_km,
pp.starting_price_inr,
pp.created_at, pp.updated_at
FROM photographer_profiles pp
INNER JOIN user_role_profiles urp ON urp.id = pp.user_role_profile_id
WHERE urp.user_id = $1 AND urp.role_key = 'photographer'"#,
)
.bind(user_id)
.fetch_optional(pool)
.await
}
pub async fn upsert_by_user_id(pool: &PgPool, user_id: Uuid, p: UpsertPhotographerProfilePayload) -> Result<PhotographerProfile, sqlx::Error> {
let user_role_profile = sqlx::query_as::<_, (Uuid,)>(
r#"SELECT id FROM user_role_profiles WHERE user_id = $1 AND role_key = 'photographer'"#,
)
.bind(user_id)
.fetch_optional(pool)
.await?
.ok_or(sqlx::Error::RowNotFound)?;
sqlx::query_as::<_, PhotographerProfile>(
r#"INSERT INTO photographer_profiles (user_role_profile_id, specialties, camera_brands,
studio_available, outdoor_shoots, travel_radius_km, starting_price_inr)
VALUES ($1, $2, $3, $4, $5, $6, $7)
ON CONFLICT (user_role_profile_id) DO UPDATE SET
specialties = COALESCE(EXCLUDED.specialties, photographer_profiles.specialties),
camera_brands = COALESCE(EXCLUDED.camera_brands, photographer_profiles.camera_brands),
studio_available = EXCLUDED.studio_available,
outdoor_shoots = EXCLUDED.outdoor_shoots,
travel_radius_km = EXCLUDED.travel_radius_km,
starting_price_inr = EXCLUDED.starting_price_inr,
updated_at = NOW()
RETURNING id, user_role_profile_id, specialties, camera_brands,
studio_available, outdoor_shoots, travel_radius_km,
starting_price_inr, created_at, updated_at"#,
)
.bind(user_role_profile.0)
.bind(&p.specialties)
.bind(&p.camera_brands)
.bind(p.studio_available)
.bind(p.outdoor_shoots)
.bind(p.travel_radius_km)
.bind(p.starting_price_inr)
.fetch_one(pool)
.await
}
pub async fn get_by_user_role_id(pool: &PgPool, user_role_profile_id: Uuid) -> Result<Option<PhotographerProfile>, sqlx::Error> { pub async fn get_by_user_role_id(pool: &PgPool, user_role_profile_id: Uuid) -> Result<Option<PhotographerProfile>, sqlx::Error> {
sqlx::query_as::<_, PhotographerProfile>( sqlx::query_as::<_, PhotographerProfile>(
r#"SELECT id, user_role_profile_id, specialties, camera_brands, r#"SELECT id, user_role_profile_id, specialties, camera_brands,

View file

@ -7,11 +7,7 @@ use uuid::Uuid;
pub struct Professional { pub struct Professional {
pub id: Uuid, pub id: Uuid,
pub user_id: Uuid, pub user_id: Uuid,
pub profession_key: String, pub role_key: String,
pub display_name: String,
pub location: Option<String>,
pub bio: Option<String>,
pub extra_data_json: Option<serde_json::Value>,
pub status: String, pub status: String,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>, pub updated_at: DateTime<Utc>,
@ -22,20 +18,19 @@ use super::requirement::Requirement;
#[derive(Debug, Serialize, Deserialize, FromRow)] #[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct PortfolioItem { pub struct PortfolioItem {
pub id: Uuid, pub id: Uuid,
pub professional_id: Uuid, pub user_role_profile_id: Uuid,
pub title: String, pub title: String,
pub description: Option<String>, pub description: Option<String>,
pub tags: Option<Vec<String>>, pub tags: Option<Vec<String>>,
pub display_order: Option<i32>,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>, pub updated_at: DateTime<Utc>,
pub user_id: Option<Uuid>,
pub profession_key: Option<String>,
} }
#[derive(Debug, Serialize, Deserialize, FromRow)] #[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct Service { pub struct Service {
pub id: Uuid, pub id: Uuid,
pub professional_id: Uuid, pub user_role_profile_id: Uuid,
pub name: String, pub name: String,
pub description: Option<String>, pub description: Option<String>,
pub price: i32, pub price: i32,
@ -43,8 +38,6 @@ pub struct Service {
pub is_active: bool, pub is_active: bool,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>, pub updated_at: DateTime<Utc>,
pub user_id: Option<Uuid>,
pub profession_key: Option<String>,
} }
#[derive(Debug, Serialize, Deserialize, FromRow)] #[derive(Debug, Serialize, Deserialize, FromRow)]
@ -117,7 +110,7 @@ pub struct ProfessionalRepository;
impl ProfessionalRepository { impl ProfessionalRepository {
pub async fn get_by_user_id(pool: &PgPool, user_id: Uuid) -> Result<Professional, sqlx::Error> { pub async fn get_by_user_id(pool: &PgPool, user_id: Uuid) -> Result<Professional, sqlx::Error> {
sqlx::query_as::<_, Professional>( sqlx::query_as::<_, Professional>(
"SELECT * FROM professionals WHERE user_id = $1", "SELECT id, user_id, role_key as profession_key, status, created_at, updated_at FROM user_role_profiles WHERE user_id = $1 AND role_key != 'CUSTOMER' AND role_key != 'COMPANY'",
) )
.bind(user_id) .bind(user_id)
.fetch_one(pool) .fetch_one(pool)
@ -133,7 +126,7 @@ impl ProfessionalRepository {
let offset = (page - 1) * limit; let offset = (page - 1) * limit;
sqlx::query_as::<_, Requirement>( sqlx::query_as::<_, Requirement>(
r#" r#"
SELECT * FROM requirements SELECT * FROM leads
WHERE profession_key = $1 AND status = 'OPEN' AND (expires_at IS NULL OR expires_at > NOW()) WHERE profession_key = $1 AND status = 'OPEN' AND (expires_at IS NULL OR expires_at > NOW())
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT $2 OFFSET $3 LIMIT $2 OFFSET $3
@ -146,20 +139,20 @@ impl ProfessionalRepository {
.await .await
} }
pub async fn get_portfolio(pool: &PgPool, professional_id: Uuid) -> Result<Vec<PortfolioItem>, sqlx::Error> { pub async fn get_portfolio(pool: &PgPool, user_role_profile_id: Uuid) -> Result<Vec<PortfolioItem>, sqlx::Error> {
sqlx::query_as::<_, PortfolioItem>( sqlx::query_as::<_, PortfolioItem>(
"SELECT * FROM portfolio_items WHERE professional_id = $1 ORDER BY created_at DESC", "SELECT * FROM portfolio_items WHERE user_role_profile_id = $1 ORDER BY display_order, created_at DESC",
) )
.bind(professional_id) .bind(user_role_profile_id)
.fetch_all(pool) .fetch_all(pool)
.await .await
} }
pub async fn get_services(pool: &PgPool, professional_id: Uuid) -> Result<Vec<Service>, sqlx::Error> { pub async fn get_services(pool: &PgPool, user_role_profile_id: Uuid) -> Result<Vec<Service>, sqlx::Error> {
sqlx::query_as::<_, Service>( sqlx::query_as::<_, Service>(
"SELECT * FROM services WHERE professional_id = $1 AND is_active = true ORDER BY name ASC", "SELECT * FROM services WHERE user_role_profile_id = $1 AND is_active = true ORDER BY name ASC",
) )
.bind(professional_id) .bind(user_role_profile_id)
.fetch_all(pool) .fetch_all(pool)
.await .await
} }
@ -187,14 +180,14 @@ impl ProfessionalRepository {
Ok(()) Ok(())
} }
pub async fn get_user_id_by_professional_id( pub async fn get_user_id_by_user_role_profile_id(
pool: &PgPool, pool: &PgPool,
professional_id: Uuid, user_role_profile_id: Uuid,
) -> Result<Option<Uuid>, sqlx::Error> { ) -> Result<Option<Uuid>, sqlx::Error> {
let row = sqlx::query_scalar::<_, Uuid>( let row = sqlx::query_scalar::<_, Uuid>(
"SELECT user_id FROM professionals WHERE id = $1", "SELECT user_id FROM user_role_profiles WHERE id = $1",
) )
.bind(professional_id) .bind(user_role_profile_id)
.fetch_optional(pool) .fetch_optional(pool)
.await?; .await?;
Ok(row) Ok(row)
@ -202,17 +195,17 @@ impl ProfessionalRepository {
pub async fn create_portfolio_item( pub async fn create_portfolio_item(
pool: &PgPool, pool: &PgPool,
professional_id: Uuid, user_role_profile_id: Uuid,
payload: CreatePortfolioItemPayload, payload: CreatePortfolioItemPayload,
) -> Result<PortfolioItem, sqlx::Error> { ) -> Result<PortfolioItem, sqlx::Error> {
sqlx::query_as::<_, PortfolioItem>( sqlx::query_as::<_, PortfolioItem>(
r#" r#"
INSERT INTO portfolio_items (professional_id, title, description, tags) INSERT INTO portfolio_items (user_role_profile_id, title, description, tags)
VALUES ($1, $2, $3, COALESCE($4::text[], '{}')) VALUES ($1, $2, $3, COALESCE($4::text[], '{}'))
RETURNING * RETURNING *
"#, "#,
) )
.bind(professional_id) .bind(user_role_profile_id)
.bind(payload.title) .bind(payload.title)
.bind(payload.description) .bind(payload.description)
.bind(payload.tags) .bind(payload.tags)
@ -222,7 +215,7 @@ impl ProfessionalRepository {
pub async fn update_portfolio_item( pub async fn update_portfolio_item(
pool: &PgPool, pool: &PgPool,
professional_id: Uuid, user_role_profile_id: Uuid,
id: Uuid, id: Uuid,
payload: UpdatePortfolioItemPayload, payload: UpdatePortfolioItemPayload,
) -> Result<Option<PortfolioItem>, sqlx::Error> { ) -> Result<Option<PortfolioItem>, sqlx::Error> {
@ -234,7 +227,7 @@ impl ProfessionalRepository {
description = COALESCE($2, description), description = COALESCE($2, description),
tags = COALESCE($3, tags), tags = COALESCE($3, tags),
updated_at = NOW() updated_at = NOW()
WHERE id = $4 AND professional_id = $5 WHERE id = $4 AND user_role_profile_id = $5
RETURNING * RETURNING *
"#, "#,
) )
@ -242,7 +235,7 @@ impl ProfessionalRepository {
.bind(payload.description) .bind(payload.description)
.bind(payload.tags) .bind(payload.tags)
.bind(id) .bind(id)
.bind(professional_id) .bind(user_role_profile_id)
.fetch_optional(pool) .fetch_optional(pool)
.await?; .await?;
Ok(row) Ok(row)
@ -250,14 +243,14 @@ impl ProfessionalRepository {
pub async fn delete_portfolio_item( pub async fn delete_portfolio_item(
pool: &PgPool, pool: &PgPool,
professional_id: Uuid, user_role_profile_id: Uuid,
id: Uuid, id: Uuid,
) -> Result<bool, sqlx::Error> { ) -> Result<bool, sqlx::Error> {
let result = sqlx::query( let result = sqlx::query(
"DELETE FROM portfolio_items WHERE id = $1 AND professional_id = $2", "DELETE FROM portfolio_items WHERE id = $1 AND user_role_profile_id = $2",
) )
.bind(id) .bind(id)
.bind(professional_id) .bind(user_role_profile_id)
.execute(pool) .execute(pool)
.await?; .await?;
Ok(result.rows_affected() > 0) Ok(result.rows_affected() > 0)
@ -265,17 +258,17 @@ impl ProfessionalRepository {
pub async fn create_service( pub async fn create_service(
pool: &PgPool, pool: &PgPool,
professional_id: Uuid, user_role_profile_id: Uuid,
payload: CreateServicePayload, payload: CreateServicePayload,
) -> Result<Service, sqlx::Error> { ) -> Result<Service, sqlx::Error> {
sqlx::query_as::<_, Service>( sqlx::query_as::<_, Service>(
r#" r#"
INSERT INTO services (professional_id, name, description, price, duration_minutes) INSERT INTO services (user_role_profile_id, name, description, price, duration_minutes)
VALUES ($1, $2, $3, $4, $5) VALUES ($1, $2, $3, $4, $5)
RETURNING * RETURNING *
"#, "#,
) )
.bind(professional_id) .bind(user_role_profile_id)
.bind(payload.name) .bind(payload.name)
.bind(payload.description) .bind(payload.description)
.bind(payload.price) .bind(payload.price)
@ -286,7 +279,7 @@ impl ProfessionalRepository {
pub async fn update_service( pub async fn update_service(
pool: &PgPool, pool: &PgPool,
professional_id: Uuid, user_role_profile_id: Uuid,
id: Uuid, id: Uuid,
payload: UpdateServicePayload, payload: UpdateServicePayload,
) -> Result<Option<Service>, sqlx::Error> { ) -> Result<Option<Service>, sqlx::Error> {
@ -300,7 +293,7 @@ impl ProfessionalRepository {
duration_minutes = COALESCE($4, duration_minutes), duration_minutes = COALESCE($4, duration_minutes),
is_active = COALESCE($5, is_active), is_active = COALESCE($5, is_active),
updated_at = NOW() updated_at = NOW()
WHERE id = $6 AND professional_id = $7 WHERE id = $6 AND user_role_profile_id = $7
RETURNING * RETURNING *
"#, "#,
) )
@ -310,7 +303,7 @@ impl ProfessionalRepository {
.bind(payload.duration_minutes) .bind(payload.duration_minutes)
.bind(payload.is_active) .bind(payload.is_active)
.bind(id) .bind(id)
.bind(professional_id) .bind(user_role_profile_id)
.fetch_optional(pool) .fetch_optional(pool)
.await?; .await?;
Ok(row) Ok(row)
@ -318,12 +311,12 @@ impl ProfessionalRepository {
pub async fn delete_service( pub async fn delete_service(
pool: &PgPool, pool: &PgPool,
professional_id: Uuid, user_role_profile_id: Uuid,
id: Uuid, id: Uuid,
) -> Result<bool, sqlx::Error> { ) -> Result<bool, sqlx::Error> {
let result = sqlx::query("DELETE FROM services WHERE id = $1 AND professional_id = $2") let result = sqlx::query("DELETE FROM services WHERE id = $1 AND user_role_profile_id = $2")
.bind(id) .bind(id)
.bind(professional_id) .bind(user_role_profile_id)
.execute(pool) .execute(pool)
.await?; .await?;
Ok(result.rows_affected() > 0) Ok(result.rows_affected() > 0)
@ -560,11 +553,13 @@ impl ProfessionalRepository {
pub async fn submit_for_verification( pub async fn submit_for_verification(
pool: &PgPool, pool: &PgPool,
user_id: Uuid, user_id: Uuid,
profession_key: &str,
) -> Result<Professional, sqlx::Error> { ) -> Result<Professional, sqlx::Error> {
let prof = sqlx::query_as::<_, Professional>( let prof = sqlx::query_as::<_, Professional>(
"SELECT * FROM professionals WHERE user_id = $1", "SELECT id, user_id, role_key as profession_key, status, created_at, updated_at FROM user_role_profiles WHERE user_id = $1 AND role_key = $2",
) )
.bind(user_id) .bind(user_id)
.bind(profession_key)
.fetch_one(pool) .fetch_one(pool)
.await?; .await?;
@ -574,13 +569,14 @@ impl ProfessionalRepository {
let prof = sqlx::query_as::<_, Professional>( let prof = sqlx::query_as::<_, Professional>(
r#" r#"
UPDATE professionals UPDATE user_role_profiles
SET status = 'PENDING_REVIEW', updated_at = NOW() SET status = 'PENDING_REVIEW', updated_at = NOW()
WHERE user_id = $1 WHERE user_id = $1 AND role_key = $2
RETURNING * RETURNING id, user_id, role_key as profession_key, status, created_at, updated_at
"#, "#,
) )
.bind(user_id) .bind(user_id)
.bind(profession_key)
.fetch_one(pool) .fetch_one(pool)
.await?; .await?;

View file

@ -6,7 +6,6 @@ use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize, FromRow)] #[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct Requirement { pub struct Requirement {
pub id: Uuid, pub id: Uuid,
pub customer_id: Uuid,
pub profession_key: String, pub profession_key: String,
pub title: String, pub title: String,
pub description: String, pub description: String,
@ -14,7 +13,7 @@ pub struct Requirement {
pub budget: Option<i32>, pub budget: Option<i32>,
pub preferred_date: Option<chrono::NaiveDate>, pub preferred_date: Option<chrono::NaiveDate>,
pub extra_data_json: Option<serde_json::Value>, pub extra_data_json: Option<serde_json::Value>,
pub status: String, // DRAFT, PENDING_APPROVAL, OPEN, CLOSED, EXPIRED, REJECTED pub status: String,
pub rejection_reason: Option<String>, pub rejection_reason: Option<String>,
pub request_count: i32, pub request_count: i32,
pub accepted_count: i32, pub accepted_count: i32,
@ -22,12 +21,13 @@ pub struct Requirement {
pub approved_at: Option<DateTime<Utc>>, pub approved_at: Option<DateTime<Utc>>,
pub approved_by: Option<Uuid>, pub approved_by: Option<Uuid>,
pub created_at: DateTime<Utc>, pub created_at: DateTime<Utc>,
pub created_by_user_id: Option<Uuid>,
pub required_date: Option<chrono::NaiveDate>,
pub updated_at: DateTime<Utc>, pub updated_at: DateTime<Utc>,
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct CreateRequirementPayload { pub struct CreateRequirementPayload {
pub customer_id: Uuid,
pub profession_key: String, pub profession_key: String,
pub title: String, pub title: String,
pub description: String, pub description: String,
@ -56,15 +56,14 @@ impl RequirementRepository {
) -> Result<Requirement, sqlx::Error> { ) -> Result<Requirement, sqlx::Error> {
let req = sqlx::query_as::<_, Requirement>( let req = sqlx::query_as::<_, Requirement>(
r#" r#"
INSERT INTO requirements ( INSERT INTO leads (
customer_id, profession_key, title, description, location, profession_key, title, description, location,
budget, preferred_date, extra_data_json budget, preferred_date, extra_data_json
) )
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING * RETURNING *
"#, "#,
) )
.bind(payload.customer_id)
.bind(payload.profession_key) .bind(payload.profession_key)
.bind(payload.title) .bind(payload.title)
.bind(payload.description) .bind(payload.description)
@ -79,28 +78,28 @@ impl RequirementRepository {
} }
pub async fn get_by_id(pool: &PgPool, id: Uuid) -> Result<Option<Requirement>, sqlx::Error> { pub async fn get_by_id(pool: &PgPool, id: Uuid) -> Result<Option<Requirement>, sqlx::Error> {
sqlx::query_as::<_, Requirement>("SELECT * FROM requirements WHERE id = $1") sqlx::query_as::<_, Requirement>("SELECT * FROM leads WHERE id = $1")
.bind(id) .bind(id)
.fetch_optional(pool) .fetch_optional(pool)
.await .await
} }
pub async fn list_by_customer_id( pub async fn list_by_user_id(
pool: &PgPool, pool: &PgPool,
customer_id: Uuid, user_id: Uuid,
page: i64, page: i64,
limit: i64, limit: i64,
) -> Result<Vec<Requirement>, sqlx::Error> { ) -> Result<Vec<Requirement>, sqlx::Error> {
let offset = (page - 1) * limit; let offset = (page - 1) * limit;
let reqs = sqlx::query_as::<_, Requirement>( let reqs = sqlx::query_as::<_, Requirement>(
r#" r#"
SELECT * FROM requirements SELECT * FROM leads
WHERE customer_id = $1 WHERE created_by_user_id = $1
ORDER BY created_at DESC ORDER BY created_at DESC
LIMIT $2 OFFSET $3 LIMIT $2 OFFSET $3
"#, "#,
) )
.bind(customer_id) .bind(user_id)
.bind(limit) .bind(limit)
.bind(offset) .bind(offset)
.fetch_all(pool) .fetch_all(pool)
@ -116,7 +115,7 @@ impl RequirementRepository {
) -> Result<Requirement, sqlx::Error> { ) -> Result<Requirement, sqlx::Error> {
let req = sqlx::query_as::<_, Requirement>( let req = sqlx::query_as::<_, Requirement>(
r#" r#"
UPDATE requirements SET UPDATE leads SET
title = COALESCE($1, title), title = COALESCE($1, title),
description = COALESCE($2, description), description = COALESCE($2, description),
location = COALESCE($3, location), location = COALESCE($3, location),
@ -147,7 +146,7 @@ impl RequirementRepository {
status: &str, status: &str,
) -> Result<Requirement, sqlx::Error> { ) -> Result<Requirement, sqlx::Error> {
let req = sqlx::query_as::<_, Requirement>( let req = sqlx::query_as::<_, Requirement>(
"UPDATE requirements SET status = $1, updated_at = NOW() WHERE id = $2 RETURNING *", "UPDATE leads SET status = $1, updated_at = NOW() WHERE id = $2 RETURNING *",
) )
.bind(status) .bind(status)
.bind(id) .bind(id)
@ -157,7 +156,7 @@ impl RequirementRepository {
} }
pub async fn increment_request_count(pool: &PgPool, id: Uuid) -> Result<(), sqlx::Error> { pub async fn increment_request_count(pool: &PgPool, id: Uuid) -> Result<(), sqlx::Error> {
sqlx::query("UPDATE requirements SET request_count = request_count + 1 WHERE id = $1") sqlx::query("UPDATE leads SET request_count = request_count + 1 WHERE id = $1")
.bind(id) .bind(id)
.execute(pool) .execute(pool)
.await?; .await?;
@ -166,7 +165,7 @@ impl RequirementRepository {
pub async fn increment_accepted_count(pool: &PgPool, id: Uuid) -> Result<(), sqlx::Error> { pub async fn increment_accepted_count(pool: &PgPool, id: Uuid) -> Result<(), sqlx::Error> {
sqlx::query( sqlx::query(
"UPDATE requirements SET accepted_count = accepted_count + 1 WHERE id = $1", "UPDATE leads SET accepted_count = accepted_count + 1 WHERE id = $1",
) )
.bind(id) .bind(id)
.execute(pool) .execute(pool)
@ -180,7 +179,7 @@ impl RequirementRepository {
) -> Result<Requirement, sqlx::Error> { ) -> Result<Requirement, sqlx::Error> {
sqlx::query_as::<_, Requirement>( sqlx::query_as::<_, Requirement>(
r#" r#"
UPDATE requirements UPDATE leads
SET accepted_count = accepted_count + 1, updated_at = NOW() SET accepted_count = accepted_count + 1, updated_at = NOW()
WHERE id = $1 WHERE id = $1
RETURNING * RETURNING *
@ -198,7 +197,7 @@ impl RequirementRepository {
) -> Result<Requirement, sqlx::Error> { ) -> Result<Requirement, sqlx::Error> {
sqlx::query_as::<_, Requirement>( sqlx::query_as::<_, Requirement>(
r#" r#"
UPDATE requirements UPDATE leads
SET status = 'OPEN', approved_at = NOW(), approved_by = $1, rejection_reason = NULL, updated_at = NOW() SET status = 'OPEN', approved_at = NOW(), approved_by = $1, rejection_reason = NULL, updated_at = NOW()
WHERE id = $2 WHERE id = $2
RETURNING * RETURNING *
@ -217,7 +216,7 @@ impl RequirementRepository {
) -> Result<Requirement, sqlx::Error> { ) -> Result<Requirement, sqlx::Error> {
sqlx::query_as::<_, Requirement>( sqlx::query_as::<_, Requirement>(
r#" r#"
UPDATE requirements UPDATE leads
SET status = 'REJECTED', rejection_reason = $1, approved_at = NULL, approved_by = NULL, updated_at = NOW() SET status = 'REJECTED', rejection_reason = $1, approved_at = NULL, approved_by = NULL, updated_at = NOW()
WHERE id = $2 WHERE id = $2
RETURNING * RETURNING *

View file

@ -28,6 +28,53 @@ pub struct UpsertSocialMediaManagerProfilePayload {
pub struct SocialMediaManagerRepository; pub struct SocialMediaManagerRepository;
impl SocialMediaManagerRepository { impl SocialMediaManagerRepository {
pub async fn get_by_user_id(pool: &PgPool, user_id: Uuid) -> Result<Option<SocialMediaManagerProfile>, sqlx::Error> {
sqlx::query_as::<_, SocialMediaManagerProfile>(
r#"SELECT smmp.id, smmp.user_role_profile_id, smmp.platforms, smmp.industries, smmp.content_types,
smmp.avg_follower_growth_pct, smmp.starting_price_inr,
smmp.created_at, smmp.updated_at
FROM social_media_manager_profiles smmp
INNER JOIN user_role_profiles urp ON urp.id = smmp.user_role_profile_id
WHERE urp.user_id = $1 AND urp.role_key = 'social_media_manager'"#,
)
.bind(user_id)
.fetch_optional(pool)
.await
}
pub async fn upsert_by_user_id(pool: &PgPool, user_id: Uuid, p: UpsertSocialMediaManagerProfilePayload) -> Result<SocialMediaManagerProfile, sqlx::Error> {
let user_role_profile = sqlx::query_as::<_, (Uuid,)>(
r#"SELECT id FROM user_role_profiles WHERE user_id = $1 AND role_key = 'social_media_manager'"#,
)
.bind(user_id)
.fetch_optional(pool)
.await?
.ok_or(sqlx::Error::RowNotFound)?;
sqlx::query_as::<_, SocialMediaManagerProfile>(
r#"INSERT INTO social_media_manager_profiles (user_role_profile_id, platforms, industries,
content_types, avg_follower_growth_pct, starting_price_inr)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (user_role_profile_id) DO UPDATE SET
platforms = COALESCE(EXCLUDED.platforms, social_media_manager_profiles.platforms),
industries = COALESCE(EXCLUDED.industries, social_media_manager_profiles.industries),
content_types = COALESCE(EXCLUDED.content_types, social_media_manager_profiles.content_types),
avg_follower_growth_pct = EXCLUDED.avg_follower_growth_pct,
starting_price_inr = EXCLUDED.starting_price_inr,
updated_at = NOW()
RETURNING id, user_role_profile_id, platforms, industries, content_types,
avg_follower_growth_pct, starting_price_inr, created_at, updated_at"#,
)
.bind(user_role_profile.0)
.bind(&p.platforms)
.bind(&p.industries)
.bind(&p.content_types)
.bind(p.avg_follower_growth_pct)
.bind(p.starting_price_inr)
.fetch_one(pool)
.await
}
pub async fn get_by_user_role_id(pool: &PgPool, user_role_profile_id: Uuid) -> Result<Option<SocialMediaManagerProfile>, sqlx::Error> { pub async fn get_by_user_role_id(pool: &PgPool, user_role_profile_id: Uuid) -> Result<Option<SocialMediaManagerProfile>, sqlx::Error> {
sqlx::query_as::<_, SocialMediaManagerProfile>( sqlx::query_as::<_, SocialMediaManagerProfile>(
r#"SELECT id, user_role_profile_id, platforms, industries, content_types, r#"SELECT id, user_role_profile_id, platforms, industries, content_types,

View file

@ -0,0 +1,220 @@
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, PgPool};
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct Wallet {
pub id: Uuid,
pub user_id: Uuid,
pub current_balance: i32,
pub reserved: i32,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct LedgerEntry {
pub id: Uuid,
pub wallet_id: Uuid,
pub transaction_type: String,
pub amount: i32,
pub reference_type: String,
pub reference_id: Option<Uuid>,
pub balance_after: Option<i32>,
pub remarks: Option<String>,
pub created_at: DateTime<Utc>,
}
pub struct TracecoinWalletRepository;
impl TracecoinWalletRepository {
pub async fn get_by_user_id(pool: &PgPool, user_id: Uuid) -> Result<Wallet, sqlx::Error> {
sqlx::query_as::<_, Wallet>(
"SELECT * FROM tracecoin_wallets WHERE user_id = $1",
)
.bind(user_id)
.fetch_one(pool)
.await
}
pub async fn ensure_wallet(pool: &PgPool, user_id: Uuid) -> Result<(), sqlx::Error> {
sqlx::query(
r#"
INSERT INTO tracecoin_wallets (user_id, current_balance, reserved)
VALUES ($1, 0, 0)
ON CONFLICT (user_id) DO NOTHING
"#,
)
.bind(user_id)
.execute(pool)
.await?;
Ok(())
}
pub async fn try_reserve_tracecoins(
pool: &PgPool,
user_id: Uuid,
amount: i32,
reference_id: Uuid,
) -> Result<bool, sqlx::Error> {
let mut tx = pool.begin().await?;
sqlx::query(
r#"
INSERT INTO tracecoin_wallets (user_id, current_balance, reserved)
VALUES ($1, 0, 0)
ON CONFLICT (user_id) DO NOTHING
"#,
)
.bind(user_id)
.execute(&mut *tx)
.await?;
let wallet = sqlx::query_as::<_, Wallet>(
"SELECT * FROM tracecoin_wallets WHERE user_id = $1 FOR UPDATE",
)
.bind(user_id)
.fetch_one(&mut *tx)
.await?;
if wallet.current_balance < amount {
tx.rollback().await?;
return Ok(false);
}
sqlx::query(
r#"
UPDATE tracecoin_wallets
SET current_balance = current_balance - $1, reserved = reserved + $1, updated_at = NOW()
WHERE id = $2
"#,
)
.bind(amount)
.bind(wallet.id)
.execute(&mut *tx)
.await?;
sqlx::query(
r#"
INSERT INTO tracecoin_ledger (wallet_id, transaction_type, amount, reference_type, reference_id)
VALUES ($1, 'RESERVE', $2, 'LEAD_REQUEST', $3)
"#,
)
.bind(wallet.id)
.bind(amount)
.bind(reference_id)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(true)
}
pub async fn try_debit_reserved_tracecoins(
pool: &PgPool,
user_id: Uuid,
amount: i32,
reference_id: Uuid,
) -> Result<bool, sqlx::Error> {
let mut tx = pool.begin().await?;
let wallet = sqlx::query_as::<_, Wallet>(
"SELECT * FROM tracecoin_wallets WHERE user_id = $1 FOR UPDATE",
)
.bind(user_id)
.fetch_optional(&mut *tx)
.await?;
let Some(wallet) = wallet else {
tx.rollback().await?;
return Ok(false);
};
if wallet.reserved < amount {
tx.rollback().await?;
return Ok(false);
}
sqlx::query(
r#"
UPDATE tracecoin_wallets
SET reserved = reserved - $1, updated_at = NOW()
WHERE id = $2
"#,
)
.bind(amount)
.bind(wallet.id)
.execute(&mut *tx)
.await?;
sqlx::query(
r#"
INSERT INTO tracecoin_ledger (wallet_id, transaction_type, amount, reference_type, reference_id)
VALUES ($1, 'DEBIT', $2, 'LEAD_ACCEPTED', $3)
"#,
)
.bind(wallet.id)
.bind(amount)
.bind(reference_id)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(true)
}
pub async fn try_release_reserved_tracecoins(
pool: &PgPool,
user_id: Uuid,
amount: i32,
reference_id: Uuid,
reason: &str,
) -> Result<bool, sqlx::Error> {
let mut tx = pool.begin().await?;
let wallet = sqlx::query_as::<_, Wallet>(
"SELECT * FROM tracecoin_wallets WHERE user_id = $1 FOR UPDATE",
)
.bind(user_id)
.fetch_optional(&mut *tx)
.await?;
let Some(wallet) = wallet else {
tx.rollback().await?;
return Ok(false);
};
if wallet.reserved < amount {
tx.rollback().await?;
return Ok(false);
}
sqlx::query(
r#"
UPDATE tracecoin_wallets
SET reserved = reserved - $1, current_balance = current_balance + $1, updated_at = NOW()
WHERE id = $2
"#,
)
.bind(amount)
.bind(wallet.id)
.execute(&mut *tx)
.await?;
sqlx::query(
r#"
INSERT INTO tracecoin_ledger (wallet_id, transaction_type, amount, reference_type, reference_id)
VALUES ($1, 'RELEASE', $2, $3, $4)
"#,
)
.bind(wallet.id)
.bind(amount)
.bind(reason)
.bind(reference_id)
.execute(&mut *tx)
.await?;
tx.commit().await?;
Ok(true)
}
}

View file

@ -32,6 +32,58 @@ pub struct UpsertTutorProfilePayload {
pub struct TutorRepository; pub struct TutorRepository;
impl TutorRepository { impl TutorRepository {
pub async fn get_by_user_id(pool: &PgPool, user_id: Uuid) -> Result<Option<TutorProfile>, sqlx::Error> {
sqlx::query_as::<_, TutorProfile>(
r#"SELECT tp.id, tp.user_role_profile_id, tp.subjects, tp.board_types, tp.qualification,
tp.teaches_online, tp.teaches_offline, tp.experience_years, tp.hourly_rate_inr,
tp.created_at, tp.updated_at
FROM tutor_profiles tp
INNER JOIN user_role_profiles urp ON urp.id = tp.user_role_profile_id
WHERE urp.user_id = $1 AND urp.role_key = 'tutor'"#,
)
.bind(user_id)
.fetch_optional(pool)
.await
}
pub async fn upsert_by_user_id(pool: &PgPool, user_id: Uuid, p: UpsertTutorProfilePayload) -> Result<TutorProfile, sqlx::Error> {
let user_role_profile = sqlx::query_as::<_, (Uuid,)>(
r#"SELECT id FROM user_role_profiles WHERE user_id = $1 AND role_key = 'tutor'"#,
)
.bind(user_id)
.fetch_optional(pool)
.await?
.ok_or(sqlx::Error::RowNotFound)?;
sqlx::query_as::<_, TutorProfile>(
r#"INSERT INTO tutor_profiles (user_role_profile_id, subjects, board_types, qualification,
teaches_online, teaches_offline, experience_years, hourly_rate_inr)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
ON CONFLICT (user_role_profile_id) DO UPDATE SET
subjects = COALESCE(EXCLUDED.subjects, tutor_profiles.subjects),
board_types = COALESCE(EXCLUDED.board_types, tutor_profiles.board_types),
qualification = EXCLUDED.qualification,
teaches_online = EXCLUDED.teaches_online,
teaches_offline = EXCLUDED.teaches_offline,
experience_years = EXCLUDED.experience_years,
hourly_rate_inr = EXCLUDED.hourly_rate_inr,
updated_at = NOW()
RETURNING id, user_role_profile_id, subjects, board_types, qualification,
teaches_online, teaches_offline, experience_years, hourly_rate_inr,
created_at, updated_at"#,
)
.bind(user_role_profile.0)
.bind(&p.subjects)
.bind(&p.board_types)
.bind(&p.qualification)
.bind(p.teaches_online)
.bind(p.teaches_offline)
.bind(p.experience_years)
.bind(p.hourly_rate_inr)
.fetch_one(pool)
.await
}
pub async fn get_by_user_role_id(pool: &PgPool, user_role_profile_id: Uuid) -> Result<Option<TutorProfile>, sqlx::Error> { pub async fn get_by_user_role_id(pool: &PgPool, user_role_profile_id: Uuid) -> Result<Option<TutorProfile>, sqlx::Error> {
sqlx::query_as::<_, TutorProfile>( sqlx::query_as::<_, TutorProfile>(
r#"SELECT id, user_role_profile_id, subjects, board_types, qualification, r#"SELECT id, user_role_profile_id, subjects, board_types, qualification,

View file

@ -28,6 +28,53 @@ pub struct UpsertUgcContentCreatorProfilePayload {
pub struct UgcContentCreatorRepository; pub struct UgcContentCreatorRepository;
impl UgcContentCreatorRepository { impl UgcContentCreatorRepository {
pub async fn get_by_user_id(pool: &PgPool, user_id: Uuid) -> Result<Option<UgcContentCreatorProfile>, sqlx::Error> {
sqlx::query_as::<_, UgcContentCreatorProfile>(
r#"SELECT uccp.id, uccp.user_role_profile_id, uccp.niche_tags, uccp.content_formats, uccp.platforms,
uccp.turnaround_days, uccp.starting_price_inr,
uccp.created_at, uccp.updated_at
FROM ugc_content_creator_profiles uccp
INNER JOIN user_role_profiles urp ON urp.id = uccp.user_role_profile_id
WHERE urp.user_id = $1 AND urp.role_key = 'ugc_content_creator'"#,
)
.bind(user_id)
.fetch_optional(pool)
.await
}
pub async fn upsert_by_user_id(pool: &PgPool, user_id: Uuid, p: UpsertUgcContentCreatorProfilePayload) -> Result<UgcContentCreatorProfile, sqlx::Error> {
let user_role_profile = sqlx::query_as::<_, (Uuid,)>(
r#"SELECT id FROM user_role_profiles WHERE user_id = $1 AND role_key = 'ugc_content_creator'"#,
)
.bind(user_id)
.fetch_optional(pool)
.await?
.ok_or(sqlx::Error::RowNotFound)?;
sqlx::query_as::<_, UgcContentCreatorProfile>(
r#"INSERT INTO ugc_content_creator_profiles (user_role_profile_id, niche_tags, content_formats,
platforms, turnaround_days, starting_price_inr)
VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (user_role_profile_id) DO UPDATE SET
niche_tags = COALESCE(EXCLUDED.niche_tags, ugc_content_creator_profiles.niche_tags),
content_formats = COALESCE(EXCLUDED.content_formats, ugc_content_creator_profiles.content_formats),
platforms = COALESCE(EXCLUDED.platforms, ugc_content_creator_profiles.platforms),
turnaround_days = EXCLUDED.turnaround_days,
starting_price_inr = EXCLUDED.starting_price_inr,
updated_at = NOW()
RETURNING id, user_role_profile_id, niche_tags, content_formats, platforms,
turnaround_days, starting_price_inr, created_at, updated_at"#,
)
.bind(user_role_profile.0)
.bind(&p.niche_tags)
.bind(&p.content_formats)
.bind(&p.platforms)
.bind(p.turnaround_days)
.bind(p.starting_price_inr)
.fetch_one(pool)
.await
}
pub async fn get_by_user_role_id(pool: &PgPool, user_role_profile_id: Uuid) -> Result<Option<UgcContentCreatorProfile>, sqlx::Error> { pub async fn get_by_user_role_id(pool: &PgPool, user_role_profile_id: Uuid) -> Result<Option<UgcContentCreatorProfile>, sqlx::Error> {
sqlx::query_as::<_, UgcContentCreatorProfile>( sqlx::query_as::<_, UgcContentCreatorProfile>(
r#"SELECT id, user_role_profile_id, niche_tags, content_formats, platforms, r#"SELECT id, user_role_profile_id, niche_tags, content_formats, platforms,

View file

@ -26,6 +26,51 @@ pub struct UpsertVideoEditorProfilePayload {
pub struct VideoEditorRepository; pub struct VideoEditorRepository;
impl VideoEditorRepository { impl VideoEditorRepository {
pub async fn get_by_user_id(pool: &PgPool, user_id: Uuid) -> Result<Option<VideoEditorProfile>, sqlx::Error> {
sqlx::query_as::<_, VideoEditorProfile>(
r#"SELECT vep.id, vep.user_role_profile_id, vep.software_skills, vep.style_tags,
vep.turnaround_days, vep.starting_price_inr,
vep.created_at, vep.updated_at
FROM video_editor_profiles vep
INNER JOIN user_role_profiles urp ON urp.id = vep.user_role_profile_id
WHERE urp.user_id = $1 AND urp.role_key = 'video_editor'"#,
)
.bind(user_id)
.fetch_optional(pool)
.await
}
pub async fn upsert_by_user_id(pool: &PgPool, user_id: Uuid, p: UpsertVideoEditorProfilePayload) -> Result<VideoEditorProfile, sqlx::Error> {
let user_role_profile = sqlx::query_as::<_, (Uuid,)>(
r#"SELECT id FROM user_role_profiles WHERE user_id = $1 AND role_key = 'video_editor'"#,
)
.bind(user_id)
.fetch_optional(pool)
.await?
.ok_or(sqlx::Error::RowNotFound)?;
sqlx::query_as::<_, VideoEditorProfile>(
r#"INSERT INTO video_editor_profiles (user_role_profile_id, software_skills, style_tags,
turnaround_days, starting_price_inr)
VALUES ($1, $2, $3, $4, $5)
ON CONFLICT (user_role_profile_id) DO UPDATE SET
software_skills = COALESCE(EXCLUDED.software_skills, video_editor_profiles.software_skills),
style_tags = COALESCE(EXCLUDED.style_tags, video_editor_profiles.style_tags),
turnaround_days = EXCLUDED.turnaround_days,
starting_price_inr = EXCLUDED.starting_price_inr,
updated_at = NOW()
RETURNING id, user_role_profile_id, software_skills, style_tags, turnaround_days,
starting_price_inr, created_at, updated_at"#,
)
.bind(user_role_profile.0)
.bind(&p.software_skills)
.bind(&p.style_tags)
.bind(p.turnaround_days)
.bind(p.starting_price_inr)
.fetch_one(pool)
.await
}
pub async fn get_by_user_role_id(pool: &PgPool, user_role_profile_id: Uuid) -> Result<Option<VideoEditorProfile>, sqlx::Error> { pub async fn get_by_user_role_id(pool: &PgPool, user_role_profile_id: Uuid) -> Result<Option<VideoEditorProfile>, sqlx::Error> {
sqlx::query_as::<_, VideoEditorProfile>( sqlx::query_as::<_, VideoEditorProfile>(
r#"SELECT id, user_role_profile_id, software_skills, style_tags, turnaround_days, r#"SELECT id, user_role_profile_id, software_skills, style_tags, turnaround_days,