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:
parent
2e283e5d67
commit
c433ab5fed
52 changed files with 1348 additions and 550 deletions
47
Cargo.lock
generated
47
Cargo.lock
generated
|
|
@ -1054,6 +1054,18 @@ dependencies = [
|
|||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "db-migrate"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.6.1"
|
||||
|
|
@ -2053,6 +2065,23 @@ dependencies = [
|
|||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobs"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"chrono",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jobserver"
|
||||
version = "0.1.34"
|
||||
|
|
@ -2097,6 +2126,23 @@ dependencies = [
|
|||
"spin",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leads"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"axum",
|
||||
"chrono",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sqlx",
|
||||
"tokio",
|
||||
"tower-http",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "leb128fmt"
|
||||
version = "0.1.0"
|
||||
|
|
@ -3852,6 +3898,7 @@ dependencies = [
|
|||
"tower",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
|
@ -7,8 +7,9 @@ use uuid::Uuid;
|
|||
#[derive(Serialize)]
|
||||
pub struct AdminCateringServiceList {
|
||||
pub id: Uuid,
|
||||
pub user_role_profile_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub business_name: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
pub location: Option<String>,
|
||||
pub status: String,
|
||||
|
|
@ -16,12 +17,13 @@ pub struct AdminCateringServiceList {
|
|||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl From<CateringServiceProfile> for AdminCateringServiceList {
|
||||
fn from(p: CateringServiceProfile) -> Self {
|
||||
impl From<UserRoleProfile> for AdminCateringServiceList {
|
||||
fn from(p: UserRoleProfile) -> Self {
|
||||
Self {
|
||||
id: p.id,
|
||||
user_role_profile_id: p.id,
|
||||
user_id: p.user_id,
|
||||
business_name: p.business_name,
|
||||
display_name: p.display_name,
|
||||
bio: p.bio,
|
||||
location: p.location,
|
||||
status: p.status,
|
||||
|
|
@ -40,10 +42,15 @@ pub fn router() -> Router<ProfessionState> {
|
|||
async fn list_catering_services(
|
||||
State(state): State<ProfessionState>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let services = sqlx::query_as::<_, CateringServiceProfile>(
|
||||
let services = sqlx::query_as::<_, UserRoleProfile>(
|
||||
r#"
|
||||
SELECT id, user_id, business_name, bio, location, custom_data, status, created_at, updated_at
|
||||
FROM catering_service_profiles
|
||||
SELECT id, user_id, role_key, display_name, bio, location,
|
||||
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
|
||||
LIMIT 100
|
||||
"#,
|
||||
|
|
@ -60,11 +67,15 @@ async fn get_catering_service(
|
|||
State(state): State<ProfessionState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let service = sqlx::query_as::<_, CateringServiceProfile>(
|
||||
let service = sqlx::query_as::<_, UserRoleProfile>(
|
||||
r#"
|
||||
SELECT id, user_id, business_name, bio, location, custom_data, status, created_at, updated_at
|
||||
FROM catering_service_profiles
|
||||
WHERE id = $1
|
||||
SELECT id, user_id, role_key, display_name, bio, location,
|
||||
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 id = $1 AND role_key = 'catering_service'
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ async fn update_profile(
|
|||
auth: AuthUser,
|
||||
Json(payload): Json<UpsertCateringServiceProfilePayload>,
|
||||
) -> 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(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -106,8 +106,7 @@ pub struct AdminApplicationRow {
|
|||
pub applicant_name: String,
|
||||
pub applicant_email: String,
|
||||
pub status: String,
|
||||
pub cover_letter: Option<String>,
|
||||
pub resume_url: Option<String>,
|
||||
pub cover_note: Option<String>,
|
||||
pub applied_at: DateTime<Utc>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
|
@ -120,12 +119,11 @@ impl From<Application> for AdminApplicationRow {
|
|||
job_title: String::new(),
|
||||
company_id: Uuid::nil(),
|
||||
company_name: String::new(),
|
||||
applicant_id: a.job_seeker_id,
|
||||
applicant_id: a.applicant_user_id,
|
||||
applicant_name: String::new(),
|
||||
applicant_email: String::new(),
|
||||
status: a.status,
|
||||
cover_letter: a.cover_letter,
|
||||
resume_url: a.resume_url,
|
||||
cover_note: a.cover_note,
|
||||
applied_at: a.applied_at,
|
||||
created_at: a.updated_at,
|
||||
}
|
||||
|
|
@ -252,9 +250,9 @@ async fn list_applications(
|
|||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let applications = sqlx::query_as::<_, Application>(
|
||||
r#"
|
||||
SELECT id, job_id, job_seeker_id, cover_letter, resume_url, status,
|
||||
applied_at, updated_at, contact_viewed
|
||||
FROM applications
|
||||
SELECT id, job_id, applicant_user_id, cover_note, status,
|
||||
applied_at, updated_at
|
||||
FROM job_applications
|
||||
ORDER BY applied_at DESC
|
||||
LIMIT 100
|
||||
"#,
|
||||
|
|
|
|||
|
|
@ -367,9 +367,9 @@ async fn update_application_status(
|
|||
Ok(updated) => {
|
||||
// Notify applicant of status change (ignore failures)
|
||||
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)
|
||||
.await;
|
||||
if let Ok(Some((name, email))) = applicant_info {
|
||||
|
|
@ -405,47 +405,15 @@ async fn view_contact(
|
|||
return (StatusCode::FORBIDDEN, "Access denied").into_response();
|
||||
}
|
||||
|
||||
// If contact was already viewed for this application, return info without deducting again
|
||||
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
|
||||
// Fetch applicant contact info via applicant_user_id → users
|
||||
let contact = sqlx::query_as::<_, (Option<String>, String, Option<String>)>(
|
||||
r#"
|
||||
SELECT u.full_name, u.email, u.phone
|
||||
FROM users u
|
||||
INNER JOIN job_seekers js ON js.user_id = u.id
|
||||
WHERE js.id = $1
|
||||
WHERE u.id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(app.job_seeker_id)
|
||||
.bind(app.applicant_user_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await;
|
||||
|
||||
|
|
|
|||
|
|
@ -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 m_req_sys = Arc::clone(&mailer);
|
||||
tokio::spawn(async move {
|
||||
let mut interval = time::interval(Duration::from_secs(60 * 60));
|
||||
loop {
|
||||
interval.tick().await;
|
||||
tracing::info!("Running Requirement Expiry Task...");
|
||||
if let Err(e) = tasks::requirements::expire_stale_requirements(&p_req_sys, &m_req_sys).await {
|
||||
tracing::error!("Requirement Expiry Task Failed: {}", e);
|
||||
tracing::info!("Running Lead Expiry Task...");
|
||||
if let Err(e) = tasks::requirements::expire_stale_leads(&p_req_sys, &m_req_sys).await {
|
||||
tracing::error!("Lead Expiry Task Failed: {}", e);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -18,19 +18,18 @@ pub async fn expire_stale_lead_requests(
|
|||
full_name: String,
|
||||
}
|
||||
|
||||
// Find stale requests that are still PENDING
|
||||
let records = sqlx::query_as::<_, Record>(
|
||||
r#"
|
||||
SELECT
|
||||
lr.id AS lead_request_id,
|
||||
lr.professional_id,
|
||||
lr.user_role_profile_id,
|
||||
lr.tracecoins_reserved,
|
||||
pp.user_id,
|
||||
urp.user_id,
|
||||
u.email,
|
||||
u.full_name
|
||||
FROM lead_requests lr
|
||||
INNER JOIN professional_profiles pp ON pp.id = lr.professional_id
|
||||
INNER JOIN users u ON u.id = pp.user_id
|
||||
INNER JOIN user_role_profiles urp ON urp.id = lr.user_role_profile_id
|
||||
INNER JOIN users u ON u.id = urp.user_id
|
||||
WHERE lr.status = 'PENDING'
|
||||
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());
|
||||
|
||||
for rec in records {
|
||||
// Run expiry flow inside a transaction to ensure we don't duplicate refunds
|
||||
let mut tx = pool.begin().await?;
|
||||
|
||||
// 1. Mark as expired
|
||||
let updated = sqlx::query(
|
||||
"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?;
|
||||
|
||||
if updated.rows_affected() == 0 {
|
||||
// Already updated concurrently
|
||||
tx.rollback().await?;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Refund Tracecoins if they were reserved
|
||||
if rec.tracecoins_reserved > 0 {
|
||||
// Re-use logic: Release reserved Tracecoins
|
||||
// 2.a Add to balance
|
||||
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.user_id)
|
||||
.execute(&mut *tx)
|
||||
.await?;
|
||||
|
||||
// 2.b Insert ledger entry
|
||||
sqlx::query(
|
||||
r#"
|
||||
INSERT INTO tracecoin_ledger (user_id, amount, transaction_type, reference_id, description, created_at)
|
||||
VALUES ($1, $2, 'RELEASE', $3, 'Lead Request Expired', $4)
|
||||
INSERT INTO tracecoin_ledger (wallet_id, amount, transaction_type, reference_type, reference_id, created_at)
|
||||
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.lead_request_id)
|
||||
.bind(Utc::now())
|
||||
.bind(rec.user_id)
|
||||
.execute(&mut *tx)
|
||||
.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;
|
||||
|
||||
tracing::info!("Expired lead request {} and refunded {} tracecoins to {}", rec.lead_request_id, rec.tracecoins_reserved, rec.email);
|
||||
|
|
|
|||
|
|
@ -2,34 +2,31 @@ use sqlx::PgPool;
|
|||
use email::Mailer;
|
||||
use chrono::Utc;
|
||||
|
||||
pub async fn expire_stale_requirements(
|
||||
pub async fn expire_stale_leads(
|
||||
pool: &PgPool,
|
||||
mailer: &Mailer,
|
||||
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
|
||||
let now = Utc::now();
|
||||
|
||||
// Find stale requirements that are still OPEN
|
||||
// Update them directly returning the affected customer info
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(sqlx::FromRow)]
|
||||
struct ReqRecord {
|
||||
requirement_id: Uuid,
|
||||
struct LeadRecord {
|
||||
lead_id: Uuid,
|
||||
title: String,
|
||||
email: String,
|
||||
full_name: String,
|
||||
}
|
||||
|
||||
let records = sqlx::query_as::<_, ReqRecord>(
|
||||
let records = sqlx::query_as::<_, LeadRecord>(
|
||||
r#"
|
||||
UPDATE requirements
|
||||
UPDATE leads
|
||||
SET status = 'EXPIRED'
|
||||
FROM customers c
|
||||
JOIN users u ON u.id = c.user_id
|
||||
WHERE requirements.customer_id = c.id
|
||||
AND requirements.status = 'OPEN'
|
||||
AND requirements.expires_at < $1
|
||||
RETURNING requirements.id as requirement_id, requirements.title, u.email, u.full_name
|
||||
FROM users u
|
||||
WHERE leads.created_by_user_id = u.id
|
||||
AND leads.status = 'OPEN'
|
||||
AND leads.expires_at < $1
|
||||
RETURNING leads.id as lead_id, leads.title, u.email, u.full_name
|
||||
"#
|
||||
)
|
||||
.bind(now)
|
||||
|
|
@ -40,11 +37,11 @@ pub async fn expire_stale_requirements(
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
tracing::info!("Expired {} stale requirements.", records.len());
|
||||
tracing::info!("Expired {} stale leads.", records.len());
|
||||
|
||||
for rec in records {
|
||||
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(())
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ use axum::{
|
|||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
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::lead_request::LeadRequestRepository;
|
||||
use db::models::user::UserRepository;
|
||||
use db::models::verification::VerificationRepository;
|
||||
use db::models::tracecoin_wallet::TracecoinWalletRepository;
|
||||
use contracts::auth_middleware::AuthUser;
|
||||
use crate::AppState;
|
||||
|
||||
|
|
@ -23,9 +23,9 @@ pub fn router() -> Router<AppState> {
|
|||
.route("/requirements", get(list_requirements).post(create_requirement))
|
||||
.route("/requirements/{id}", get(get_requirement).patch(update_requirement))
|
||||
.route("/requirements/{id}/submit", post(submit_requirement))
|
||||
.route("/requirements/{id}/requests", get(list_requests))
|
||||
.route("/requirements/{id}/requests/{lead_id}/approve", post(approve_request))
|
||||
.route("/requirements/{id}/requests/{lead_id}/reject", post(reject_request))
|
||||
.route("/requests", get(list_requests))
|
||||
.route("/requests/{lead_id}/approve", post(approve_request))
|
||||
.route("/requests/{lead_id}/reject", post(reject_request))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
|
@ -109,14 +109,9 @@ async fn list_requirements(
|
|||
auth: AuthUser,
|
||||
Query(q): Query<PaginationQuery>,
|
||||
) -> 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 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!({
|
||||
"data": reqs,
|
||||
"pagination": { "page": page, "limit": limit }
|
||||
|
|
@ -130,23 +125,9 @@ async fn create_requirement(
|
|||
auth: AuthUser,
|
||||
Json(payload): Json<CreateRequirementRequest>,
|
||||
) -> 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 db_payload = DbCreateRequirementPayload {
|
||||
customer_id: customer.id,
|
||||
profession_key: payload.profession_key,
|
||||
title: payload.title,
|
||||
description: payload.description,
|
||||
|
|
@ -157,10 +138,7 @@ async fn create_requirement(
|
|||
};
|
||||
|
||||
match RequirementRepository::create(&state.pool, db_payload).await {
|
||||
Ok(req) => {
|
||||
let _ = CustomerRepository::update_active_requirement_count(&state.pool, customer.id, 1).await;
|
||||
(StatusCode::CREATED, Json(req)).into_response()
|
||||
},
|
||||
Ok(req) => (StatusCode::CREATED, Json(req)).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(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
auth: AuthUser,
|
||||
_auth: AuthUser,
|
||||
Json(payload): Json<DbUpdateRequirementPayload>,
|
||||
) -> 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(),
|
||||
Ok(Some(r)) => r,
|
||||
_ => return (StatusCode::NOT_FOUND, "Requirement not found").into_response(),
|
||||
};
|
||||
|
||||
|
|
@ -205,18 +177,8 @@ async fn submit_requirement(
|
|||
Path(id): Path<Uuid>,
|
||||
auth: AuthUser,
|
||||
) -> 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 {
|
||||
Ok(Some(r)) if r.customer_id == customer.id => r,
|
||||
Ok(Some(_)) => return (StatusCode::FORBIDDEN, "Access denied").into_response(),
|
||||
Ok(Some(r)) => r,
|
||||
_ => return (StatusCode::NOT_FOUND, "Requirement not found").into_response(),
|
||||
};
|
||||
|
||||
|
|
@ -240,7 +202,7 @@ async fn submit_requirement(
|
|||
"location": updated.location,
|
||||
"budget": updated.budget,
|
||||
"status": updated.status,
|
||||
"customer_id": updated.customer_id,
|
||||
"created_by_user_id": updated.created_by_user_id,
|
||||
});
|
||||
let _ = VerificationRepository::create(
|
||||
&state.pool,
|
||||
|
|
@ -261,45 +223,22 @@ async fn submit_requirement(
|
|||
async fn list_requests(
|
||||
State(state): State<AppState>,
|
||||
Path(id): Path<Uuid>,
|
||||
auth: AuthUser,
|
||||
_auth: AuthUser,
|
||||
Query(q): Query<PaginationQuery>,
|
||||
) -> 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 limit = q.limit.unwrap_or(20);
|
||||
let offset = (page - 1) * limit;
|
||||
|
||||
#[derive(serde::Serialize, sqlx::FromRow)]
|
||||
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>(
|
||||
let rows_result = sqlx::query_as::<_, db::models::lead_request::LeadRequest>(
|
||||
r#"
|
||||
SELECT lr.*, u.full_name as professional_name, u.avatar_url as professional_avatar_url
|
||||
FROM lead_requests lr
|
||||
LEFT JOIN professional_profiles pp ON pp.id = lr.professional_id
|
||||
LEFT JOIN users u ON u.id = pp.user_id
|
||||
WHERE lr.requirement_id = $1
|
||||
ORDER BY lr.requested_at DESC
|
||||
SELECT * FROM lead_requests
|
||||
WHERE user_role_profile_id = $1
|
||||
ORDER BY requested_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
"#
|
||||
)
|
||||
.bind(req.id)
|
||||
.bind(id)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(&state.pool)
|
||||
|
|
@ -316,22 +255,11 @@ async fn list_requests(
|
|||
|
||||
async fn approve_request(
|
||||
State(state): State<AppState>,
|
||||
Path((req_id, lead_id)): Path<(Uuid, Uuid)>,
|
||||
Path(lead_id): Path<Uuid>,
|
||||
auth: AuthUser,
|
||||
) -> 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 {
|
||||
Ok(Some(l)) if l.requirement_id == req.id => l,
|
||||
Ok(Some(l)) => l,
|
||||
_ => 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 {
|
||||
Ok(updated) => {
|
||||
let prof_user_id = match ProfessionalRepository::get_user_id_by_professional_id(&state.pool, lead.professional_id).await {
|
||||
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(
|
||||
match TracecoinWalletRepository::try_debit_reserved_tracecoins(
|
||||
&state.pool,
|
||||
prof_user_id,
|
||||
lead.user_role_profile_id,
|
||||
lead.tracecoins_reserved,
|
||||
lead.id,
|
||||
).await {
|
||||
|
|
@ -358,33 +280,8 @@ async fn approve_request(
|
|||
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!({
|
||||
"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()
|
||||
},
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
|
|
@ -393,23 +290,12 @@ async fn approve_request(
|
|||
|
||||
async fn reject_request(
|
||||
State(state): State<AppState>,
|
||||
Path((req_id, lead_id)): Path<(Uuid, Uuid)>,
|
||||
auth: AuthUser,
|
||||
Path(lead_id): Path<Uuid>,
|
||||
_auth: AuthUser,
|
||||
Json(_payload): Json<RejectRequestPayload>,
|
||||
) -> 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 {
|
||||
Ok(Some(l)) if l.requirement_id == req.id => l,
|
||||
Ok(Some(l)) => l,
|
||||
_ => 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 {
|
||||
Ok(updated) => {
|
||||
let prof_user_id = match ProfessionalRepository::get_user_id_by_professional_id(&state.pool, lead.professional_id).await {
|
||||
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(
|
||||
match TracecoinWalletRepository::try_release_reserved_tracecoins(
|
||||
&state.pool,
|
||||
prof_user_id,
|
||||
lead.user_role_profile_id,
|
||||
lead.tracecoins_reserved,
|
||||
lead.id,
|
||||
"LEAD_REJECTED",
|
||||
|
|
@ -437,13 +317,6 @@ async fn reject_request(
|
|||
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()
|
||||
},
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
|
@ -7,6 +7,7 @@ use uuid::Uuid;
|
|||
#[derive(Serialize)]
|
||||
pub struct AdminDeveloperList {
|
||||
pub id: Uuid,
|
||||
pub user_role_profile_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub display_name: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
|
|
@ -16,10 +17,11 @@ pub struct AdminDeveloperList {
|
|||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl From<DeveloperProfile> for AdminDeveloperList {
|
||||
fn from(p: DeveloperProfile) -> Self {
|
||||
impl From<UserRoleProfile> for AdminDeveloperList {
|
||||
fn from(p: UserRoleProfile) -> Self {
|
||||
Self {
|
||||
id: p.id,
|
||||
user_role_profile_id: p.id,
|
||||
user_id: p.user_id,
|
||||
display_name: p.display_name,
|
||||
bio: p.bio,
|
||||
|
|
@ -40,10 +42,15 @@ pub fn router() -> Router<ProfessionState> {
|
|||
async fn list_developers(
|
||||
State(state): State<ProfessionState>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let developers = sqlx::query_as::<_, DeveloperProfile>(
|
||||
let developers = sqlx::query_as::<_, UserRoleProfile>(
|
||||
r#"
|
||||
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at
|
||||
FROM developer_profiles
|
||||
SELECT id, user_id, role_key, display_name, bio, location,
|
||||
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
|
||||
LIMIT 100
|
||||
"#,
|
||||
|
|
@ -60,11 +67,15 @@ async fn get_developer(
|
|||
State(state): State<ProfessionState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let developer = sqlx::query_as::<_, DeveloperProfile>(
|
||||
let developer = sqlx::query_as::<_, UserRoleProfile>(
|
||||
r#"
|
||||
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at
|
||||
FROM developer_profiles
|
||||
WHERE id = $1
|
||||
SELECT id, user_id, role_key, display_name, bio, location,
|
||||
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 id = $1 AND role_key = 'developer'
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ async fn update_profile(
|
|||
auth: AuthUser,
|
||||
Json(payload): Json<UpsertDeveloperProfilePayload>,
|
||||
) -> 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(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
|
@ -7,6 +7,7 @@ use uuid::Uuid;
|
|||
#[derive(Serialize)]
|
||||
pub struct AdminFitnessTrainerList {
|
||||
pub id: Uuid,
|
||||
pub user_role_profile_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub display_name: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
|
|
@ -16,10 +17,11 @@ pub struct AdminFitnessTrainerList {
|
|||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl From<FitnessTrainerProfile> for AdminFitnessTrainerList {
|
||||
fn from(p: FitnessTrainerProfile) -> Self {
|
||||
impl From<UserRoleProfile> for AdminFitnessTrainerList {
|
||||
fn from(p: UserRoleProfile) -> Self {
|
||||
Self {
|
||||
id: p.id,
|
||||
user_role_profile_id: p.id,
|
||||
user_id: p.user_id,
|
||||
display_name: p.display_name,
|
||||
bio: p.bio,
|
||||
|
|
@ -40,10 +42,15 @@ pub fn router() -> Router<ProfessionState> {
|
|||
async fn list_fitness_trainers(
|
||||
State(state): State<ProfessionState>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let trainers = sqlx::query_as::<_, FitnessTrainerProfile>(
|
||||
let trainers = sqlx::query_as::<_, UserRoleProfile>(
|
||||
r#"
|
||||
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at
|
||||
FROM fitness_trainer_profiles
|
||||
SELECT id, user_id, role_key, display_name, bio, location,
|
||||
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
|
||||
LIMIT 100
|
||||
"#,
|
||||
|
|
@ -60,11 +67,15 @@ async fn get_fitness_trainer(
|
|||
State(state): State<ProfessionState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let trainer = sqlx::query_as::<_, FitnessTrainerProfile>(
|
||||
let trainer = sqlx::query_as::<_, UserRoleProfile>(
|
||||
r#"
|
||||
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at
|
||||
FROM fitness_trainer_profiles
|
||||
WHERE id = $1
|
||||
SELECT id, user_id, role_key, display_name, bio, location,
|
||||
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 id = $1 AND role_key = 'fitness_trainer'
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ async fn update_profile(
|
|||
auth: AuthUser,
|
||||
Json(payload): Json<UpsertFitnessTrainerProfilePayload>,
|
||||
) -> 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(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
|
@ -7,6 +7,7 @@ use uuid::Uuid;
|
|||
#[derive(Serialize)]
|
||||
pub struct AdminGraphicDesignerList {
|
||||
pub id: Uuid,
|
||||
pub user_role_profile_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub display_name: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
|
|
@ -16,10 +17,11 @@ pub struct AdminGraphicDesignerList {
|
|||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl From<GraphicDesignerProfile> for AdminGraphicDesignerList {
|
||||
fn from(p: GraphicDesignerProfile) -> Self {
|
||||
impl From<UserRoleProfile> for AdminGraphicDesignerList {
|
||||
fn from(p: UserRoleProfile) -> Self {
|
||||
Self {
|
||||
id: p.id,
|
||||
user_role_profile_id: p.id,
|
||||
user_id: p.user_id,
|
||||
display_name: p.display_name,
|
||||
bio: p.bio,
|
||||
|
|
@ -40,10 +42,15 @@ pub fn router() -> Router<ProfessionState> {
|
|||
async fn list_graphic_designers(
|
||||
State(state): State<ProfessionState>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let designers = sqlx::query_as::<_, GraphicDesignerProfile>(
|
||||
let designers = sqlx::query_as::<_, UserRoleProfile>(
|
||||
r#"
|
||||
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at
|
||||
FROM graphic_designer_profiles
|
||||
SELECT id, user_id, role_key, display_name, bio, location,
|
||||
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
|
||||
LIMIT 100
|
||||
"#,
|
||||
|
|
@ -60,11 +67,15 @@ async fn get_graphic_designer(
|
|||
State(state): State<ProfessionState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let designer = sqlx::query_as::<_, GraphicDesignerProfile>(
|
||||
let designer = sqlx::query_as::<_, UserRoleProfile>(
|
||||
r#"
|
||||
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at
|
||||
FROM graphic_designer_profiles
|
||||
WHERE id = $1
|
||||
SELECT id, user_id, role_key, display_name, bio, location,
|
||||
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 id = $1 AND role_key = 'graphic_designer'
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ async fn update_profile(
|
|||
auth: AuthUser,
|
||||
Json(payload): Json<UpsertGraphicDesignerProfilePayload>,
|
||||
) -> 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(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ pub struct JobBrowseQuery {
|
|||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ApplyRequest {
|
||||
pub cover_letter: Option<String>,
|
||||
pub cover_note: Option<String>,
|
||||
pub resume_url: Option<String>,
|
||||
}
|
||||
|
||||
|
|
@ -234,9 +234,8 @@ async fn apply_to_job(
|
|||
|
||||
let db_payload = CreateApplicationPayload {
|
||||
job_id: job.id,
|
||||
job_seeker_id: seeker.id,
|
||||
cover_letter: payload.cover_letter,
|
||||
resume_url: payload.resume_url.or(seeker.resume_url),
|
||||
applicant_user_id: auth.user_id,
|
||||
cover_note: payload.cover_note,
|
||||
};
|
||||
|
||||
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 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!({
|
||||
"data": apps,
|
||||
"pagination": { "page": page, "limit": limit }
|
||||
|
|
@ -307,7 +306,7 @@ async fn get_my_application(
|
|||
};
|
||||
|
||||
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(None) => (StatusCode::NOT_FOUND, "Application not found").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 {
|
||||
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(),
|
||||
_ => return (StatusCode::NOT_FOUND, "Application not found").into_response(),
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use axum::{
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
routing::{get, post, put, delete},
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -16,7 +16,7 @@ pub struct AppState {
|
|||
pub pool: PgPool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct Job {
|
||||
pub id: uuid::Uuid,
|
||||
pub title: String,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use axum::{
|
||||
extract::State,
|
||||
http::StatusCode,
|
||||
routing::{get, post, put, delete},
|
||||
routing::{get, post},
|
||||
Json, Router,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
|
@ -16,7 +16,7 @@ pub struct AppState {
|
|||
pub pool: PgPool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
|
||||
pub struct Lead {
|
||||
pub id: uuid::Uuid,
|
||||
pub title: String,
|
||||
|
|
@ -37,7 +37,7 @@ pub struct CreateLead {
|
|||
|
||||
async fn list_leads(State(state): State<Arc<AppState>>) -> Result<Json<Vec<Lead>>, StatusCode> {
|
||||
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)
|
||||
.await
|
||||
|
|
@ -52,7 +52,7 @@ async fn create_lead(
|
|||
) -> Result<Json<Lead>, StatusCode> {
|
||||
let lead = sqlx::query_as::<_, Lead>(
|
||||
r#"
|
||||
INSERT INTO requirements (title, description, location, profession_key)
|
||||
INSERT INTO leads (title, description, location, profession_key)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
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>,
|
||||
) -> Result<Json<Lead>, StatusCode> {
|
||||
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)
|
||||
.fetch_optional(&state.pool)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
|
@ -7,6 +7,7 @@ use uuid::Uuid;
|
|||
#[derive(Serialize)]
|
||||
pub struct AdminMakeupArtistList {
|
||||
pub id: Uuid,
|
||||
pub user_role_profile_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub display_name: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
|
|
@ -16,10 +17,11 @@ pub struct AdminMakeupArtistList {
|
|||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl From<MakeupArtistProfile> for AdminMakeupArtistList {
|
||||
fn from(p: MakeupArtistProfile) -> Self {
|
||||
impl From<UserRoleProfile> for AdminMakeupArtistList {
|
||||
fn from(p: UserRoleProfile) -> Self {
|
||||
Self {
|
||||
id: p.id,
|
||||
user_role_profile_id: p.id,
|
||||
user_id: p.user_id,
|
||||
display_name: p.display_name,
|
||||
bio: p.bio,
|
||||
|
|
@ -40,10 +42,15 @@ pub fn router() -> Router<ProfessionState> {
|
|||
async fn list_makeup_artists(
|
||||
State(state): State<ProfessionState>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let artists = sqlx::query_as::<_, MakeupArtistProfile>(
|
||||
let artists = sqlx::query_as::<_, UserRoleProfile>(
|
||||
r#"
|
||||
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at
|
||||
FROM makeup_artist_profiles
|
||||
SELECT id, user_id, role_key, display_name, bio, location,
|
||||
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
|
||||
LIMIT 100
|
||||
"#,
|
||||
|
|
@ -60,11 +67,15 @@ async fn get_makeup_artist(
|
|||
State(state): State<ProfessionState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let artist = sqlx::query_as::<_, MakeupArtistProfile>(
|
||||
let artist = sqlx::query_as::<_, UserRoleProfile>(
|
||||
r#"
|
||||
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at
|
||||
FROM makeup_artist_profiles
|
||||
WHERE id = $1
|
||||
SELECT id, user_id, role_key, display_name, bio, location,
|
||||
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 id = $1 AND role_key = 'makeup_artist'
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ async fn update_profile(
|
|||
auth: AuthUser,
|
||||
Json(payload): Json<UpsertMakeupArtistProfilePayload>,
|
||||
) -> 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(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
|
@ -7,6 +7,7 @@ use uuid::Uuid;
|
|||
#[derive(Serialize)]
|
||||
pub struct AdminPhotographerList {
|
||||
pub id: Uuid,
|
||||
pub user_role_profile_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub display_name: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
|
|
@ -16,10 +17,11 @@ pub struct AdminPhotographerList {
|
|||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl From<PhotographerProfile> for AdminPhotographerList {
|
||||
fn from(p: PhotographerProfile) -> Self {
|
||||
impl From<UserRoleProfile> for AdminPhotographerList {
|
||||
fn from(p: UserRoleProfile) -> Self {
|
||||
Self {
|
||||
id: p.id,
|
||||
user_role_profile_id: p.id,
|
||||
user_id: p.user_id,
|
||||
display_name: p.display_name,
|
||||
bio: p.bio,
|
||||
|
|
@ -40,10 +42,15 @@ pub fn router() -> Router<ProfessionState> {
|
|||
async fn list_photographers(
|
||||
State(state): State<ProfessionState>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let photographers = sqlx::query_as::<_, PhotographerProfile>(
|
||||
let photographers = sqlx::query_as::<_, UserRoleProfile>(
|
||||
r#"
|
||||
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at
|
||||
FROM photographer_profiles
|
||||
SELECT id, user_id, role_key, display_name, bio, location,
|
||||
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
|
||||
LIMIT 100
|
||||
"#,
|
||||
|
|
@ -60,11 +67,15 @@ async fn get_photographer(
|
|||
State(state): State<ProfessionState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let photographer = sqlx::query_as::<_, PhotographerProfile>(
|
||||
let photographer = sqlx::query_as::<_, UserRoleProfile>(
|
||||
r#"
|
||||
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at
|
||||
FROM photographer_profiles
|
||||
WHERE id = $1
|
||||
SELECT id, user_id, role_key, display_name, bio, location,
|
||||
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 id = $1 AND role_key = 'photographer'
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ async fn update_profile(
|
|||
auth: AuthUser,
|
||||
Json(payload): Json<UpsertPhotographerProfilePayload>,
|
||||
) -> 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(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
|
@ -7,6 +7,7 @@ use uuid::Uuid;
|
|||
#[derive(Serialize)]
|
||||
pub struct AdminSocialMediaManagerList {
|
||||
pub id: Uuid,
|
||||
pub user_role_profile_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub display_name: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
|
|
@ -16,10 +17,11 @@ pub struct AdminSocialMediaManagerList {
|
|||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl From<SocialMediaManagerProfile> for AdminSocialMediaManagerList {
|
||||
fn from(p: SocialMediaManagerProfile) -> Self {
|
||||
impl From<UserRoleProfile> for AdminSocialMediaManagerList {
|
||||
fn from(p: UserRoleProfile) -> Self {
|
||||
Self {
|
||||
id: p.id,
|
||||
user_role_profile_id: p.id,
|
||||
user_id: p.user_id,
|
||||
display_name: p.display_name,
|
||||
bio: p.bio,
|
||||
|
|
@ -40,10 +42,15 @@ pub fn router() -> Router<ProfessionState> {
|
|||
async fn list_social_media_managers(
|
||||
State(state): State<ProfessionState>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let managers = sqlx::query_as::<_, SocialMediaManagerProfile>(
|
||||
let managers = sqlx::query_as::<_, UserRoleProfile>(
|
||||
r#"
|
||||
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at
|
||||
FROM social_media_manager_profiles
|
||||
SELECT id, user_id, role_key, display_name, bio, location,
|
||||
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
|
||||
LIMIT 100
|
||||
"#,
|
||||
|
|
@ -60,11 +67,15 @@ async fn get_social_media_manager(
|
|||
State(state): State<ProfessionState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let manager = sqlx::query_as::<_, SocialMediaManagerProfile>(
|
||||
let manager = sqlx::query_as::<_, UserRoleProfile>(
|
||||
r#"
|
||||
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at
|
||||
FROM social_media_manager_profiles
|
||||
WHERE id = $1
|
||||
SELECT id, user_id, role_key, display_name, bio, location,
|
||||
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 id = $1 AND role_key = 'social_media_manager'
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ async fn update_profile(
|
|||
auth: AuthUser,
|
||||
Json(payload): Json<UpsertSocialMediaManagerProfilePayload>,
|
||||
) -> 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(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
|
@ -7,6 +7,7 @@ use uuid::Uuid;
|
|||
#[derive(Serialize)]
|
||||
pub struct AdminTutorList {
|
||||
pub id: Uuid,
|
||||
pub user_role_profile_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub display_name: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
|
|
@ -16,10 +17,11 @@ pub struct AdminTutorList {
|
|||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl From<TutorProfile> for AdminTutorList {
|
||||
fn from(p: TutorProfile) -> Self {
|
||||
impl From<UserRoleProfile> for AdminTutorList {
|
||||
fn from(p: UserRoleProfile) -> Self {
|
||||
Self {
|
||||
id: p.id,
|
||||
user_role_profile_id: p.id,
|
||||
user_id: p.user_id,
|
||||
display_name: p.display_name,
|
||||
bio: p.bio,
|
||||
|
|
@ -43,10 +45,15 @@ pub fn router() -> Router<ProfessionState> {
|
|||
async fn list_tutors(
|
||||
State(state): State<ProfessionState>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let tutors = sqlx::query_as::<_, TutorProfile>(
|
||||
let tutors = sqlx::query_as::<_, UserRoleProfile>(
|
||||
r#"
|
||||
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at
|
||||
FROM tutor_profiles
|
||||
SELECT id, user_id, role_key, display_name, bio, location,
|
||||
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
|
||||
LIMIT 100
|
||||
"#,
|
||||
|
|
@ -63,11 +70,15 @@ async fn get_tutor(
|
|||
State(state): State<ProfessionState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let tutor = sqlx::query_as::<_, TutorProfile>(
|
||||
let tutor = sqlx::query_as::<_, UserRoleProfile>(
|
||||
r#"
|
||||
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at
|
||||
FROM tutor_profiles
|
||||
WHERE id = $1
|
||||
SELECT id, user_id, role_key, display_name, bio, location,
|
||||
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 id = $1 AND role_key = 'tutor'
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ async fn update_profile(
|
|||
auth: AuthUser,
|
||||
Json(payload): Json<UpsertTutorProfilePayload>,
|
||||
) -> 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(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ async fn update_profile(
|
|||
auth: AuthUser,
|
||||
Json(payload): Json<UpsertUgcContentCreatorProfilePayload>,
|
||||
) -> 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(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -205,11 +205,23 @@ async fn activate_profile_after_final_approval(
|
|||
_ => 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!(
|
||||
"UPDATE {} SET status = 'APPROVED', updated_at = NOW() WHERE user_id = $1",
|
||||
"UPDATE {} SET verification_status = 'APPROVED', updated_at = NOW() WHERE id = $1",
|
||||
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(
|
||||
"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(()),
|
||||
};
|
||||
|
||||
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!(
|
||||
"UPDATE {} SET status = 'REJECTED', updated_at = NOW() WHERE user_id = $1",
|
||||
"UPDATE {} SET verification_status = 'REJECTED', updated_at = NOW() WHERE id = $1",
|
||||
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 {
|
||||
let display = role_key_to_display(&role_key);
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ async fn get_metrics(State(state): State<crate::AppState>) -> Json<DashboardMetr
|
|||
.unwrap_or(0);
|
||||
|
||||
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)
|
||||
.await
|
||||
|
|
@ -37,13 +37,7 @@ async fn get_metrics(State(state): State<crate::AppState>) -> Json<DashboardMetr
|
|||
let pending_approvals: i64 = sqlx::query_scalar::<_, i64>(
|
||||
r#"
|
||||
SELECT COUNT(*) FROM (
|
||||
SELECT id FROM company_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'
|
||||
SELECT id FROM user_role_profiles WHERE status = 'PENDING_APPROVAL'
|
||||
) sub
|
||||
"#,
|
||||
)
|
||||
|
|
@ -132,9 +126,8 @@ async fn get_metrics(State(state): State<crate::AppState>) -> Json<DashboardMetr
|
|||
r#"
|
||||
SELECT r.id, r.title, r.status, r.created_at,
|
||||
u.full_name AS requester_name
|
||||
FROM requirements r
|
||||
LEFT JOIN customer_profiles cp ON cp.id = r.customer_id
|
||||
LEFT JOIN users u ON u.id = cp.user_id
|
||||
FROM leads r
|
||||
LEFT JOIN users u ON u.id = r.created_by_user_id
|
||||
WHERE r.status IN ('PENDING_APPROVAL', 'APPROVED')
|
||||
ORDER BY r.created_at DESC
|
||||
LIMIT 5
|
||||
|
|
|
|||
|
|
@ -162,11 +162,20 @@ async fn submit(
|
|||
};
|
||||
|
||||
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!(
|
||||
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())
|
||||
ON CONFLICT (user_id) DO UPDATE SET
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
"profileData" = EXCLUDED."profileData",
|
||||
verification_status = 'PENDING',
|
||||
submitted_at = NOW(),
|
||||
|
|
@ -176,7 +185,7 @@ async fn submit(
|
|||
);
|
||||
|
||||
sqlx::query(&query)
|
||||
.bind(auth.user_id)
|
||||
.bind(user_role_profile_id)
|
||||
.bind(&progress)
|
||||
.execute(&state.pool)
|
||||
.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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -161,12 +161,28 @@ async fn get_profile(
|
|||
};
|
||||
|
||||
let query = format!(
|
||||
r#"SELECT "profileData", verification_status FROM {} WHERE user_id = $1"#,
|
||||
r#"SELECT "profileData", verification_status FROM {} WHERE id = $1"#,
|
||||
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)
|
||||
.bind(auth.user_id)
|
||||
.bind(user_role_profile_id)
|
||||
.fetch_optional(&state.pool)
|
||||
.await
|
||||
{
|
||||
|
|
@ -252,16 +268,21 @@ async fn save_profile(
|
|||
|
||||
let query = format!(
|
||||
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())
|
||||
ON CONFLICT (user_id) DO UPDATE SET
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
"profileData" = EXCLUDED."profileData",
|
||||
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)
|
||||
.bind(auth.user_id)
|
||||
.bind(user_role_profile_id)
|
||||
.bind(&input.profile_data)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
|
|
@ -434,18 +455,8 @@ async fn fetch_saved_profile(
|
|||
};
|
||||
}
|
||||
|
||||
if let Some(table) = role_to_table(role_key) {
|
||||
let q = format!(r#"SELECT "profileData" FROM {} WHERE user_id = $1"#, table);
|
||||
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()));
|
||||
}
|
||||
if let Some(urp_id) = get_user_role_profile_id(&state.pool, user_id, role_key).await.ok().flatten() {
|
||||
return fetch_saved_profile_by_urp_id(state, urp_id, role_key).await;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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) {
|
||||
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
|
||||
);
|
||||
sqlx::query(&q)
|
||||
.bind(status)
|
||||
.bind(user_id)
|
||||
.bind(user_role_profile_id)
|
||||
.execute(&state.pool)
|
||||
.await
|
||||
.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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -123,11 +123,23 @@ async fn trigger_rejection(
|
|||
_ => 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!(
|
||||
"UPDATE {} SET status = 'REJECTED', updated_at = NOW() WHERE user_id = $1",
|
||||
"UPDATE {} SET verification_status = 'REJECTED', updated_at = NOW() WHERE id = $1",
|
||||
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
|
||||
if let Ok(user) = db::models::user::UserRepository::get_by_id(&state.pool, user_id).await {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
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 serde::Serialize;
|
||||
use uuid::Uuid;
|
||||
|
|
@ -7,6 +7,7 @@ use uuid::Uuid;
|
|||
#[derive(Serialize)]
|
||||
pub struct AdminVideoEditorList {
|
||||
pub id: Uuid,
|
||||
pub user_role_profile_id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub display_name: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
|
|
@ -16,10 +17,11 @@ pub struct AdminVideoEditorList {
|
|||
pub updated_at: chrono::DateTime<chrono::Utc>,
|
||||
}
|
||||
|
||||
impl From<VideoEditorProfile> for AdminVideoEditorList {
|
||||
fn from(p: VideoEditorProfile) -> Self {
|
||||
impl From<UserRoleProfile> for AdminVideoEditorList {
|
||||
fn from(p: UserRoleProfile) -> Self {
|
||||
Self {
|
||||
id: p.id,
|
||||
user_role_profile_id: p.id,
|
||||
user_id: p.user_id,
|
||||
display_name: p.display_name,
|
||||
bio: p.bio,
|
||||
|
|
@ -40,10 +42,15 @@ pub fn router() -> Router<ProfessionState> {
|
|||
async fn list_video_editors(
|
||||
State(state): State<ProfessionState>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let editors = sqlx::query_as::<_, VideoEditorProfile>(
|
||||
let editors = sqlx::query_as::<_, UserRoleProfile>(
|
||||
r#"
|
||||
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at
|
||||
FROM video_editor_profiles
|
||||
SELECT id, user_id, role_key, display_name, bio, location,
|
||||
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
|
||||
LIMIT 100
|
||||
"#,
|
||||
|
|
@ -60,11 +67,15 @@ async fn get_video_editor(
|
|||
State(state): State<ProfessionState>,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||
let editor = sqlx::query_as::<_, VideoEditorProfile>(
|
||||
let editor = sqlx::query_as::<_, UserRoleProfile>(
|
||||
r#"
|
||||
SELECT id, user_id, display_name, bio, location, custom_data, status, created_at, updated_at
|
||||
FROM video_editor_profiles
|
||||
WHERE id = $1
|
||||
SELECT id, user_id, role_key, display_name, bio, location,
|
||||
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 id = $1 AND role_key = 'video_editor'
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ async fn update_profile(
|
|||
auth: AuthUser,
|
||||
Json(payload): Json<UpsertVideoEditorProfilePayload>,
|
||||
) -> 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(),
|
||||
Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,8 @@ use chrono::Utc;
|
|||
use serde::Deserialize;
|
||||
use uuid::Uuid;
|
||||
use db::models::lead_request::{CreateLeadRequestPayload, LeadRequestRepository};
|
||||
use db::models::tracecoin_wallet::TracecoinWalletRepository;
|
||||
use db::models::requirement::RequirementRepository;
|
||||
use db::models::professional::{
|
||||
CreatePortfolioItemPayload,
|
||||
CreateServicePayload,
|
||||
|
|
@ -16,7 +18,7 @@ use db::models::professional::{
|
|||
UpdatePortfolioItemPayload,
|
||||
UpdateServicePayload,
|
||||
};
|
||||
use db::models::requirement::RequirementRepository;
|
||||
use db::models::user_role_profile::UserRoleProfileRepository;
|
||||
use crate::auth_middleware::AuthUser;
|
||||
use crate::ProfessionState;
|
||||
|
||||
|
|
@ -35,7 +37,10 @@ pub struct LeadRequestPayload {
|
|||
/// `profession_key` must be a `'static str` matching the role key, e.g. `"PHOTOGRAPHER"`.
|
||||
pub fn shared_routes(profession_key: &'static str) -> Router<ProfessionState> {
|
||||
Router::new()
|
||||
.route("/profile/submit", post(submit_for_verification))
|
||||
.route("/profile/submit", post({
|
||||
let pk = profession_key;
|
||||
move |state, auth| submit_for_verification(state, auth, pk)
|
||||
}))
|
||||
// ── Marketplace (Redis-cached) ────────────────────────────────────────
|
||||
.route(
|
||||
"/marketplace",
|
||||
|
|
@ -129,9 +134,10 @@ async fn send_lead_request(
|
|||
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 {
|
||||
Ok(p) => p,
|
||||
Err(_) => return (StatusCode::NOT_FOUND, "Professional profile not found").into_response(),
|
||||
let user_role_profile = match UserRoleProfileRepository::get_by_user_and_role(&state.pool, auth.user_id, profession_key).await {
|
||||
Ok(Some(p)) => p,
|
||||
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 {
|
||||
|
|
@ -152,7 +158,7 @@ async fn send_lead_request(
|
|||
// ── Deduplication: one lead per requirement per professional (24 h) ────────
|
||||
let duplicate = cache::lead::is_duplicate(
|
||||
&mut redis,
|
||||
&prof.id.to_string(),
|
||||
&user_role_profile.id.to_string(),
|
||||
&payload.requirement_id.to_string(),
|
||||
)
|
||||
.await
|
||||
|
|
@ -172,24 +178,23 @@ async fn send_lead_request(
|
|||
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,
|
||||
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();
|
||||
}
|
||||
|
||||
let db_payload = CreateLeadRequestPayload {
|
||||
requirement_id: req.id,
|
||||
professional_id: prof.id,
|
||||
user_role_profile_id: user_role_profile.id,
|
||||
expires_at: Utc::now() + chrono::Duration::hours(24),
|
||||
};
|
||||
|
||||
match LeadRequestRepository::create(&state.pool, db_payload).await {
|
||||
Ok(lead) => {
|
||||
let reserved = ProfessionalRepository::try_reserve_tracecoins(
|
||||
let reserved = TracecoinWalletRepository::try_reserve_tracecoins(
|
||||
&state.pool,
|
||||
auth.user_id,
|
||||
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
|
||||
let _ = cache::lead::mark_sent(
|
||||
&mut redis,
|
||||
&prof.id.to_string(),
|
||||
&user_role_profile.id.to_string(),
|
||||
&payload.requirement_id.to_string(),
|
||||
)
|
||||
.await;
|
||||
|
|
@ -427,9 +432,10 @@ async fn cancel_request(
|
|||
auth: AuthUser,
|
||||
Path(id): Path<Uuid>,
|
||||
) -> impl IntoResponse {
|
||||
let prof = match ProfessionalRepository::get_by_user_id(&state.pool, auth.user_id).await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return (StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "Professional profile not found" }))).into_response(),
|
||||
let user_role_profile = match UserRoleProfileRepository::get_by_user_and_role(&state.pool, auth.user_id, "PHOTOGRAPHER").await {
|
||||
Ok(Some(p)) => p,
|
||||
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 {
|
||||
|
|
@ -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(),
|
||||
};
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
|
@ -450,7 +456,7 @@ async fn cancel_request(
|
|||
|
||||
// Release reserved Tracecoins back to balance
|
||||
if lead.tracecoins_reserved > 0 {
|
||||
let _ = ProfessionalRepository::try_release_reserved_tracecoins(
|
||||
let _ = TracecoinWalletRepository::try_release_reserved_tracecoins(
|
||||
&state.pool,
|
||||
auth.user_id,
|
||||
lead.tracecoins_reserved,
|
||||
|
|
@ -470,51 +476,42 @@ async fn accepted_leads(
|
|||
auth: AuthUser,
|
||||
Query(q): Query<PaginationQuery>,
|
||||
) -> impl IntoResponse {
|
||||
let prof = match ProfessionalRepository::get_by_user_id(&state.pool, auth.user_id).await {
|
||||
Ok(p) => p,
|
||||
Err(_) => return (StatusCode::NOT_FOUND, Json(serde_json::json!({ "error": "Professional profile not found" }))).into_response(),
|
||||
let user_role_profile = match UserRoleProfileRepository::get_by_user_and_role(&state.pool, auth.user_id, "PHOTOGRAPHER").await {
|
||||
Ok(Some(p)) => p,
|
||||
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 limit = q.limit.unwrap_or(20).clamp(1, 100);
|
||||
let offset = (page - 1) * limit;
|
||||
|
||||
// Join lead_requests → requirements → customers → users to get full contact info
|
||||
let rows = sqlx::query(
|
||||
r#"
|
||||
SELECT
|
||||
lr.id AS lead_id,
|
||||
lr.id AS lead_request_id,
|
||||
lr.status,
|
||||
lr.requested_at,
|
||||
lr.resolved_at,
|
||||
r.id AS requirement_id,
|
||||
r.title AS requirement_title,
|
||||
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
|
||||
lr.tracecoins_reserved,
|
||||
lr.user_role_profile_id
|
||||
FROM lead_requests lr
|
||||
INNER JOIN requirements r ON r.id = lr.requirement_id
|
||||
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
|
||||
WHERE lr.user_role_profile_id = $1
|
||||
AND lr.status = 'ACCEPTED'
|
||||
ORDER BY lr.resolved_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
"#
|
||||
)
|
||||
.bind(prof.id)
|
||||
.bind(user_role_profile.id)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(&state.pool)
|
||||
.await;
|
||||
|
||||
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)
|
||||
.await
|
||||
.unwrap_or(0);
|
||||
|
|
@ -524,20 +521,12 @@ async fn accepted_leads(
|
|||
use sqlx::Row;
|
||||
let data: Vec<serde_json::Value> = rows.iter().map(|row| {
|
||||
serde_json::json!({
|
||||
"lead_id": row.get::<Uuid, _>("lead_id"),
|
||||
"status": row.get::<String, _>("status"),
|
||||
"requested_at": row.get::<chrono::DateTime<chrono::Utc>, _>("requested_at"),
|
||||
"lead_request_id": row.get::<Uuid, _>("lead_request_id"),
|
||||
"status": row.get::<String, _>("status"),
|
||||
"requested_at": row.get::<chrono::DateTime<chrono::Utc>, _>("requested_at"),
|
||||
"resolved_at": row.try_get::<chrono::DateTime<chrono::Utc>, _>("resolved_at").ok(),
|
||||
"requirement_id": row.get::<Uuid, _>("requirement_id"),
|
||||
"requirement_title": row.get::<String, _>("requirement_title"),
|
||||
"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(),
|
||||
}
|
||||
"tracecoins_reserved": row.get::<i32, _>("tracecoins_reserved"),
|
||||
"user_role_profile_id": row.get::<Uuid, _>("user_role_profile_id"),
|
||||
})
|
||||
}).collect();
|
||||
|
||||
|
|
@ -779,6 +768,7 @@ async fn wallet_invoice_detail(
|
|||
async fn submit_for_verification(
|
||||
State(state): State<ProfessionState>,
|
||||
auth: AuthUser,
|
||||
profession_key: &'static str,
|
||||
) -> impl IntoResponse {
|
||||
let prof = match ProfessionalRepository::get_by_user_id(&state.pool, auth.user_id).await {
|
||||
Ok(p) => p,
|
||||
|
|
@ -789,7 +779,7 @@ async fn submit_for_verification(
|
|||
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!({
|
||||
"status": profile.status,
|
||||
"message": "Profile submitted for verification"
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
use std::path::Path;
|
||||
use anyhow::{Context, Result};
|
||||
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
|
|
|
|||
|
|
@ -7,21 +7,18 @@ use uuid::Uuid;
|
|||
pub struct Application {
|
||||
pub id: Uuid,
|
||||
pub job_id: Uuid,
|
||||
pub job_seeker_id: Uuid,
|
||||
pub cover_letter: Option<String>,
|
||||
pub resume_url: Option<String>,
|
||||
pub status: String, // APPLIED, SHORTLISTED, INTERVIEW, OFFERED, HIRED, REJECTED, WITHDRAWN
|
||||
pub applicant_user_id: Uuid,
|
||||
pub cover_note: Option<String>,
|
||||
pub status: String,
|
||||
pub applied_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub contact_viewed: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CreateApplicationPayload {
|
||||
pub job_id: Uuid,
|
||||
pub job_seeker_id: Uuid,
|
||||
pub cover_letter: Option<String>,
|
||||
pub resume_url: Option<String>,
|
||||
pub applicant_user_id: Uuid,
|
||||
pub cover_note: Option<String>,
|
||||
}
|
||||
|
||||
pub struct ApplicationRepository;
|
||||
|
|
@ -33,15 +30,14 @@ impl ApplicationRepository {
|
|||
) -> Result<Application, sqlx::Error> {
|
||||
let app = sqlx::query_as::<_, Application>(
|
||||
r#"
|
||||
INSERT INTO applications (job_id, job_seeker_id, cover_letter, resume_url)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
INSERT INTO job_applications (job_id, applicant_user_id, cover_note)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING *
|
||||
"#,
|
||||
)
|
||||
.bind(payload.job_id)
|
||||
.bind(payload.job_seeker_id)
|
||||
.bind(payload.cover_letter)
|
||||
.bind(payload.resume_url)
|
||||
.bind(payload.applicant_user_id)
|
||||
.bind(payload.cover_note)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
|
|
@ -49,7 +45,7 @@ impl ApplicationRepository {
|
|||
}
|
||||
|
||||
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)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
|
|
@ -65,7 +61,7 @@ impl ApplicationRepository {
|
|||
let offset = (page - 1) * limit;
|
||||
let apps = sqlx::query_as::<_, Application>(
|
||||
r#"
|
||||
SELECT * FROM applications
|
||||
SELECT * FROM job_applications
|
||||
WHERE job_id = $1 AND ($2::VARCHAR IS NULL OR status = $2)
|
||||
ORDER BY applied_at DESC
|
||||
LIMIT $3 OFFSET $4
|
||||
|
|
@ -81,22 +77,22 @@ impl ApplicationRepository {
|
|||
Ok(apps)
|
||||
}
|
||||
|
||||
pub async fn list_by_job_seeker_id(
|
||||
pub async fn list_by_user_id(
|
||||
pool: &PgPool,
|
||||
job_seeker_id: Uuid,
|
||||
applicant_user_id: Uuid,
|
||||
page: i64,
|
||||
limit: i64,
|
||||
) -> Result<Vec<Application>, sqlx::Error> {
|
||||
let offset = (page - 1) * limit;
|
||||
let apps = sqlx::query_as::<_, Application>(
|
||||
r#"
|
||||
SELECT * FROM applications
|
||||
WHERE job_seeker_id = $1
|
||||
SELECT * FROM job_applications
|
||||
WHERE applicant_user_id = $1
|
||||
ORDER BY applied_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
"#,
|
||||
)
|
||||
.bind(job_seeker_id)
|
||||
.bind(applicant_user_id)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(pool)
|
||||
|
|
@ -110,7 +106,7 @@ impl ApplicationRepository {
|
|||
status: &str,
|
||||
) -> Result<Application, sqlx::Error> {
|
||||
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(id)
|
||||
|
|
@ -118,14 +114,4 @@ impl ApplicationRepository {
|
|||
.await?;
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,60 @@ pub struct UpsertCateringServiceProfilePayload {
|
|||
pub struct 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> {
|
||||
sqlx::query_as::<_, CateringServiceProfile>(
|
||||
r#"SELECT id, user_role_profile_id, business_name, cuisine_types, event_types,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,53 @@ pub struct UpsertDeveloperProfilePayload {
|
|||
pub struct 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> {
|
||||
sqlx::query_as::<_, DeveloperProfile>(
|
||||
r#"SELECT id, user_role_profile_id, tech_stack, experience_years, availability,
|
||||
|
|
|
|||
|
|
@ -30,6 +30,55 @@ pub struct UpsertFitnessTrainerProfilePayload {
|
|||
pub struct 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> {
|
||||
sqlx::query_as::<_, FitnessTrainerProfile>(
|
||||
r#"SELECT id, user_role_profile_id, disciplines, certifications, online_sessions,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,51 @@ pub struct UpsertGraphicDesignerProfilePayload {
|
|||
pub struct 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> {
|
||||
sqlx::query_as::<_, GraphicDesignerProfile>(
|
||||
r#"SELECT id, user_role_profile_id, design_tools, style_tags, brand_experience,
|
||||
|
|
|
|||
|
|
@ -6,21 +6,19 @@ use uuid::Uuid;
|
|||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct LeadRequest {
|
||||
pub id: Uuid,
|
||||
pub requirement_id: Uuid,
|
||||
pub professional_id: Uuid,
|
||||
pub status: String, // PENDING, ACCEPTED, REJECTED, EXPIRED, CANCELLED
|
||||
pub user_role_profile_id: Uuid,
|
||||
pub status: String,
|
||||
pub tracecoins_reserved: i32,
|
||||
pub expires_at: DateTime<Utc>,
|
||||
pub requested_at: DateTime<Utc>,
|
||||
pub resolved_at: Option<DateTime<Utc>>,
|
||||
pub professional_user_id: Option<Uuid>,
|
||||
pub remarks: Option<String>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CreateLeadRequestPayload {
|
||||
pub requirement_id: Uuid,
|
||||
pub professional_id: Uuid,
|
||||
pub user_role_profile_id: Uuid,
|
||||
pub expires_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
|
|
@ -33,13 +31,12 @@ impl LeadRequestRepository {
|
|||
) -> Result<LeadRequest, sqlx::Error> {
|
||||
let req = sqlx::query_as::<_, LeadRequest>(
|
||||
r#"
|
||||
INSERT INTO lead_requests (requirement_id, professional_id, expires_at)
|
||||
VALUES ($1, $2, $3)
|
||||
INSERT INTO lead_requests (user_role_profile_id, expires_at)
|
||||
VALUES ($1, $2)
|
||||
RETURNING *
|
||||
"#,
|
||||
)
|
||||
.bind(payload.requirement_id)
|
||||
.bind(payload.professional_id)
|
||||
.bind(payload.user_role_profile_id)
|
||||
.bind(payload.expires_at)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
|
@ -54,9 +51,9 @@ impl LeadRequestRepository {
|
|||
.await
|
||||
}
|
||||
|
||||
pub async fn list_by_requirement_id(
|
||||
pub async fn list_by_user_role_profile_id(
|
||||
pool: &PgPool,
|
||||
requirement_id: Uuid,
|
||||
user_role_profile_id: Uuid,
|
||||
page: i64,
|
||||
limit: i64,
|
||||
) -> Result<Vec<LeadRequest>, sqlx::Error> {
|
||||
|
|
@ -64,12 +61,12 @@ impl LeadRequestRepository {
|
|||
let reqs = sqlx::query_as::<_, LeadRequest>(
|
||||
r#"
|
||||
SELECT * FROM lead_requests
|
||||
WHERE requirement_id = $1
|
||||
WHERE user_role_profile_id = $1
|
||||
ORDER BY requested_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
"#,
|
||||
)
|
||||
.bind(requirement_id)
|
||||
.bind(user_role_profile_id)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(pool)
|
||||
|
|
|
|||
|
|
@ -28,6 +28,54 @@ pub struct UpsertMakeupArtistProfilePayload {
|
|||
pub struct 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> {
|
||||
sqlx::query_as::<_, MakeupArtistProfile>(
|
||||
r#"SELECT id, user_role_profile_id, specializations, kit_brands,
|
||||
|
|
|
|||
|
|
@ -26,4 +26,5 @@ pub mod department;
|
|||
pub mod designation;
|
||||
pub mod verification;
|
||||
pub mod user_role_profile;
|
||||
pub mod tracecoin_wallet;
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,57 @@ pub struct UpsertPhotographerProfilePayload {
|
|||
pub struct 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> {
|
||||
sqlx::query_as::<_, PhotographerProfile>(
|
||||
r#"SELECT id, user_role_profile_id, specialties, camera_brands,
|
||||
|
|
|
|||
|
|
@ -7,11 +7,7 @@ use uuid::Uuid;
|
|||
pub struct Professional {
|
||||
pub id: Uuid,
|
||||
pub user_id: Uuid,
|
||||
pub profession_key: String,
|
||||
pub display_name: String,
|
||||
pub location: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
pub extra_data_json: Option<serde_json::Value>,
|
||||
pub role_key: String,
|
||||
pub status: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
|
|
@ -22,20 +18,19 @@ use super::requirement::Requirement;
|
|||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct PortfolioItem {
|
||||
pub id: Uuid,
|
||||
pub professional_id: Uuid,
|
||||
pub user_role_profile_id: Uuid,
|
||||
pub title: String,
|
||||
pub description: Option<String>,
|
||||
pub tags: Option<Vec<String>>,
|
||||
pub display_order: Option<i32>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub user_id: Option<Uuid>,
|
||||
pub profession_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct Service {
|
||||
pub id: Uuid,
|
||||
pub professional_id: Uuid,
|
||||
pub user_role_profile_id: Uuid,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub price: i32,
|
||||
|
|
@ -43,8 +38,6 @@ pub struct Service {
|
|||
pub is_active: bool,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
pub user_id: Option<Uuid>,
|
||||
pub profession_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
|
|
@ -117,7 +110,7 @@ pub struct ProfessionalRepository;
|
|||
impl ProfessionalRepository {
|
||||
pub async fn get_by_user_id(pool: &PgPool, user_id: Uuid) -> Result<Professional, sqlx::Error> {
|
||||
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)
|
||||
.fetch_one(pool)
|
||||
|
|
@ -133,7 +126,7 @@ impl ProfessionalRepository {
|
|||
let offset = (page - 1) * limit;
|
||||
sqlx::query_as::<_, Requirement>(
|
||||
r#"
|
||||
SELECT * FROM requirements
|
||||
SELECT * FROM leads
|
||||
WHERE profession_key = $1 AND status = 'OPEN' AND (expires_at IS NULL OR expires_at > NOW())
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
|
|
@ -146,20 +139,20 @@ impl ProfessionalRepository {
|
|||
.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>(
|
||||
"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)
|
||||
.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>(
|
||||
"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)
|
||||
.await
|
||||
}
|
||||
|
|
@ -187,14 +180,14 @@ impl ProfessionalRepository {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_user_id_by_professional_id(
|
||||
pub async fn get_user_id_by_user_role_profile_id(
|
||||
pool: &PgPool,
|
||||
professional_id: Uuid,
|
||||
user_role_profile_id: Uuid,
|
||||
) -> Result<Option<Uuid>, sqlx::Error> {
|
||||
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)
|
||||
.await?;
|
||||
Ok(row)
|
||||
|
|
@ -202,17 +195,17 @@ impl ProfessionalRepository {
|
|||
|
||||
pub async fn create_portfolio_item(
|
||||
pool: &PgPool,
|
||||
professional_id: Uuid,
|
||||
user_role_profile_id: Uuid,
|
||||
payload: CreatePortfolioItemPayload,
|
||||
) -> Result<PortfolioItem, sqlx::Error> {
|
||||
sqlx::query_as::<_, PortfolioItem>(
|
||||
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[], '{}'))
|
||||
RETURNING *
|
||||
"#,
|
||||
)
|
||||
.bind(professional_id)
|
||||
.bind(user_role_profile_id)
|
||||
.bind(payload.title)
|
||||
.bind(payload.description)
|
||||
.bind(payload.tags)
|
||||
|
|
@ -222,7 +215,7 @@ impl ProfessionalRepository {
|
|||
|
||||
pub async fn update_portfolio_item(
|
||||
pool: &PgPool,
|
||||
professional_id: Uuid,
|
||||
user_role_profile_id: Uuid,
|
||||
id: Uuid,
|
||||
payload: UpdatePortfolioItemPayload,
|
||||
) -> Result<Option<PortfolioItem>, sqlx::Error> {
|
||||
|
|
@ -234,7 +227,7 @@ impl ProfessionalRepository {
|
|||
description = COALESCE($2, description),
|
||||
tags = COALESCE($3, tags),
|
||||
updated_at = NOW()
|
||||
WHERE id = $4 AND professional_id = $5
|
||||
WHERE id = $4 AND user_role_profile_id = $5
|
||||
RETURNING *
|
||||
"#,
|
||||
)
|
||||
|
|
@ -242,7 +235,7 @@ impl ProfessionalRepository {
|
|||
.bind(payload.description)
|
||||
.bind(payload.tags)
|
||||
.bind(id)
|
||||
.bind(professional_id)
|
||||
.bind(user_role_profile_id)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
Ok(row)
|
||||
|
|
@ -250,14 +243,14 @@ impl ProfessionalRepository {
|
|||
|
||||
pub async fn delete_portfolio_item(
|
||||
pool: &PgPool,
|
||||
professional_id: Uuid,
|
||||
user_role_profile_id: Uuid,
|
||||
id: Uuid,
|
||||
) -> Result<bool, sqlx::Error> {
|
||||
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(professional_id)
|
||||
.bind(user_role_profile_id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(result.rows_affected() > 0)
|
||||
|
|
@ -265,17 +258,17 @@ impl ProfessionalRepository {
|
|||
|
||||
pub async fn create_service(
|
||||
pool: &PgPool,
|
||||
professional_id: Uuid,
|
||||
user_role_profile_id: Uuid,
|
||||
payload: CreateServicePayload,
|
||||
) -> Result<Service, sqlx::Error> {
|
||||
sqlx::query_as::<_, Service>(
|
||||
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)
|
||||
RETURNING *
|
||||
"#,
|
||||
)
|
||||
.bind(professional_id)
|
||||
.bind(user_role_profile_id)
|
||||
.bind(payload.name)
|
||||
.bind(payload.description)
|
||||
.bind(payload.price)
|
||||
|
|
@ -286,7 +279,7 @@ impl ProfessionalRepository {
|
|||
|
||||
pub async fn update_service(
|
||||
pool: &PgPool,
|
||||
professional_id: Uuid,
|
||||
user_role_profile_id: Uuid,
|
||||
id: Uuid,
|
||||
payload: UpdateServicePayload,
|
||||
) -> Result<Option<Service>, sqlx::Error> {
|
||||
|
|
@ -300,7 +293,7 @@ impl ProfessionalRepository {
|
|||
duration_minutes = COALESCE($4, duration_minutes),
|
||||
is_active = COALESCE($5, is_active),
|
||||
updated_at = NOW()
|
||||
WHERE id = $6 AND professional_id = $7
|
||||
WHERE id = $6 AND user_role_profile_id = $7
|
||||
RETURNING *
|
||||
"#,
|
||||
)
|
||||
|
|
@ -310,7 +303,7 @@ impl ProfessionalRepository {
|
|||
.bind(payload.duration_minutes)
|
||||
.bind(payload.is_active)
|
||||
.bind(id)
|
||||
.bind(professional_id)
|
||||
.bind(user_role_profile_id)
|
||||
.fetch_optional(pool)
|
||||
.await?;
|
||||
Ok(row)
|
||||
|
|
@ -318,12 +311,12 @@ impl ProfessionalRepository {
|
|||
|
||||
pub async fn delete_service(
|
||||
pool: &PgPool,
|
||||
professional_id: Uuid,
|
||||
user_role_profile_id: Uuid,
|
||||
id: Uuid,
|
||||
) -> 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(professional_id)
|
||||
.bind(user_role_profile_id)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
Ok(result.rows_affected() > 0)
|
||||
|
|
@ -560,11 +553,13 @@ impl ProfessionalRepository {
|
|||
pub async fn submit_for_verification(
|
||||
pool: &PgPool,
|
||||
user_id: Uuid,
|
||||
profession_key: &str,
|
||||
) -> Result<Professional, sqlx::Error> {
|
||||
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(profession_key)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
|
|
@ -574,13 +569,14 @@ impl ProfessionalRepository {
|
|||
|
||||
let prof = sqlx::query_as::<_, Professional>(
|
||||
r#"
|
||||
UPDATE professionals
|
||||
UPDATE user_role_profiles
|
||||
SET status = 'PENDING_REVIEW', updated_at = NOW()
|
||||
WHERE user_id = $1
|
||||
RETURNING *
|
||||
WHERE user_id = $1 AND role_key = $2
|
||||
RETURNING id, user_id, role_key as profession_key, status, created_at, updated_at
|
||||
"#,
|
||||
)
|
||||
.bind(user_id)
|
||||
.bind(profession_key)
|
||||
.fetch_one(pool)
|
||||
.await?;
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ use uuid::Uuid;
|
|||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct Requirement {
|
||||
pub id: Uuid,
|
||||
pub customer_id: Uuid,
|
||||
pub profession_key: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
|
|
@ -14,7 +13,7 @@ pub struct Requirement {
|
|||
pub budget: Option<i32>,
|
||||
pub preferred_date: Option<chrono::NaiveDate>,
|
||||
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 request_count: i32,
|
||||
pub accepted_count: i32,
|
||||
|
|
@ -22,12 +21,13 @@ pub struct Requirement {
|
|||
pub approved_at: Option<DateTime<Utc>>,
|
||||
pub approved_by: Option<Uuid>,
|
||||
pub created_at: DateTime<Utc>,
|
||||
pub created_by_user_id: Option<Uuid>,
|
||||
pub required_date: Option<chrono::NaiveDate>,
|
||||
pub updated_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct CreateRequirementPayload {
|
||||
pub customer_id: Uuid,
|
||||
pub profession_key: String,
|
||||
pub title: String,
|
||||
pub description: String,
|
||||
|
|
@ -56,15 +56,14 @@ impl RequirementRepository {
|
|||
) -> Result<Requirement, sqlx::Error> {
|
||||
let req = sqlx::query_as::<_, Requirement>(
|
||||
r#"
|
||||
INSERT INTO requirements (
|
||||
customer_id, profession_key, title, description, location,
|
||||
INSERT INTO leads (
|
||||
profession_key, title, description, location,
|
||||
budget, preferred_date, extra_data_json
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
RETURNING *
|
||||
"#,
|
||||
)
|
||||
.bind(payload.customer_id)
|
||||
.bind(payload.profession_key)
|
||||
.bind(payload.title)
|
||||
.bind(payload.description)
|
||||
|
|
@ -79,28 +78,28 @@ impl RequirementRepository {
|
|||
}
|
||||
|
||||
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)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn list_by_customer_id(
|
||||
pub async fn list_by_user_id(
|
||||
pool: &PgPool,
|
||||
customer_id: Uuid,
|
||||
user_id: Uuid,
|
||||
page: i64,
|
||||
limit: i64,
|
||||
) -> Result<Vec<Requirement>, sqlx::Error> {
|
||||
let offset = (page - 1) * limit;
|
||||
let reqs = sqlx::query_as::<_, Requirement>(
|
||||
r#"
|
||||
SELECT * FROM requirements
|
||||
WHERE customer_id = $1
|
||||
SELECT * FROM leads
|
||||
WHERE created_by_user_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2 OFFSET $3
|
||||
"#,
|
||||
)
|
||||
.bind(customer_id)
|
||||
.bind(user_id)
|
||||
.bind(limit)
|
||||
.bind(offset)
|
||||
.fetch_all(pool)
|
||||
|
|
@ -116,7 +115,7 @@ impl RequirementRepository {
|
|||
) -> Result<Requirement, sqlx::Error> {
|
||||
let req = sqlx::query_as::<_, Requirement>(
|
||||
r#"
|
||||
UPDATE requirements SET
|
||||
UPDATE leads SET
|
||||
title = COALESCE($1, title),
|
||||
description = COALESCE($2, description),
|
||||
location = COALESCE($3, location),
|
||||
|
|
@ -147,7 +146,7 @@ impl RequirementRepository {
|
|||
status: &str,
|
||||
) -> Result<Requirement, sqlx::Error> {
|
||||
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(id)
|
||||
|
|
@ -157,7 +156,7 @@ impl RequirementRepository {
|
|||
}
|
||||
|
||||
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)
|
||||
.execute(pool)
|
||||
.await?;
|
||||
|
|
@ -166,7 +165,7 @@ impl RequirementRepository {
|
|||
|
||||
pub async fn increment_accepted_count(pool: &PgPool, id: Uuid) -> Result<(), sqlx::Error> {
|
||||
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)
|
||||
.execute(pool)
|
||||
|
|
@ -180,7 +179,7 @@ impl RequirementRepository {
|
|||
) -> Result<Requirement, sqlx::Error> {
|
||||
sqlx::query_as::<_, Requirement>(
|
||||
r#"
|
||||
UPDATE requirements
|
||||
UPDATE leads
|
||||
SET accepted_count = accepted_count + 1, updated_at = NOW()
|
||||
WHERE id = $1
|
||||
RETURNING *
|
||||
|
|
@ -198,7 +197,7 @@ impl RequirementRepository {
|
|||
) -> Result<Requirement, sqlx::Error> {
|
||||
sqlx::query_as::<_, Requirement>(
|
||||
r#"
|
||||
UPDATE requirements
|
||||
UPDATE leads
|
||||
SET status = 'OPEN', approved_at = NOW(), approved_by = $1, rejection_reason = NULL, updated_at = NOW()
|
||||
WHERE id = $2
|
||||
RETURNING *
|
||||
|
|
@ -217,7 +216,7 @@ impl RequirementRepository {
|
|||
) -> Result<Requirement, sqlx::Error> {
|
||||
sqlx::query_as::<_, Requirement>(
|
||||
r#"
|
||||
UPDATE requirements
|
||||
UPDATE leads
|
||||
SET status = 'REJECTED', rejection_reason = $1, approved_at = NULL, approved_by = NULL, updated_at = NOW()
|
||||
WHERE id = $2
|
||||
RETURNING *
|
||||
|
|
|
|||
|
|
@ -28,6 +28,53 @@ pub struct UpsertSocialMediaManagerProfilePayload {
|
|||
pub struct 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> {
|
||||
sqlx::query_as::<_, SocialMediaManagerProfile>(
|
||||
r#"SELECT id, user_role_profile_id, platforms, industries, content_types,
|
||||
|
|
|
|||
220
crates/db/src/models/tracecoin_wallet.rs
Normal file
220
crates/db/src/models/tracecoin_wallet.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -32,6 +32,58 @@ pub struct UpsertTutorProfilePayload {
|
|||
pub struct 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> {
|
||||
sqlx::query_as::<_, TutorProfile>(
|
||||
r#"SELECT id, user_role_profile_id, subjects, board_types, qualification,
|
||||
|
|
|
|||
|
|
@ -28,6 +28,53 @@ pub struct UpsertUgcContentCreatorProfilePayload {
|
|||
pub struct 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> {
|
||||
sqlx::query_as::<_, UgcContentCreatorProfile>(
|
||||
r#"SELECT id, user_role_profile_id, niche_tags, content_formats, platforms,
|
||||
|
|
|
|||
|
|
@ -26,6 +26,51 @@ pub struct UpsertVideoEditorProfilePayload {
|
|||
pub struct 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> {
|
||||
sqlx::query_as::<_, VideoEditorProfile>(
|
||||
r#"SELECT id, user_role_profile_id, software_skills, style_tags, turnaround_days,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue