Add public packages endpoint and register in gateway

- pricing.rs: public GET /api/packages?role= for user-facing package list
- main.rs: nest /api/packages public route
- gateway: route /api/packages to users service

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Ashwin Kumar 2026-04-02 18:11:44 +02:00
parent d900c361d8
commit 2312f5dfdc
3 changed files with 67 additions and 0 deletions

View file

@ -79,6 +79,7 @@ impl Services {
|| path.starts_with("/api/notifications")
|| path.starts_with("/api/config")
|| path.starts_with("/api/kb")
|| path.starts_with("/api/packages")
|| path.starts_with("/api/support")
|| path.starts_with("/api/admin/kb")
|| path.starts_with("/api/admin/support-cases")

View file

@ -12,6 +12,12 @@ use uuid::Uuid;
// ── Routers ───────────────────────────────────────────────────────────────────
/// Public (user-facing) — list active packages for their role
pub fn public_packages_router() -> Router<AppState> {
Router::new().route("/", get(public_list_packages))
}
/// Admin CRUD
pub fn packages_router() -> Router<AppState> {
Router::new()
.route("/", get(list_packages).post(create_package))
@ -69,6 +75,64 @@ struct DateRangeQuery {
// ── Package handlers ──────────────────────────────────────────────────────────
#[derive(Deserialize)]
struct PackageQuery {
role: Option<String>,
}
async fn public_list_packages(
State(state): State<AppState>,
Query(params): Query<PackageQuery>,
) -> impl IntoResponse {
let rows = if let Some(role) = params.role {
sqlx::query!(
r#"
SELECT id, name, role_key, package_type, tracecoins_amount, price_inr, description, is_active
FROM pricing_packages
WHERE is_active = true AND (role_key = $1 OR role_key = 'ALL')
ORDER BY price_inr
"#,
role
)
.fetch_all(&state.pool)
.await
} else {
sqlx::query!(
r#"
SELECT id, name, role_key, package_type, tracecoins_amount, price_inr, description, is_active
FROM pricing_packages
WHERE is_active = true
ORDER BY role_key, price_inr
"#
)
.fetch_all(&state.pool)
.await
};
match rows {
Ok(rows) => {
let dtos: Vec<PackageDto> = rows
.into_iter()
.map(|r| PackageDto {
id: r.id,
name: r.name,
role_key: r.role_key,
package_type: r.package_type,
tracecoins_amount: r.tracecoins_amount,
price_inr: r.price_inr,
description: r.description,
is_active: r.is_active,
})
.collect();
(StatusCode::OK, Json(serde_json::json!({ "packages": dtos }))).into_response()
}
Err(e) => {
tracing::error!("Failed to list packages: {e}");
(StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({ "error": "Failed to load packages" }))).into_response()
}
}
}
async fn list_packages(
_auth: AuthUser,
State(state): State<AppState>,

View file

@ -90,6 +90,8 @@ async fn main() {
// ── Coupons & Discounts (admin) ───────────────────────────────────
.nest("/api/admin/coupons", handlers::coupons::coupons_router())
.nest("/api/admin/discounts", handlers::coupons::discounts_router())
// ── Tracecoin Packages (public) ───────────────────────────────────
.nest("/api/packages", handlers::pricing::public_packages_router())
// ── Tracecoin Packages & Reports (admin) ──────────────────────────
.nest("/api/admin/tracecoin-packages", handlers::pricing::packages_router())
.nest("/api/admin/reports", handlers::pricing::reports_router())