nxtgauge-backend-rust/crates/db/src/models/professional.rs
Tracewebstudio Dev c433ab5fed 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
2026-04-13 00:29:44 +02:00

585 lines
16 KiB
Rust

use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use sqlx::{FromRow, PgPool};
use uuid::Uuid;
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct Professional {
pub id: Uuid,
pub user_id: Uuid,
pub role_key: String,
pub status: String,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
use super::requirement::Requirement;
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct PortfolioItem {
pub 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>,
}
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct Service {
pub id: Uuid,
pub user_role_profile_id: Uuid,
pub name: String,
pub description: Option<String>,
pub price: i32,
pub duration_minutes: Option<i32>,
pub is_active: bool,
pub created_at: DateTime<Utc>,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct Wallet {
pub id: Uuid,
pub user_id: Uuid,
pub balance: i32,
pub reserved: i32,
pub updated_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct TracecoinLedgerEntry {
pub id: Uuid,
pub wallet_id: Uuid,
pub r#type: String,
pub amount: i32,
pub reason: String,
pub reference_id: Option<Uuid>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct Invoice {
pub id: Uuid,
pub payment_id: Uuid,
pub user_id: Uuid,
pub invoice_number: String,
pub subtotal: i32,
pub gst_amount: i32,
pub total: i32,
pub status: String,
pub issued_at: DateTime<Utc>,
pub file_url: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreatePortfolioItemPayload {
pub title: String,
pub description: Option<String>,
pub tags: Option<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UpdatePortfolioItemPayload {
pub title: Option<String>,
pub description: Option<String>,
pub tags: Option<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct CreateServicePayload {
pub name: String,
pub description: Option<String>,
pub price: i32,
pub duration_minutes: Option<i32>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct UpdateServicePayload {
pub name: Option<String>,
pub description: Option<String>,
pub price: Option<i32>,
pub duration_minutes: Option<i32>,
pub is_active: Option<bool>,
}
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 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)
.await
}
pub async fn get_marketplace(
pool: &PgPool,
profession_key: &str,
page: i64,
limit: i64,
) -> Result<Vec<Requirement>, sqlx::Error> {
let offset = (page - 1) * limit;
sqlx::query_as::<_, Requirement>(
r#"
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
"#,
)
.bind(profession_key)
.bind(limit)
.bind(offset)
.fetch_all(pool)
.await
}
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 user_role_profile_id = $1 ORDER BY display_order, created_at DESC",
)
.bind(user_role_profile_id)
.fetch_all(pool)
.await
}
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 user_role_profile_id = $1 AND is_active = true ORDER BY name ASC",
)
.bind(user_role_profile_id)
.fetch_all(pool)
.await
}
pub async fn get_wallet(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, balance, reserved)
VALUES ($1, 0, 0)
ON CONFLICT (user_id) DO NOTHING
"#,
)
.bind(user_id)
.execute(pool)
.await?;
Ok(())
}
pub async fn get_user_id_by_user_role_profile_id(
pool: &PgPool,
user_role_profile_id: Uuid,
) -> Result<Option<Uuid>, sqlx::Error> {
let row = sqlx::query_scalar::<_, Uuid>(
"SELECT user_id FROM user_role_profiles WHERE id = $1",
)
.bind(user_role_profile_id)
.fetch_optional(pool)
.await?;
Ok(row)
}
pub async fn create_portfolio_item(
pool: &PgPool,
user_role_profile_id: Uuid,
payload: CreatePortfolioItemPayload,
) -> Result<PortfolioItem, sqlx::Error> {
sqlx::query_as::<_, PortfolioItem>(
r#"
INSERT INTO portfolio_items (user_role_profile_id, title, description, tags)
VALUES ($1, $2, $3, COALESCE($4::text[], '{}'))
RETURNING *
"#,
)
.bind(user_role_profile_id)
.bind(payload.title)
.bind(payload.description)
.bind(payload.tags)
.fetch_one(pool)
.await
}
pub async fn update_portfolio_item(
pool: &PgPool,
user_role_profile_id: Uuid,
id: Uuid,
payload: UpdatePortfolioItemPayload,
) -> Result<Option<PortfolioItem>, sqlx::Error> {
let row = sqlx::query_as::<_, PortfolioItem>(
r#"
UPDATE portfolio_items
SET
title = COALESCE($1, title),
description = COALESCE($2, description),
tags = COALESCE($3, tags),
updated_at = NOW()
WHERE id = $4 AND user_role_profile_id = $5
RETURNING *
"#,
)
.bind(payload.title)
.bind(payload.description)
.bind(payload.tags)
.bind(id)
.bind(user_role_profile_id)
.fetch_optional(pool)
.await?;
Ok(row)
}
pub async fn delete_portfolio_item(
pool: &PgPool,
user_role_profile_id: Uuid,
id: Uuid,
) -> Result<bool, sqlx::Error> {
let result = sqlx::query(
"DELETE FROM portfolio_items WHERE id = $1 AND user_role_profile_id = $2",
)
.bind(id)
.bind(user_role_profile_id)
.execute(pool)
.await?;
Ok(result.rows_affected() > 0)
}
pub async fn create_service(
pool: &PgPool,
user_role_profile_id: Uuid,
payload: CreateServicePayload,
) -> Result<Service, sqlx::Error> {
sqlx::query_as::<_, Service>(
r#"
INSERT INTO services (user_role_profile_id, name, description, price, duration_minutes)
VALUES ($1, $2, $3, $4, $5)
RETURNING *
"#,
)
.bind(user_role_profile_id)
.bind(payload.name)
.bind(payload.description)
.bind(payload.price)
.bind(payload.duration_minutes)
.fetch_one(pool)
.await
}
pub async fn update_service(
pool: &PgPool,
user_role_profile_id: Uuid,
id: Uuid,
payload: UpdateServicePayload,
) -> Result<Option<Service>, sqlx::Error> {
let row = sqlx::query_as::<_, Service>(
r#"
UPDATE services
SET
name = COALESCE($1, name),
description = COALESCE($2, description),
price = COALESCE($3, price),
duration_minutes = COALESCE($4, duration_minutes),
is_active = COALESCE($5, is_active),
updated_at = NOW()
WHERE id = $6 AND user_role_profile_id = $7
RETURNING *
"#,
)
.bind(payload.name)
.bind(payload.description)
.bind(payload.price)
.bind(payload.duration_minutes)
.bind(payload.is_active)
.bind(id)
.bind(user_role_profile_id)
.fetch_optional(pool)
.await?;
Ok(row)
}
pub async fn delete_service(
pool: &PgPool,
user_role_profile_id: Uuid,
id: Uuid,
) -> Result<bool, sqlx::Error> {
let result = sqlx::query("DELETE FROM services WHERE id = $1 AND user_role_profile_id = $2")
.bind(id)
.bind(user_role_profile_id)
.execute(pool)
.await?;
Ok(result.rows_affected() > 0)
}
pub async fn list_wallet_ledger(
pool: &PgPool,
user_id: Uuid,
page: i64,
limit: i64,
) -> Result<Vec<TracecoinLedgerEntry>, sqlx::Error> {
let offset = (page - 1) * limit;
sqlx::query_as::<_, TracecoinLedgerEntry>(
r#"
SELECT l.*
FROM tracecoin_ledger l
INNER JOIN tracecoin_wallets w ON w.id = l.wallet_id
WHERE w.user_id = $1
ORDER BY l.created_at DESC
LIMIT $2 OFFSET $3
"#,
)
.bind(user_id)
.bind(limit)
.bind(offset)
.fetch_all(pool)
.await
}
pub async fn list_wallet_invoices(
pool: &PgPool,
user_id: Uuid,
page: i64,
limit: i64,
) -> Result<Vec<Invoice>, sqlx::Error> {
let offset = (page - 1) * limit;
sqlx::query_as::<_, Invoice>(
r#"
SELECT *
FROM invoices
WHERE user_id = $1
ORDER BY issued_at DESC
LIMIT $2 OFFSET $3
"#,
)
.bind(user_id)
.bind(limit)
.bind(offset)
.fetch_all(pool)
.await
}
pub async fn get_invoice_by_id_for_user(
pool: &PgPool,
user_id: Uuid,
id: Uuid,
) -> Result<Option<Invoice>, sqlx::Error> {
sqlx::query_as::<_, Invoice>(
"SELECT * FROM invoices WHERE id = $1 AND user_id = $2",
)
.bind(id)
.bind(user_id)
.fetch_optional(pool)
.await
}
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, 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.balance < amount {
tx.rollback().await?;
return Ok(false);
}
sqlx::query(
r#"
UPDATE tracecoin_wallets
SET balance = 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, type, amount, reason, 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, type, amount, reason, 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, balance = 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, type, amount, reason, 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)
}
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 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?;
if prof.status == "PENDING_REVIEW" || prof.status == "APPROVED" {
return Err(sqlx::Error::Protocol(format!("Professional profile is already {}", prof.status).into()));
}
let prof = sqlx::query_as::<_, Professional>(
r#"
UPDATE user_role_profiles
SET status = 'PENDING_REVIEW', updated_at = NOW()
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?;
Ok(prof)
}
}