From 319b384f0a286ace38b0ac3f0602ae46d459b6f5 Mon Sep 17 00:00:00 2001 From: Tracewebstudio Dev Date: Wed, 10 Jun 2026 16:19:46 +0200 Subject: [PATCH] fix(session1): customer list_requests path arg, external-role by-key endpoint, RuntimeRoleDetail type --- apps/customers/src/handlers.rs | 11 ++- apps/users/src/handlers/external_roles.rs | 99 +++++++++++++++++++++-- crates/contracts/src/profession_shared.rs | 14 ++-- 3 files changed, 108 insertions(+), 16 deletions(-) diff --git a/apps/customers/src/handlers.rs b/apps/customers/src/handlers.rs index f5edabf..6d5881f 100644 --- a/apps/customers/src/handlers.rs +++ b/apps/customers/src/handlers.rs @@ -222,8 +222,7 @@ async fn submit_requirement( async fn list_requests( State(state): State, - Path(id): Path, - _auth: AuthUser, + auth: AuthUser, Query(q): Query, ) -> impl IntoResponse { let page = q.page.unwrap_or(1); @@ -233,12 +232,12 @@ async fn list_requests( let rows_result = sqlx::query_as::<_, db::models::lead_request::LeadRequest>( r#" SELECT * FROM lead_requests - WHERE user_role_profile_id = $1 + WHERE professional_user_id = $1 ORDER BY requested_at DESC LIMIT $2 OFFSET $3 "# ) - .bind(id) + .bind(auth.user_id) .bind(limit) .bind(offset) .fetch_all(&state.pool) @@ -271,7 +270,7 @@ async fn approve_request( Ok(updated) => { match TracecoinWalletRepository::try_debit_reserved_tracecoins( &state.pool, - lead.user_role_profile_id, + lead.user_role_profile_id.unwrap(), lead.tracecoins_reserved, lead.id, ).await { @@ -307,7 +306,7 @@ async fn reject_request( Ok(updated) => { match TracecoinWalletRepository::try_release_reserved_tracecoins( &state.pool, - lead.user_role_profile_id, + lead.user_role_profile_id.unwrap(), lead.tracecoins_reserved, lead.id, "LEAD_REJECTED", diff --git a/apps/users/src/handlers/external_roles.rs b/apps/users/src/handlers/external_roles.rs index 4a03970..806c7ec 100644 --- a/apps/users/src/handlers/external_roles.rs +++ b/apps/users/src/handlers/external_roles.rs @@ -3,7 +3,7 @@ use axum::{ extract::{Path, Query, State}, http::StatusCode, response::IntoResponse, - routing::get, + routing::{get, patch}, Json, Router, }; use serde::{Deserialize, Serialize}; @@ -14,6 +14,8 @@ use contracts::auth_middleware::{AuthUser, require_admin}; pub fn router() -> Router { Router::new() .route("/", get(list_external_roles).post(create_external_role)) + .route("/by-key/{role_key}", get(get_external_role_by_key)) + .route("/by-key/{role_key}", patch(update_external_role_by_key)) .route("/{id}", get(get_external_role).put(update_external_role).delete(delete_external_role)) } @@ -247,6 +249,60 @@ async fn get_external_role( })) } +async fn get_external_role_by_key( + auth: AuthUser, + State(state): State, + Path(role_key): Path, +) -> Result { + if let Err(_e) = require_admin(&auth) { + return Err((StatusCode::FORBIDDEN, "Forbidden".to_string())); + } + let row = sqlx::query_as::<_, ExternalRoleDetailRow>( + r#" + SELECT r.id, r.name, r.key as code, r.audience, r.is_active, r.created_at, + rc.updated_at as updated_at, rc.config_json as config_json + FROM roles r + LEFT JOIN role_runtime_configs rc ON rc.role_id = r.id AND rc.is_active = true + WHERE r.key = $1 AND r.audience = 'EXTERNAL' + "#, + ) + .bind(&role_key) + .fetch_optional(&state.pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))? + .ok_or((StatusCode::NOT_FOUND, "External role not found".to_string()))?; + + Ok(Json(ExternalRoleDetail { + id: row.id, + name: row.name, + code: row.code, + audience: row.audience, + is_active: row.is_active, + runtime: row.config_json.unwrap_or_else(|| serde_json::json!({})), + created_at: row.created_at, + updated_at: row.updated_at, + })) +} + +async fn update_external_role_by_key( + auth: AuthUser, + State(state): State, + Path(role_key): Path, + Json(payload): Json, +) -> Result { + if let Err(_e) = require_admin(&auth) { + return Err((StatusCode::FORBIDDEN, "Forbidden".to_string())); + } + let row: (Uuid,) = sqlx::query_as("SELECT id FROM roles WHERE key = $1 AND audience = 'EXTERNAL'") + .bind(&role_key) + .fetch_optional(&state.pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))? + .ok_or((StatusCode::NOT_FOUND, "External role not found".to_string()))?; + + update_external_role_impl(&state, row.0, payload).await +} + #[derive(Deserialize)] struct CreateExternalRolePayload { name: String, @@ -340,6 +396,14 @@ async fn update_external_role( if let Err(_e) = require_admin(&auth) { return Err((StatusCode::FORBIDDEN, "Forbidden".to_string())); } + update_external_role_impl(&state, id, payload).await +} + +async fn update_external_role_impl( + state: &AppState, + role_id: Uuid, + payload: UpdateExternalRolePayload, +) -> Result { if payload.name.is_some() || payload.is_active.is_some() { sqlx::query( r#" @@ -351,7 +415,7 @@ async fn update_external_role( ) .bind(payload.name) .bind(payload.is_active) - .bind(id) + .bind(role_id) .execute(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; @@ -364,7 +428,7 @@ async fn update_external_role( WHERE role_id = $1 AND is_active = true "#, ) - .bind(id) + .bind(role_id) .execute(&state.pool) .await .ok(); @@ -379,13 +443,38 @@ async fn update_external_role( ) "#, ) - .bind(id) + .bind(role_id) .bind(runtime) .execute(&state.pool) .await .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))?; } - get_external_role(auth, State(state), Path(id)).await + // Return the updated role detail + let row = sqlx::query_as::<_, ExternalRoleDetailRow>( + r#" + SELECT r.id, r.name, r.key as code, r.audience, r.is_active, r.created_at, + rc.updated_at as updated_at, rc.config_json as config_json + FROM roles r + LEFT JOIN role_runtime_configs rc ON rc.role_id = r.id AND rc.is_active = true + WHERE r.id = $1 AND r.audience = 'EXTERNAL' + "#, + ) + .bind(role_id) + .fetch_optional(&state.pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("DB error: {e}")))? + .ok_or((StatusCode::NOT_FOUND, "External role not found".to_string()))?; + + Ok(Json(ExternalRoleDetail { + id: row.id, + name: row.name, + code: row.code, + audience: row.audience, + is_active: row.is_active, + runtime: row.config_json.unwrap_or_else(|| serde_json::json!({})), + created_at: row.created_at, + updated_at: row.updated_at, + })) } async fn delete_external_role( diff --git a/crates/contracts/src/profession_shared.rs b/crates/contracts/src/profession_shared.rs index ae0286a..66de4c9 100644 --- a/crates/contracts/src/profession_shared.rs +++ b/crates/contracts/src/profession_shared.rs @@ -32,6 +32,8 @@ pub struct PaginationQuery { #[derive(Deserialize)] pub struct LeadRequestPayload { pub requirement_id: Uuid, + #[serde(default)] + pub message: Option, } /// Build the shared Router that every profession service merges into its own Router. @@ -189,10 +191,12 @@ async fn send_lead_request( return (StatusCode::PAYMENT_REQUIRED, "Insufficient Tracecoin balance").into_response(); } - let db_payload = CreateLeadRequestPayload { - user_role_profile_id: user_role_profile.id, - expires_at: Utc::now() + chrono::Duration::hours(24), - }; + let db_payload = CreateLeadRequestPayload::new( + req.id, + user_role_profile.id, + auth.user_id, + payload.message.clone(), + ); match LeadRequestRepository::create(&state.pool, db_payload).await { Ok(lead) => { @@ -456,7 +460,7 @@ async fn cancel_request( Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": e.to_string() }))).into_response(), }; - if lead.user_role_profile_id != user_role_profile.id { + if lead.user_role_profile_id != Some(user_role_profile.id) { return (StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "Access denied" }))).into_response(); }