mirror of
https://github.com/Traceworks2023/nxtgauge-backend-rust.git
synced 2026-06-11 14:00:01 +00:00
fix(e2e): 14 bug fixes across users, leads, gateway, KB, and reviews
DB:
- Add niche_tags column to ugc_content_creator_profiles (was blocking UGC service)
- Add turnaround_days and fix user_role_profile_id NOT NULL for UGC
- leads/lead_requests tables (already created in session 1)
Code:
- Add UGC_CONTENT_CREATOR to is_professional_role() to auto-create user_role_profiles
- Fix onboarding INSERT to include user_id for photographer_profiles
- Fix send_lead_request_ai to use correct customer_user_id (was self-notifying)
- Add PATCH /api/leads/:id support + mount leads at /api/* for gateway compatibility
- Fix admin_list_cases query (WHERE was using wrong params)
- Fix admin_get_case query (was using list query instead of fetch-by-id)
- Add GET /api/me in profile.rs (moved from onboarding)
- Add KB articles by ID route /api/kb/articles/id/{id}
- Rewrite reviews handlers to match actual reviews table schema
- Add public reviews router GET /api/reviews
Gateway:
- Add /api/reviews route to users service
This commit is contained in:
parent
52e30a1b4b
commit
2c6d102205
17 changed files with 407 additions and 84 deletions
|
|
@ -83,7 +83,7 @@ impl Services {
|
||||||
|
|
||||||
|
|
||||||
fn resolve_upstream(&self, path: &str) -> Option<String> {
|
fn resolve_upstream(&self, path: &str) -> Option<String> {
|
||||||
// Auth, users, roles, notifications, runtime-config, config, KB, support
|
// Auth, users, roles, notifications, runtime-config, config, KB, support, reviews
|
||||||
if path.starts_with("/api/auth")
|
if path.starts_with("/api/auth")
|
||||||
|| path.starts_with("/api/users")
|
|| path.starts_with("/api/users")
|
||||||
|| path.starts_with("/api/v1/users")
|
|| path.starts_with("/api/v1/users")
|
||||||
|
|
@ -96,6 +96,7 @@ impl Services {
|
||||||
|| path.starts_with("/api/kb")
|
|| path.starts_with("/api/kb")
|
||||||
|| path.starts_with("/api/packages")
|
|| path.starts_with("/api/packages")
|
||||||
|| path.starts_with("/api/support")
|
|| path.starts_with("/api/support")
|
||||||
|
|| path.starts_with("/api/reviews")
|
||||||
|| path.starts_with("/api/admin/roles")
|
|| path.starts_with("/api/admin/roles")
|
||||||
|| path.starts_with("/api/admin/users")
|
|| path.starts_with("/api/admin/users")
|
||||||
|| path.starts_with("/api/admin/verifications")
|
|| path.starts_with("/api/admin/verifications")
|
||||||
|
|
|
||||||
|
|
@ -380,6 +380,7 @@ async fn send_lead_request_ai(
|
||||||
};
|
};
|
||||||
|
|
||||||
let expires_at = chrono::Utc::now() + chrono::Duration::hours(24);
|
let expires_at = chrono::Utc::now() + chrono::Duration::hours(24);
|
||||||
|
let customer_id = lead.2.clone();
|
||||||
|
|
||||||
let result = sqlx::query_as::<_, LeadRequestRow>(
|
let result = sqlx::query_as::<_, LeadRequestRow>(
|
||||||
r#"
|
r#"
|
||||||
|
|
@ -390,7 +391,7 @@ async fn send_lead_request_ai(
|
||||||
)
|
)
|
||||||
.bind(payload.lead_id)
|
.bind(payload.lead_id)
|
||||||
.bind(user_role_profile_id)
|
.bind(user_role_profile_id)
|
||||||
.bind(user_id)
|
.bind(&customer_id) // customer_user_id from the lead
|
||||||
.bind(tracecoins_cost)
|
.bind(tracecoins_cost)
|
||||||
.bind(&ai_message)
|
.bind(&ai_message)
|
||||||
.bind(expires_at)
|
.bind(expires_at)
|
||||||
|
|
@ -419,7 +420,7 @@ async fn send_lead_request_ai(
|
||||||
VALUES ($1, $2, $3, $4, $5)
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
"#
|
"#
|
||||||
)
|
)
|
||||||
.bind(user_id)
|
.bind(&customer_id) // notify the customer
|
||||||
.bind("AI Auto-Respond Sent")
|
.bind("AI Auto-Respond Sent")
|
||||||
.bind("Your AI-assisted response has been sent to the customer.")
|
.bind("Your AI-assisted response has been sent to the customer.")
|
||||||
.bind("LEAD_REQUEST")
|
.bind("LEAD_REQUEST")
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::State,
|
extract::State,
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
routing::{get, post},
|
routing::{get, post, patch},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use reqwest::Client;
|
use reqwest::Client;
|
||||||
|
|
@ -41,6 +41,14 @@ pub struct CreateLead {
|
||||||
pub profession_key: String,
|
pub profession_key: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct UpdateLead {
|
||||||
|
pub title: Option<String>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
pub location: Option<String>,
|
||||||
|
pub status: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
async fn list_leads(State(state): State<Arc<AppState>>) -> Result<Json<Vec<Lead>>, StatusCode> {
|
async fn list_leads(State(state): State<Arc<AppState>>) -> Result<Json<Vec<Lead>>, StatusCode> {
|
||||||
let leads = sqlx::query_as::<_, Lead>(
|
let leads = sqlx::query_as::<_, Lead>(
|
||||||
"SELECT id, title, description, location, profession_key, status, created_at FROM leads ORDER BY created_at DESC"
|
"SELECT id, title, description, location, profession_key, status, created_at FROM leads ORDER BY created_at DESC"
|
||||||
|
|
@ -94,6 +102,38 @@ async fn health() -> &'static str {
|
||||||
"Leads Service OK"
|
"Leads Service OK"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn update_lead(
|
||||||
|
State(state): State<Arc<AppState>>,
|
||||||
|
axum::extract::Path(id): axum::extract::Path<uuid::Uuid>,
|
||||||
|
Json(payload): Json<UpdateLead>,
|
||||||
|
) -> Result<Json<Lead>, StatusCode> {
|
||||||
|
let status = payload.status.as_deref().unwrap_or("OPEN");
|
||||||
|
|
||||||
|
let lead = sqlx::query_as::<_, Lead>(
|
||||||
|
r#"
|
||||||
|
UPDATE leads
|
||||||
|
SET title = COALESCE($1, title),
|
||||||
|
description = COALESCE($2, description),
|
||||||
|
location = COALESCE($3, location),
|
||||||
|
status = $4,
|
||||||
|
updated_at = NOW()
|
||||||
|
WHERE id = $5
|
||||||
|
RETURNING id, title, description, location, profession_key, status, created_at
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(&payload.title)
|
||||||
|
.bind(&payload.description)
|
||||||
|
.bind(&payload.location)
|
||||||
|
.bind(status)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_optional(&state.pool)
|
||||||
|
.await
|
||||||
|
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?
|
||||||
|
.ok_or(StatusCode::NOT_FOUND)?;
|
||||||
|
|
||||||
|
Ok(Json(lead))
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() {
|
||||||
tracing_subscriber::registry()
|
tracing_subscriber::registry()
|
||||||
|
|
@ -130,10 +170,13 @@ async fn main() {
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
.route("/health", get(health))
|
.route("/health", get(health))
|
||||||
|
.nest("/api", Router::new()
|
||||||
.route("/leads", get(list_leads))
|
.route("/leads", get(list_leads))
|
||||||
.route("/leads", post(create_lead))
|
.route("/leads", post(create_lead))
|
||||||
.route("/leads/{id}", get(get_lead))
|
.route("/leads/{id}", get(get_lead))
|
||||||
.nest("/api/lead-requests", lead_requests::router())
|
.route("/leads/{id}", patch(update_lead))
|
||||||
|
.nest("/lead-requests", lead_requests::router())
|
||||||
|
)
|
||||||
.layer(cors)
|
.layer(cors)
|
||||||
.with_state(state);
|
.with_state(state);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ pub fn public_router() -> Router<AppState> {
|
||||||
.route("/categories", get(public_list_categories))
|
.route("/categories", get(public_list_categories))
|
||||||
.route("/articles", get(public_list_articles))
|
.route("/articles", get(public_list_articles))
|
||||||
.route("/articles/{slug}", get(public_get_article))
|
.route("/articles/{slug}", get(public_get_article))
|
||||||
|
.route("/articles/id/{id}", get(public_get_article_by_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Admin CRUD routes
|
/// Admin CRUD routes
|
||||||
|
|
@ -346,6 +347,68 @@ async fn public_get_article(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Public: single article by ID ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
async fn public_get_article_by_id(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(id): Path<Uuid>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let row = sqlx::query_as::<_, PublicArticleRow>(
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
a.id, a.title, a.slug, a.summary, a.body, a.target_roles, a.tags,
|
||||||
|
a.updated_at,
|
||||||
|
c.name AS category_name, c.slug AS category_slug
|
||||||
|
FROM kb_articles a
|
||||||
|
JOIN kb_categories c ON c.id = a.category_id
|
||||||
|
WHERE a.id = $1 AND a.status = 'PUBLISHED' AND c.is_active = true
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(id)
|
||||||
|
.fetch_optional(&state.pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let pool = state.pool.clone();
|
||||||
|
tokio::spawn(async move {
|
||||||
|
let _ = sqlx::query("UPDATE kb_articles SET views = views + 1 WHERE id = $1")
|
||||||
|
.bind(id)
|
||||||
|
.execute(&pool)
|
||||||
|
.await;
|
||||||
|
});
|
||||||
|
|
||||||
|
match row {
|
||||||
|
Ok(Some(r)) => {
|
||||||
|
let role = derive_role(r.target_roles.as_deref().unwrap_or(&[]));
|
||||||
|
let dto = PublicArticleDto {
|
||||||
|
id: r.id,
|
||||||
|
slug: r.slug,
|
||||||
|
title: r.title,
|
||||||
|
summary: r.summary,
|
||||||
|
category_key: r.category_slug,
|
||||||
|
category: r.category_name,
|
||||||
|
role,
|
||||||
|
tags: r.tags,
|
||||||
|
updated_at: r.updated_at.to_rfc3339(),
|
||||||
|
content: r.body,
|
||||||
|
};
|
||||||
|
(StatusCode::OK, Json(dto)).into_response()
|
||||||
|
}
|
||||||
|
Ok(None) => (
|
||||||
|
StatusCode::NOT_FOUND,
|
||||||
|
Json(serde_json::json!({ "error": "Article not found" })),
|
||||||
|
)
|
||||||
|
.into_response(),
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to fetch KB article by id {}: {}", id, e);
|
||||||
|
(
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
|
Json(serde_json::json!({ "error": "Failed to fetch article" })),
|
||||||
|
)
|
||||||
|
.into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Admin: categories ─────────────────────────────────────────────────────────
|
// ── Admin: categories ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
async fn admin_list_categories(
|
async fn admin_list_categories(
|
||||||
|
|
|
||||||
|
|
@ -173,8 +173,8 @@ async fn submit(
|
||||||
|
|
||||||
let query = format!(
|
let query = format!(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO {} (id, custom_data, status, updated_at)
|
INSERT INTO {} (id, user_id, custom_data, status, updated_at)
|
||||||
VALUES ($1, $2, 'PENDING', NOW())
|
VALUES ($1, $2, $3, 'PENDING', NOW())
|
||||||
ON CONFLICT (id) DO UPDATE SET
|
ON CONFLICT (id) DO UPDATE SET
|
||||||
custom_data = EXCLUDED.custom_data,
|
custom_data = EXCLUDED.custom_data,
|
||||||
status = 'PENDING',
|
status = 'PENDING',
|
||||||
|
|
@ -185,6 +185,7 @@ async fn submit(
|
||||||
|
|
||||||
sqlx::query(&query)
|
sqlx::query(&query)
|
||||||
.bind(user_role_profile_id)
|
.bind(user_role_profile_id)
|
||||||
|
.bind(auth.user_id)
|
||||||
.bind(&progress)
|
.bind(&progress)
|
||||||
.execute(&state.pool)
|
.execute(&state.pool)
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ use axum::{
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use contracts::auth_middleware::AuthUser;
|
use contracts::auth_middleware::AuthUser;
|
||||||
use db::models::{role::RoleRepository, verification::VerificationRepository};
|
use db::models::{role::RoleRepository, user::UserRepository, verification::VerificationRepository};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
|
@ -19,8 +19,9 @@ pub fn router() -> Router<AppState> {
|
||||||
.route("/submit-for-verification", post(submit_for_verification))
|
.route("/submit-for-verification", post(submit_for_verification))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn me_verification_router() -> Router<AppState> {
|
pub fn me_router() -> Router<AppState> {
|
||||||
Router::new()
|
Router::new()
|
||||||
|
.route("/", get(get_me))
|
||||||
.route("/verification-status", get(verification_status))
|
.route("/verification-status", get(verification_status))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -557,3 +558,22 @@ async fn fetch_saved_profile_by_urp_id(
|
||||||
}
|
}
|
||||||
serde_json::Value::Object(Default::default())
|
serde_json::Value::Object(Default::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// GET /api/me — returns the authenticated user's basic info
|
||||||
|
pub async fn get_me(
|
||||||
|
auth: AuthUser,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
) -> Result<impl IntoResponse, (StatusCode, String)> {
|
||||||
|
let user = UserRepository::get_by_id(&state.pool, auth.user_id)
|
||||||
|
.await
|
||||||
|
.map_err(|_| (StatusCode::UNAUTHORIZED, "User not found".to_string()))?;
|
||||||
|
|
||||||
|
Ok(Json(serde_json::json!({
|
||||||
|
"id": user.id,
|
||||||
|
"email": user.email,
|
||||||
|
"firstName": user.first_name,
|
||||||
|
"lastName": user.last_name,
|
||||||
|
"activeRole": auth.claims.active_role,
|
||||||
|
"emailVerified": user.email_verified,
|
||||||
|
})))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
use crate::AppState;
|
use crate::AppState;
|
||||||
use axum::{
|
use axum::{
|
||||||
extract::{Path, State},
|
extract::{Path, Query, State},
|
||||||
http::StatusCode,
|
http::StatusCode,
|
||||||
response::IntoResponse,
|
response::IntoResponse,
|
||||||
routing::get,
|
routing::{get, post},
|
||||||
Json, Router,
|
Json, Router,
|
||||||
};
|
};
|
||||||
use contracts::auth_middleware::AuthUser;
|
use contracts::auth_middleware::AuthUser;
|
||||||
|
|
@ -18,35 +18,50 @@ pub fn admin_router() -> Router<AppState> {
|
||||||
.route("/{id}", axum::routing::patch(admin_update_review).delete(admin_delete_review))
|
.route("/{id}", axum::routing::patch(admin_update_review).delete(admin_delete_review))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn public_router() -> Router<AppState> {
|
||||||
|
Router::new()
|
||||||
|
.route("/", get(list_reviews))
|
||||||
|
.route("/professional/{professional_id}", get(list_reviews_by_professional))
|
||||||
|
}
|
||||||
|
|
||||||
// ── DTOs ──────────────────────────────────────────────────────────────────────
|
// ── DTOs ──────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(Serialize)]
|
||||||
struct ReviewDto {
|
struct ReviewDto {
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
subject_type: String,
|
professional_id: Uuid,
|
||||||
subject_id: Option<String>,
|
customer_id: Uuid,
|
||||||
reviewer_name: Option<String>,
|
|
||||||
reviewer_id: Option<Uuid>,
|
|
||||||
rating: i16,
|
rating: i16,
|
||||||
title: Option<String>,
|
|
||||||
comment: Option<String>,
|
comment: Option<String>,
|
||||||
status: String,
|
is_published: bool,
|
||||||
created_at: chrono::DateTime<chrono::Utc>,
|
created_at: chrono::DateTime<chrono::Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct PublicReviewDto {
|
||||||
|
id: Uuid,
|
||||||
|
professional_id: Uuid,
|
||||||
|
rating: i16,
|
||||||
|
comment: Option<String>,
|
||||||
|
created_at: String,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct CreateReviewBody {
|
struct CreateReviewBody {
|
||||||
subject_type: Option<String>,
|
lead_request_id: Uuid,
|
||||||
subject_id: Option<String>,
|
|
||||||
reviewer_name: Option<String>,
|
|
||||||
rating: i16,
|
rating: i16,
|
||||||
title: Option<String>,
|
|
||||||
comment: Option<String>,
|
comment: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct PatchReviewBody {
|
struct PatchReviewBody {
|
||||||
status: Option<String>,
|
is_published: Option<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct PublicListQuery {
|
||||||
|
page: Option<i64>,
|
||||||
|
limit: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── FromRow structs ──────────────────────────────────────────────────────────
|
// ── FromRow structs ──────────────────────────────────────────────────────────
|
||||||
|
|
@ -54,14 +69,12 @@ struct PatchReviewBody {
|
||||||
#[derive(sqlx::FromRow)]
|
#[derive(sqlx::FromRow)]
|
||||||
struct ReviewRow {
|
struct ReviewRow {
|
||||||
id: Uuid,
|
id: Uuid,
|
||||||
subject_type: String,
|
lead_request_id: Uuid,
|
||||||
subject_id: Option<String>,
|
customer_id: Uuid,
|
||||||
reviewer_name: Option<String>,
|
professional_id: Uuid,
|
||||||
reviewer_id: Option<Uuid>,
|
|
||||||
rating: i16,
|
rating: i16,
|
||||||
title: Option<String>,
|
|
||||||
comment: Option<String>,
|
comment: Option<String>,
|
||||||
status: String,
|
is_published: bool,
|
||||||
created_at: chrono::DateTime<chrono::Utc>,
|
created_at: chrono::DateTime<chrono::Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -75,14 +88,12 @@ async fn admin_list_reviews(
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
r.id,
|
r.id,
|
||||||
r.subject_type,
|
r.lead_request_id,
|
||||||
r.subject_id,
|
r.customer_id,
|
||||||
r.reviewer_name,
|
r.professional_id,
|
||||||
r.reviewer_user_id AS reviewer_id,
|
|
||||||
r.rating,
|
r.rating,
|
||||||
r.title,
|
|
||||||
r.comment,
|
r.comment,
|
||||||
r.status,
|
r.is_published,
|
||||||
r.created_at
|
r.created_at
|
||||||
FROM reviews r
|
FROM reviews r
|
||||||
ORDER BY r.created_at DESC
|
ORDER BY r.created_at DESC
|
||||||
|
|
@ -97,14 +108,11 @@ async fn admin_list_reviews(
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|r| ReviewDto {
|
.map(|r| ReviewDto {
|
||||||
id: r.id,
|
id: r.id,
|
||||||
subject_type: r.subject_type,
|
professional_id: r.professional_id,
|
||||||
subject_id: r.subject_id,
|
customer_id: r.customer_id,
|
||||||
reviewer_name: r.reviewer_name,
|
|
||||||
reviewer_id: r.reviewer_id,
|
|
||||||
rating: r.rating,
|
rating: r.rating,
|
||||||
title: r.title,
|
|
||||||
comment: r.comment,
|
comment: r.comment,
|
||||||
status: r.status,
|
is_published: r.is_published,
|
||||||
created_at: r.created_at,
|
created_at: r.created_at,
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|
@ -126,24 +134,19 @@ async fn admin_create_review(
|
||||||
return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "Rating must be 1-5" }))).into_response();
|
return (StatusCode::BAD_REQUEST, Json(serde_json::json!({ "error": "Rating must be 1-5" }))).into_response();
|
||||||
}
|
}
|
||||||
|
|
||||||
let subject_type = body.subject_type.unwrap_or_else(|| "PLATFORM".to_string());
|
|
||||||
let status = "PUBLISHED".to_string();
|
|
||||||
|
|
||||||
let row = sqlx::query_as::<_, ReviewRow>(
|
let row = sqlx::query_as::<_, ReviewRow>(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO reviews (subject_type, subject_id, reviewer_name, rating, title, comment, status)
|
INSERT INTO reviews (lead_request_id, customer_id, professional_id, rating, comment, is_published)
|
||||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
SELECT $1,
|
||||||
RETURNING id, subject_type, subject_id, reviewer_name, reviewer_user_id AS reviewer_id,
|
(SELECT id FROM customer_profiles WHERE user_id = (SELECT user_id FROM lead_requests WHERE id = $1)),
|
||||||
rating, title, comment, status, created_at
|
(SELECT user_role_profile_id FROM lead_requests WHERE id = $1),
|
||||||
|
$2, $3, true
|
||||||
|
RETURNING id, lead_request_id, customer_id, professional_id, rating, comment, is_published, created_at
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(&subject_type)
|
.bind(body.lead_request_id)
|
||||||
.bind(&body.subject_id)
|
|
||||||
.bind(&body.reviewer_name)
|
|
||||||
.bind(body.rating)
|
.bind(body.rating)
|
||||||
.bind(&body.title)
|
|
||||||
.bind(&body.comment)
|
.bind(&body.comment)
|
||||||
.bind(&status)
|
|
||||||
.fetch_one(&state.pool)
|
.fetch_one(&state.pool)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
|
@ -151,14 +154,11 @@ async fn admin_create_review(
|
||||||
Ok(r) => {
|
Ok(r) => {
|
||||||
let dto = ReviewDto {
|
let dto = ReviewDto {
|
||||||
id: r.id,
|
id: r.id,
|
||||||
subject_type: r.subject_type,
|
professional_id: r.professional_id,
|
||||||
subject_id: r.subject_id,
|
customer_id: r.customer_id,
|
||||||
reviewer_name: r.reviewer_name,
|
|
||||||
reviewer_id: r.reviewer_id,
|
|
||||||
rating: r.rating,
|
rating: r.rating,
|
||||||
title: r.title,
|
|
||||||
comment: r.comment,
|
comment: r.comment,
|
||||||
status: r.status,
|
is_published: r.is_published,
|
||||||
created_at: r.created_at,
|
created_at: r.created_at,
|
||||||
};
|
};
|
||||||
(StatusCode::CREATED, Json(serde_json::json!(dto))).into_response()
|
(StatusCode::CREATED, Json(serde_json::json!(dto))).into_response()
|
||||||
|
|
@ -176,13 +176,12 @@ async fn admin_update_review(
|
||||||
Path(id): Path<Uuid>,
|
Path(id): Path<Uuid>,
|
||||||
Json(body): Json<PatchReviewBody>,
|
Json(body): Json<PatchReviewBody>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
let status = body.status.as_deref().unwrap_or("PUBLISHED").to_string();
|
let is_published = body.is_published.unwrap_or(true);
|
||||||
|
|
||||||
let result = sqlx::query(
|
let result = sqlx::query(
|
||||||
"UPDATE reviews SET status = $1, updated_at = NOW() WHERE id = $2",
|
"UPDATE reviews SET is_published = $1, updated_at = NOW() WHERE id = $2",
|
||||||
)
|
)
|
||||||
.bind(&status)
|
.bind(is_published)
|
||||||
.bind(id)
|
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.execute(&state.pool)
|
.execute(&state.pool)
|
||||||
.await;
|
.await;
|
||||||
|
|
@ -220,3 +219,107 @@ async fn admin_delete_review(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Public handlers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
async fn list_reviews(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Query(q): Query<PublicListQuery>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let page = q.page.unwrap_or(1).max(1);
|
||||||
|
let limit = q.limit.unwrap_or(20).clamp(1, 100);
|
||||||
|
let offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
let rows = sqlx::query_as::<_, ReviewRow>(
|
||||||
|
r#"
|
||||||
|
SELECT id, lead_request_id, customer_id, professional_id, rating, comment, is_published, created_at
|
||||||
|
FROM reviews
|
||||||
|
WHERE is_published = true
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT $1 OFFSET $2
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(limit)
|
||||||
|
.bind(offset)
|
||||||
|
.fetch_all(&state.pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match rows {
|
||||||
|
Ok(rows) => {
|
||||||
|
let dtos: Vec<PublicReviewDto> = rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| PublicReviewDto {
|
||||||
|
id: r.id,
|
||||||
|
professional_id: r.professional_id,
|
||||||
|
rating: r.rating,
|
||||||
|
comment: r.comment,
|
||||||
|
created_at: r.created_at.to_rfc3339(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
(StatusCode::OK, Json(serde_json::json!({ "reviews": dtos }))).into_response()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to list public reviews: {e}");
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "Failed to load reviews" }))).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn list_reviews_by_professional(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Path(professional_id): Path<Uuid>,
|
||||||
|
Query(q): Query<PublicListQuery>,
|
||||||
|
) -> impl IntoResponse {
|
||||||
|
let page = q.page.unwrap_or(1).max(1);
|
||||||
|
let limit = q.limit.unwrap_or(20).clamp(1, 100);
|
||||||
|
let offset = (page - 1) * limit;
|
||||||
|
|
||||||
|
let rows = sqlx::query_as::<_, ReviewRow>(
|
||||||
|
r#"
|
||||||
|
SELECT id, lead_request_id, customer_id, professional_id, rating, comment, is_published, created_at
|
||||||
|
FROM reviews
|
||||||
|
WHERE professional_id = $1 AND is_published = true
|
||||||
|
ORDER BY created_at DESC
|
||||||
|
LIMIT $2 OFFSET $3
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(professional_id)
|
||||||
|
.bind(limit)
|
||||||
|
.bind(offset)
|
||||||
|
.fetch_all(&state.pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
match rows {
|
||||||
|
Ok(rows) => {
|
||||||
|
let avg: (f64,) = sqlx::query_as("SELECT COALESCE(AVG(rating), 0)::float FROM reviews WHERE professional_id = $1 AND is_published = true")
|
||||||
|
.bind(professional_id)
|
||||||
|
.fetch_one(&state.pool)
|
||||||
|
.await
|
||||||
|
.unwrap_or((0.0,));
|
||||||
|
let count: (i64,) = sqlx::query_scalar("SELECT COUNT(*) FROM reviews WHERE professional_id = $1 AND is_published = true")
|
||||||
|
.bind(professional_id)
|
||||||
|
.fetch_one(&state.pool)
|
||||||
|
.await
|
||||||
|
.unwrap_or((0,));
|
||||||
|
let dtos: Vec<PublicReviewDto> = rows
|
||||||
|
.into_iter()
|
||||||
|
.map(|r| PublicReviewDto {
|
||||||
|
id: r.id,
|
||||||
|
professional_id: r.professional_id,
|
||||||
|
rating: r.rating,
|
||||||
|
comment: r.comment,
|
||||||
|
created_at: r.created_at.to_rfc3339(),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
(StatusCode::OK, Json(serde_json::json!({
|
||||||
|
"reviews": dtos,
|
||||||
|
"averageRating": avg.0,
|
||||||
|
"totalCount": count.0
|
||||||
|
}))).into_response()
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
tracing::error!("Failed to list reviews for professional {professional_id}: {e}");
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "Failed to load reviews" }))).into_response()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -479,7 +479,7 @@ struct AdminTicketRow {
|
||||||
created_at: chrono::DateTime<chrono::Utc>,
|
created_at: chrono::DateTime<chrono::Utc>,
|
||||||
updated_at: chrono::DateTime<chrono::Utc>,
|
updated_at: chrono::DateTime<chrono::Utc>,
|
||||||
user_name: Option<String>,
|
user_name: Option<String>,
|
||||||
user_email: String,
|
user_email: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn admin_list_cases(
|
async fn admin_list_cases(
|
||||||
|
|
@ -503,7 +503,11 @@ async fn admin_list_cases(
|
||||||
CONCAT(u.first_name, ' ', u.last_name) AS user_name, u.email AS user_email
|
CONCAT(u.first_name, ' ', u.last_name) AS user_name, u.email AS user_email
|
||||||
FROM support_tickets t
|
FROM support_tickets t
|
||||||
LEFT JOIN users u ON u.id = t.user_id
|
LEFT JOIN users u ON u.id = t.user_id
|
||||||
WHERE t.id = $1
|
WHERE ($1 = '' OR t.status = $1)
|
||||||
|
AND ($2 = '' OR t.priority = $2)
|
||||||
|
AND ($3 = '' OR t.category = $3)
|
||||||
|
ORDER BY t.updated_at DESC
|
||||||
|
LIMIT $4 OFFSET $5
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(&status_filter)
|
.bind(&status_filter)
|
||||||
|
|
@ -526,7 +530,7 @@ async fn admin_list_cases(
|
||||||
.map(|r| {
|
.map(|r| {
|
||||||
// Use user info if available, fall back to requester fields
|
// Use user info if available, fall back to requester fields
|
||||||
let requester_name = r.requester_name.or(r.user_name);
|
let requester_name = r.requester_name.or(r.user_name);
|
||||||
let requester_email = r.requester_email.or(Some(r.user_email));
|
let requester_email = r.requester_email.or(r.user_email);
|
||||||
serde_json::json!({
|
serde_json::json!({
|
||||||
"id": r.id,
|
"id": r.id,
|
||||||
"title": r.subject,
|
"title": r.subject,
|
||||||
|
|
@ -642,11 +646,7 @@ async fn admin_get_case(
|
||||||
CONCAT(u.first_name, ' ', u.last_name) AS user_name, u.email AS user_email
|
CONCAT(u.first_name, ' ', u.last_name) AS user_name, u.email AS user_email
|
||||||
FROM support_tickets t
|
FROM support_tickets t
|
||||||
LEFT JOIN users u ON u.id = t.user_id
|
LEFT JOIN users u ON u.id = t.user_id
|
||||||
WHERE ($1 = '' OR t.status = $1)
|
WHERE t.id = $1
|
||||||
AND ($2 = '' OR t.priority = $2)
|
|
||||||
AND ($3 = '' OR t.category = $3)
|
|
||||||
ORDER BY t.updated_at DESC
|
|
||||||
LIMIT $4 OFFSET $5
|
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(id)
|
.bind(id)
|
||||||
|
|
@ -681,7 +681,7 @@ async fn admin_get_case(
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let requester_name = t.requester_name.or(t.user_name);
|
let requester_name = t.requester_name.or(t.user_name);
|
||||||
let requester_email = t.requester_email.or(Some(t.user_email));
|
let requester_email = t.requester_email.or(t.user_email);
|
||||||
|
|
||||||
(StatusCode::OK, Json(serde_json::json!({
|
(StatusCode::OK, Json(serde_json::json!({
|
||||||
"ticket": {
|
"ticket": {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use axum::{
|
||||||
};
|
};
|
||||||
use contracts::auth_middleware::AuthUser;
|
use contracts::auth_middleware::AuthUser;
|
||||||
use db::models::role::RoleRepository;
|
use db::models::role::RoleRepository;
|
||||||
|
use db::models::user_role_profile::UserRoleProfileRepository;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub fn router() -> Router<AppState> {
|
pub fn router() -> Router<AppState> {
|
||||||
|
|
@ -42,6 +43,7 @@ fn is_professional_role(role_key: &str) -> bool {
|
||||||
| "SOCIAL_MEDIA_MANAGER"
|
| "SOCIAL_MEDIA_MANAGER"
|
||||||
| "FITNESS_TRAINER"
|
| "FITNESS_TRAINER"
|
||||||
| "CATERING_SERVICES"
|
| "CATERING_SERVICES"
|
||||||
|
| "UGC_CONTENT_CREATOR"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -112,7 +114,14 @@ async fn register_role(
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?;
|
||||||
|
|
||||||
// Note: Professional profile creation is now handled upon successful submission of onboarding data.
|
// For professional/external roles, also create the user_role_profiles entry so
|
||||||
|
// downstream services (e.g. leads) can find the professional's profile.
|
||||||
|
if is_professional_role(&role_key) {
|
||||||
|
if let Err(e) = UserRoleProfileRepository::create(&state.pool, auth.user_id, &role_key).await {
|
||||||
|
tracing::warn!("Failed to create user_role_profiles entry for {}: {}", role_key, e);
|
||||||
|
// Non-fatal — the assignment is created; the profile row can be backfilled later.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Ok((
|
Ok((
|
||||||
StatusCode::OK,
|
StatusCode::OK,
|
||||||
|
|
|
||||||
|
|
@ -72,8 +72,7 @@ async fn main() {
|
||||||
.nest("/api/admin/approvals", handlers::approvals::router())
|
.nest("/api/admin/approvals", handlers::approvals::router())
|
||||||
.nest("/api/admin/verifications", handlers::verifications::router())
|
.nest("/api/admin/verifications", handlers::verifications::router())
|
||||||
// ── Me: Profile Status + Verification Status ──────────────────────
|
// ── Me: Profile Status + Verification Status ──────────────────────
|
||||||
.nest("/api/me", handlers::onboarding::me_router())
|
.nest("/api/me", handlers::profile::me_router())
|
||||||
.nest("/api/me", handlers::profile::me_verification_router())
|
|
||||||
// ── Profile (save + submit-for-verification) ──────────────────────
|
// ── Profile (save + submit-for-verification) ──────────────────────
|
||||||
.nest("/api/profile", handlers::profile::router())
|
.nest("/api/profile", handlers::profile::router())
|
||||||
// ── Onboarding State (legacy, kept for compatibility) ────────────
|
// ── Onboarding State (legacy, kept for compatibility) ────────────
|
||||||
|
|
@ -97,7 +96,8 @@ async fn main() {
|
||||||
.nest("/api/support/tickets", handlers::support::user_router())
|
.nest("/api/support/tickets", handlers::support::user_router())
|
||||||
// ── Support Tickets (admin) ───────────────────────────────────────
|
// ── Support Tickets (admin) ───────────────────────────────────────
|
||||||
.nest("/api/admin/support-cases", handlers::support::admin_router())
|
.nest("/api/admin/support-cases", handlers::support::admin_router())
|
||||||
// ── Reviews (admin) ───────────────────────────────────────────────
|
// ── Reviews (public + admin) ─────────────────────────────────────
|
||||||
|
.nest("/api/reviews", handlers::reviews::public_router())
|
||||||
.nest("/api/admin/reviews", handlers::reviews::admin_router())
|
.nest("/api/admin/reviews", handlers::reviews::admin_router())
|
||||||
// ── Coupons & Discounts (admin) ───────────────────────────────────
|
// ── Coupons & Discounts (admin) ───────────────────────────────────
|
||||||
.nest("/api/admin/coupons", handlers::coupons::coupons_router())
|
.nest("/api/admin/coupons", handlers::coupons::coupons_router())
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE IF EXISTS leads;
|
||||||
27
crates/db/migrations/20260610003321_create_leads.up.sql
Normal file
27
crates/db/migrations/20260610003321_create_leads.up.sql
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
-- Create the leads table (also called "requirements" in some contexts)
|
||||||
|
-- Required by RequirementRepository in crates/db/src/models/requirement.rs
|
||||||
|
CREATE TABLE IF NOT EXISTS leads (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
profession_key VARCHAR(100) NOT NULL,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT NOT NULL DEFAULT '',
|
||||||
|
location VARCHAR(255) NOT NULL DEFAULT '',
|
||||||
|
budget_inr INT,
|
||||||
|
required_date DATE,
|
||||||
|
extra_data_json JSONB,
|
||||||
|
status VARCHAR(50) NOT NULL DEFAULT 'DRAFT',
|
||||||
|
rejection_reason TEXT,
|
||||||
|
request_count INT NOT NULL DEFAULT 0,
|
||||||
|
accepted_count INT NOT NULL DEFAULT 0,
|
||||||
|
expires_at TIMESTAMPTZ,
|
||||||
|
approved_at TIMESTAMPTZ,
|
||||||
|
approved_by UUID,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
created_by_user_id UUID,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_leads_status ON leads(status);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_leads_profession_key ON leads(profession_key);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_leads_created_by_user_id ON leads(created_by_user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_leads_status_profession ON leads(status, profession_key);
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
DROP TABLE IF EXISTS lead_requests;
|
||||||
|
|
@ -0,0 +1,21 @@
|
||||||
|
-- Create the lead_requests table for professional responses to leads
|
||||||
|
CREATE TABLE IF NOT EXISTS lead_requests (
|
||||||
|
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||||
|
lead_id UUID NOT NULL REFERENCES leads(id) ON DELETE CASCADE,
|
||||||
|
user_role_profile_id UUID NOT NULL,
|
||||||
|
customer_user_id UUID NOT NULL,
|
||||||
|
status VARCHAR(50) NOT NULL DEFAULT 'PENDING',
|
||||||
|
tracecoins_reserved INT NOT NULL DEFAULT 0,
|
||||||
|
message TEXT,
|
||||||
|
expires_at TIMESTAMPTZ NOT NULL,
|
||||||
|
accepted_at TIMESTAMPTZ,
|
||||||
|
rejected_at TIMESTAMPTZ,
|
||||||
|
rejected_reason TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_lead_requests_lead_id ON lead_requests(lead_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_lead_requests_user_role_profile_id ON lead_requests(user_role_profile_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_lead_requests_customer_user_id ON lead_requests(customer_user_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_lead_requests_status ON lead_requests(status);
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE ugc_content_creator_profiles DROP COLUMN IF EXISTS niche_tags;
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
-- Fix UGC Content Creator schema: rename content_niches→niche_tags (matches Rust model)
|
||||||
|
ALTER TABLE ugc_content_creator_profiles
|
||||||
|
ADD COLUMN IF NOT EXISTS niche_tags TEXT[] DEFAULT '{}';
|
||||||
|
|
||||||
|
UPDATE ugc_content_creator_profiles
|
||||||
|
SET niche_tags = COALESCE(content_niches, '{}')
|
||||||
|
WHERE niche_tags IS NULL;
|
||||||
|
|
@ -6,20 +6,41 @@ use uuid::Uuid;
|
||||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||||
pub struct LeadRequest {
|
pub struct LeadRequest {
|
||||||
pub id: Uuid,
|
pub id: Uuid,
|
||||||
pub user_role_profile_id: Uuid,
|
pub lead_id: Option<Uuid>,
|
||||||
|
pub user_role_profile_id: Option<Uuid>,
|
||||||
|
pub professional_user_id: Option<Uuid>,
|
||||||
pub status: String,
|
pub status: String,
|
||||||
pub tracecoins_reserved: i32,
|
pub tracecoins_reserved: i32,
|
||||||
|
pub remarks: Option<String>,
|
||||||
pub expires_at: DateTime<Utc>,
|
pub expires_at: DateTime<Utc>,
|
||||||
pub requested_at: DateTime<Utc>,
|
pub requested_at: DateTime<Utc>,
|
||||||
pub resolved_at: Option<DateTime<Utc>>,
|
pub resolved_at: Option<DateTime<Utc>>,
|
||||||
pub remarks: Option<String>,
|
pub created_at: DateTime<Utc>,
|
||||||
pub updated_at: DateTime<Utc>,
|
pub updated_at: DateTime<Utc>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct CreateLeadRequestPayload {
|
pub struct CreateLeadRequestPayload {
|
||||||
|
pub lead_id: Uuid,
|
||||||
pub user_role_profile_id: Uuid,
|
pub user_role_profile_id: Uuid,
|
||||||
pub expires_at: DateTime<Utc>,
|
pub professional_user_id: Uuid,
|
||||||
|
pub remarks: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CreateLeadRequestPayload {
|
||||||
|
pub fn new(
|
||||||
|
lead_id: Uuid,
|
||||||
|
user_role_profile_id: Uuid,
|
||||||
|
professional_user_id: Uuid,
|
||||||
|
remarks: Option<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
lead_id,
|
||||||
|
user_role_profile_id,
|
||||||
|
professional_user_id,
|
||||||
|
remarks,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LeadRequestRepository;
|
pub struct LeadRequestRepository;
|
||||||
|
|
@ -31,13 +52,15 @@ impl LeadRequestRepository {
|
||||||
) -> Result<LeadRequest, sqlx::Error> {
|
) -> Result<LeadRequest, sqlx::Error> {
|
||||||
let req = sqlx::query_as::<_, LeadRequest>(
|
let req = sqlx::query_as::<_, LeadRequest>(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO lead_requests (user_role_profile_id, expires_at)
|
INSERT INTO lead_requests (lead_id, user_role_profile_id, professional_user_id, remarks)
|
||||||
VALUES ($1, $2)
|
VALUES ($1, $2, $3, $4)
|
||||||
RETURNING *
|
RETURNING *
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
|
.bind(payload.lead_id)
|
||||||
.bind(payload.user_role_profile_id)
|
.bind(payload.user_role_profile_id)
|
||||||
.bind(payload.expires_at)
|
.bind(payload.professional_user_id)
|
||||||
|
.bind(&payload.remarks)
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
|
@ -92,6 +115,7 @@ impl LeadRequestRepository {
|
||||||
.bind(id)
|
.bind(id)
|
||||||
.fetch_one(pool)
|
.fetch_one(pool)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
Ok(req)
|
Ok(req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue