diff --git a/Cargo.lock b/Cargo.lock index c13f0ad..73bac3e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -599,6 +599,12 @@ dependencies = [ "fastrand", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.22.1" @@ -958,6 +964,18 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + [[package]] name = "crypto-common" version = "0.1.7" @@ -986,6 +1004,33 @@ dependencies = [ "cmov", ] +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures 0.2.17", + "curve25519-dalek-derive", + "digest 0.10.7", + "fiat-crypto", + "rustc_version", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "customers" version = "0.1.0" @@ -1116,6 +1161,44 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" +[[package]] +name = "ecdsa" +version = "0.16.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + +[[package]] +name = "ed25519" +version = "2.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" +dependencies = [ + "pkcs8", + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9" +dependencies = [ + "curve25519-dalek", + "ed25519", + "serde", + "sha2 0.10.9", + "subtle", + "zeroize", +] + [[package]] name = "either" version = "1.16.0" @@ -1125,6 +1208,27 @@ dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.13.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "hkdf", + "pem-rfc7468", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "email" version = "0.1.0" @@ -1228,6 +1332,22 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" +[[package]] +name = "ff" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1434,6 +1554,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -1495,6 +1616,17 @@ dependencies = [ "uuid", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + [[package]] name = "h2" version = "0.3.27" @@ -2048,11 +2180,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc" dependencies = [ "base64", + "ed25519-dalek", "getrandom 0.2.17", + "hmac 0.12.1", "js-sys", + "p256", + "p384", "pem", + "rand 0.8.6", + "rsa", "serde", "serde_json", + "sha2 0.10.9", "signature", "simple_asn1", "zeroize", @@ -2451,6 +2590,30 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" +[[package]] +name = "p256" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + +[[package]] +name = "p384" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe42f1670a52a47d448f14b6a5c61dd78fce51856e68edaa38f7ae3a46b8d6b6" +dependencies = [ + "ecdsa", + "elliptic-curve", + "primeorder", + "sha2 0.10.9", +] + [[package]] name = "parking" version = "2.2.1" @@ -2632,6 +2795,15 @@ dependencies = [ "syn", ] +[[package]] +name = "primeorder" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6" +dependencies = [ + "elliptic-curve", +] + [[package]] name = "proc-macro2" version = "1.0.106" @@ -2909,6 +3081,16 @@ dependencies = [ "webpki-roots 1.0.7", ] +[[package]] +name = "rfc6979" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" +dependencies = [ + "hmac 0.12.1", + "subtle", +] + [[package]] name = "ring" version = "0.17.14" @@ -3080,6 +3262,20 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "security-framework" version = "3.7.0" diff --git a/apps/payments/src/main.rs b/apps/payments/src/main.rs index 7e5c7c8..8908333 100644 --- a/apps/payments/src/main.rs +++ b/apps/payments/src/main.rs @@ -133,7 +133,7 @@ async fn create_order( sqlx::query( r#" - INSERT INTO payments (user_id, package_id, razorpay_order_id, amount, tracecoins_credited, status) + INSERT INTO payments (user_id, package_id, razorpay_order_id, amount_inr, tracecoins_credited, status) VALUES ($1, $2, $3, $4, $5, 'PENDING') "#, ) @@ -248,8 +248,8 @@ async fn verify_payment( { sqlx::query( r#" - INSERT INTO tracecoin_ledger (wallet_id, transaction_type, amount, balance_after, reference_type, reference_id, description) - VALUES ($1, 'CREDIT', $2, $2, 'PAYMENT', $3, 'Package purchase') + INSERT INTO tracecoin_ledger (wallet_id, transaction_type, amount, reference_type, reference_id) + VALUES ($1, 'CREDIT', $2, 'PAYMENT', $3) "#, ) .bind(wallet_id) @@ -262,7 +262,7 @@ async fn verify_payment( let _ = sqlx::query( r#" - INSERT INTO notifications (user_id, title, body, notification_type, reference_id) + INSERT INTO notifications (user_id, title, body, type, reference_id) VALUES ($1, $2, $3, $4, $5) "#, ) diff --git a/apps/payments/src/packages.rs b/apps/payments/src/packages.rs index 90355de..3242d66 100644 --- a/apps/payments/src/packages.rs +++ b/apps/payments/src/packages.rs @@ -14,6 +14,7 @@ use uuid::Uuid; pub struct PackageTypeQuery { pub package_type: Option, pub applicable_role: Option, + pub role: Option, pub active_only: Option, } @@ -29,48 +30,37 @@ pub struct CreatePackageRequest { pub name: String, pub description: Option, pub package_type: String, - pub applicable_roles: Vec, + pub role_key: Option, + pub applicable_roles: Option>, pub tracecoins_amount: i32, - pub price: i32, - pub duration_days: Option, - pub valid_from: Option>, - pub valid_until: Option>, - pub is_promotional: Option, + pub price: Option, + pub price_inr: Option, pub is_active: Option, - pub features: Option, } #[derive(Debug, Deserialize)] pub struct UpdatePackageRequest { pub name: Option, pub description: Option, + pub role_key: Option, + pub applicable_roles: Option>, pub tracecoins_amount: Option, pub price: Option, - pub duration_days: Option, - pub valid_from: Option>, - pub valid_until: Option>, - pub is_promotional: Option, + pub price_inr: Option, pub is_active: Option, - pub features: Option, } #[derive(Debug, FromRow)] pub struct PricingPackageRow { pub id: Uuid, pub name: String, - pub description: Option, + pub role_key: String, pub package_type: String, - pub applicable_roles: Vec, pub tracecoins_amount: i32, - pub price: i32, - pub duration_days: Option, - pub valid_from: Option>, - pub valid_until: Option>, - pub is_promotional: bool, + pub price_inr: i32, + pub description: Option, pub is_active: bool, - pub features: Option, pub created_at: chrono::DateTime, - pub updated_at: chrono::DateTime, } #[derive(Debug, Serialize)] @@ -78,10 +68,12 @@ pub struct PricingPackageResponse { pub id: Uuid, pub name: String, pub description: Option, - pub package_type: String, + pub role_key: String, pub applicable_roles: Vec, + pub package_type: String, pub tracecoins_amount: i32, pub price: i32, + pub price_inr: i32, pub duration_days: Option, pub valid_from: Option>, pub valid_until: Option>, @@ -96,33 +88,58 @@ pub struct PricingPackageResponse { impl From for PricingPackageResponse { fn from(row: PricingPackageRow) -> Self { - let now = chrono::Utc::now(); - let is_expired = row.valid_until.map(|v| v < now).unwrap_or(false); - let is_not_started = row.valid_from.map(|v| v > now).unwrap_or(false); - let is_available = row.is_active && !is_expired && !is_not_started; - - PricingPackageResponse { + Self { id: row.id, name: row.name, description: row.description, + role_key: row.role_key.clone(), + applicable_roles: vec![row.role_key], package_type: row.package_type, - applicable_roles: row.applicable_roles, tracecoins_amount: row.tracecoins_amount, - price: row.price, - duration_days: row.duration_days, - valid_from: row.valid_from, - valid_until: row.valid_until, - is_promotional: row.is_promotional, + price: row.price_inr, + price_inr: row.price_inr, + duration_days: None, + valid_from: None, + valid_until: None, + is_promotional: false, is_active: row.is_active, - features: row.features, + features: None, created_at: row.created_at, - updated_at: row.updated_at, - is_available, - is_expired, + updated_at: row.created_at, + is_available: row.is_active, + is_expired: false, } } } +fn normalize_role_key(role_key: Option, applicable_roles: Option>) -> Result { + if let Some(role) = role_key { + let cleaned = role.trim().to_uppercase(); + if !cleaned.is_empty() { + return Ok(cleaned); + } + } + + if let Some(roles) = applicable_roles { + if let Some(role) = roles.into_iter().map(|role| role.trim().to_uppercase()).find(|role| !role.is_empty()) { + return Ok(role); + } + } + + Err("role_key is required".to_string()) +} + +fn package_query(base_where: &str, order_by: &str) -> String { + format!( + r#" + SELECT id, name, role_key, package_type, tracecoins_amount, price_inr, description, is_active, created_at + FROM pricing_packages + {base_where} + {order_by} + "# + ) +} + pub fn router() -> Router { Router::new() .route("/", get(list_packages)) @@ -138,83 +155,67 @@ async fn list_packages( State(state): State, Query(q): Query, ) -> impl IntoResponse { - let page = q.page.unwrap_or(1); - let limit = q.limit.unwrap_or(20).min(100); + 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 search = q.search.unwrap_or_default().trim().to_string(); - let search_filter = q.search - .as_ref() - .map(|s| format!("AND (name ILIKE '%{}%' OR description ILIKE '%{}%')", s.replace('\'', "''"), s.replace('\'', "''"))) - .unwrap_or_default(); - - let packages = sqlx::query_as::<_, PricingPackageRow>( + let rows = sqlx::query_as::<_, PricingPackageRow>( &format!( - r#" - SELECT id, name, description, package_type, applicable_roles, - tracecoins_amount, price, duration_days, valid_from, valid_until, - is_promotional, is_active, features, created_at, updated_at - FROM pricing_packages - WHERE 1=1 {} - ORDER BY created_at DESC - LIMIT {} OFFSET {} - "#, - search_filter, limit, offset - ) + "{} LIMIT $2 OFFSET $3", + package_query( + "WHERE ($1 = '' OR name ILIKE '%' || $1 || '%' OR COALESCE(description, '') ILIKE '%' || $1 || '%')", + "ORDER BY created_at DESC" + ) + ), ) + .bind(&search) + .bind(limit) + .bind(offset) .fetch_all(&state.pool) .await; - let packages = match packages { - Ok(p) => p, + let rows = match rows { + Ok(rows) => rows, Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), }; - let total: (i64,) = match sqlx::query_as( - &format!( - "SELECT COUNT(*) FROM pricing_packages WHERE 1=1 {}", - search_filter - ) + let total: i64 = match sqlx::query_scalar( + "SELECT COUNT(*) FROM pricing_packages WHERE ($1 = '' OR name ILIKE '%' || $1 || '%' OR COALESCE(description, '') ILIKE '%' || $1 || '%')", ) + .bind(&search) .fetch_one(&state.pool) .await { - Ok(t) => t, + Ok(total) => total, Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), }; - let packages: Vec = packages.into_iter().map(|p| p.into()).collect(); + let packages: Vec = rows.into_iter().map(Into::into).collect(); (StatusCode::OK, Json(serde_json::json!({ "data": packages, + "packages": packages, "pagination": { "page": page, "limit": limit, - "total": total.0, - "pages": (total.0 as f64 / limit as f64).ceil() as i64 + "total": total, + "pages": (total as f64 / limit as f64).ceil() as i64 } - }))).into_response() + }))) + .into_response() } async fn get_package( State(state): State, Path(id): Path, ) -> impl IntoResponse { - match sqlx::query_as::<_, PricingPackageRow>( - r#" - SELECT id, name, description, package_type, applicable_roles, - tracecoins_amount, price, duration_days, valid_from, valid_until, - is_promotional, is_active, features, created_at, updated_at - FROM pricing_packages WHERE id = $1 - "# - ) - .bind(id) - .fetch_optional(&state.pool) - .await + match sqlx::query_as::<_, PricingPackageRow>(&package_query("WHERE id = $1", "")) + .bind(id) + .fetch_optional(&state.pool) + .await { - Ok(Some(pkg)) => { - let response: PricingPackageResponse = pkg.into(); - (StatusCode::OK, Json(response)).into_response() - } + Ok(Some(pkg)) => (StatusCode::OK, Json(PricingPackageResponse::from(pkg))).into_response(), Ok(None) => (StatusCode::NOT_FOUND, "Package not found").into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } @@ -224,37 +225,31 @@ async fn create_package( State(state): State, Json(payload): Json, ) -> impl IntoResponse { + let role_key = match normalize_role_key(payload.role_key, payload.applicable_roles) { + Ok(role_key) => role_key, + Err(message) => return (StatusCode::BAD_REQUEST, message).into_response(), + }; + let price_inr = payload.price_inr.or(payload.price).unwrap_or(0); + let result = sqlx::query_as::<_, PricingPackageRow>( r#" - INSERT INTO pricing_packages (name, description, package_type, applicable_roles, - tracecoins_amount, price, duration_days, valid_from, valid_until, - is_promotional, is_active, features) - VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) - RETURNING id, name, description, package_type, applicable_roles, - tracecoins_amount, price, duration_days, valid_from, valid_until, - is_promotional, is_active, features, created_at, updated_at - "# + INSERT INTO pricing_packages (name, role_key, package_type, tracecoins_amount, price_inr, description, is_active) + VALUES ($1, $2, $3, $4, $5, $6, $7) + RETURNING id, name, role_key, package_type, tracecoins_amount, price_inr, description, is_active, created_at + "#, ) .bind(&payload.name) - .bind(&payload.description) + .bind(&role_key) .bind(&payload.package_type) - .bind(&payload.applicable_roles) .bind(payload.tracecoins_amount) - .bind(payload.price) - .bind(payload.duration_days) - .bind(payload.valid_from) - .bind(payload.valid_until) - .bind(payload.is_promotional.unwrap_or(false)) + .bind(price_inr) + .bind(&payload.description) .bind(payload.is_active.unwrap_or(true)) - .bind(payload.features) .fetch_one(&state.pool) .await; match result { - Ok(pkg) => { - let response: PricingPackageResponse = pkg.into(); - (StatusCode::CREATED, Json(response)).into_response() - } + Ok(pkg) => (StatusCode::CREATED, Json(PricingPackageResponse::from(pkg))).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } @@ -264,58 +259,47 @@ async fn update_package( Path(id): Path, Json(payload): Json, ) -> impl IntoResponse { - let existing = sqlx::query_as::<_, PricingPackageRow>( - "SELECT * FROM pricing_packages WHERE id = $1" - ) - .bind(id) - .fetch_optional(&state.pool) - .await; - - let _existing = match existing { - Ok(Some(e)) => e, + let current = match sqlx::query_as::<_, PricingPackageRow>(&package_query("WHERE id = $1", "")) + .bind(id) + .fetch_optional(&state.pool) + .await + { + Ok(Some(pkg)) => pkg, Ok(None) => return (StatusCode::NOT_FOUND, "Package not found").into_response(), Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), }; + let role_key = match normalize_role_key(payload.role_key, payload.applicable_roles) { + Ok(role_key) => role_key, + Err(_) => current.role_key.clone(), + }; + let price_inr = payload.price_inr.or(payload.price).unwrap_or(current.price_inr); + let updated = sqlx::query_as::<_, PricingPackageRow>( r#" UPDATE pricing_packages SET name = COALESCE($2, name), - description = COALESCE($3, description), - tracecoins_amount = COALESCE($4, tracecoins_amount), - price = COALESCE($5, price), - duration_days = COALESCE($6, duration_days), - valid_from = COALESCE($7, valid_from), - valid_until = COALESCE($8, valid_until), - is_promotional = COALESCE($9, is_promotional), - is_active = COALESCE($10, is_active), - features = COALESCE($11, features), - updated_at = NOW() + role_key = $3, + description = COALESCE($4, description), + tracecoins_amount = COALESCE($5, tracecoins_amount), + price_inr = $6, + is_active = COALESCE($7, is_active) WHERE id = $1 - RETURNING id, name, description, package_type, applicable_roles, - tracecoins_amount, price, duration_days, valid_from, valid_until, - is_promotional, is_active, features, created_at, updated_at - "# + RETURNING id, name, role_key, package_type, tracecoins_amount, price_inr, description, is_active, created_at + "#, ) .bind(id) .bind(&payload.name) + .bind(&role_key) .bind(&payload.description) .bind(payload.tracecoins_amount) - .bind(payload.price) - .bind(payload.duration_days) - .bind(payload.valid_from) - .bind(payload.valid_until) - .bind(payload.is_promotional) + .bind(price_inr) .bind(payload.is_active) - .bind(payload.features) .fetch_one(&state.pool) .await; match updated { - Ok(pkg) => { - let response: PricingPackageResponse = pkg.into(); - (StatusCode::OK, Json(response)).into_response() - } + Ok(pkg) => (StatusCode::OK, Json(PricingPackageResponse::from(pkg))).into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), } } @@ -329,7 +313,7 @@ async fn delete_package( .execute(&state.pool) .await { - Ok(r) if r.rows_affected() > 0 => { + Ok(result) if result.rows_affected() > 0 => { (StatusCode::OK, Json(serde_json::json!({"message": "Package deleted"}))).into_response() } Ok(_) => (StatusCode::NOT_FOUND, "Package not found").into_response(), @@ -341,78 +325,65 @@ async fn get_packages_by_type( State(state): State, Query(q): Query, ) -> impl IntoResponse { - let package_type = q.package_type.as_deref().unwrap_or("TRACECOIN_BUNDLE"); - let now = chrono::Utc::now(); + let package_type = q.package_type.unwrap_or_else(|| "TRACECOIN_BUNDLE".to_string()); - let packages = sqlx::query_as::<_, PricingPackageRow>( - r#" - SELECT id, name, description, package_type, applicable_roles, - tracecoins_amount, price, duration_days, valid_from, valid_until, - is_promotional, is_active, features, created_at, updated_at - FROM pricing_packages - WHERE package_type = $1 - AND is_active = true - AND (valid_from IS NULL OR valid_from <= $2) - AND (valid_until IS NULL OR valid_until > $2) - ORDER BY is_promotional DESC, price ASC - "# + let rows = sqlx::query_as::<_, PricingPackageRow>( + &package_query( + "WHERE package_type = $1 AND is_active = true", + "ORDER BY price_inr ASC, created_at DESC", + ), ) - .bind(package_type) - .bind(now) + .bind(&package_type) .fetch_all(&state.pool) .await; - let packages = match packages { - Ok(p) => p, + let rows = match rows { + Ok(rows) => rows, Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), }; - let packages: Vec = packages.into_iter().map(|p| p.into()).collect(); + let packages: Vec = rows.into_iter().map(Into::into).collect(); (StatusCode::OK, Json(serde_json::json!({ "data": packages, "package_type": package_type - }))).into_response() + }))) + .into_response() } async fn get_packages_for_role( State(state): State, Query(q): Query, ) -> impl IntoResponse { - let applicable_role = q.applicable_role.as_deref().unwrap_or(""); + let role = q + .applicable_role + .or(q.role) + .unwrap_or_default() + .trim() + .to_uppercase(); let active_only = q.active_only.unwrap_or(true); - let now = chrono::Utc::now(); - let packages = sqlx::query_as::<_, PricingPackageRow>( - &format!( - r#" - SELECT id, name, description, package_type, applicable_roles, - tracecoins_amount, price, duration_days, valid_from, valid_until, - is_promotional, is_active, features, created_at, updated_at - FROM pricing_packages - WHERE ($1 = '' OR $1 = ANY(applicable_roles)) - AND (is_active = true OR {} = false) - AND (valid_from IS NULL OR valid_from <= $2) - AND (valid_until IS NULL OR valid_until > $2) - ORDER BY is_promotional DESC, price ASC - "#, - if active_only { "true" } else { "false" } - ) + let rows = sqlx::query_as::<_, PricingPackageRow>( + &package_query( + "WHERE ($1 = '' OR role_key = $1) AND ($2 = false OR is_active = true)", + "ORDER BY price_inr ASC, created_at DESC", + ), ) - .bind(applicable_role) - .bind(now) + .bind(&role) + .bind(active_only) .fetch_all(&state.pool) .await; - let packages = match packages { - Ok(p) => p, + let rows = match rows { + Ok(rows) => rows, Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()).into_response(), }; - let packages: Vec = packages.into_iter().map(|p| p.into()).collect(); + let packages: Vec = rows.into_iter().map(Into::into).collect(); (StatusCode::OK, Json(serde_json::json!({ "data": packages, - "applicable_role": applicable_role - }))).into_response() + "applicable_role": role + }))) + .into_response() } diff --git a/crates/auth/Cargo.toml b/crates/auth/Cargo.toml index 166016b..4ae55b3 100644 --- a/crates/auth/Cargo.toml +++ b/crates/auth/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -jsonwebtoken = "10.3" +jsonwebtoken = { version = "10.3", features = ["rust_crypto"] } argon2 = "0.5" rand_core = { version = "0.6", features = ["std"] } serde = { workspace = true } diff --git a/crates/contracts/Cargo.toml b/crates/contracts/Cargo.toml index cf53366..8b0cf29 100644 --- a/crates/contracts/Cargo.toml +++ b/crates/contracts/Cargo.toml @@ -13,7 +13,7 @@ chrono = { workspace = true } anyhow = { workspace = true } sqlx = { workspace = true } async-trait = { workspace = true } -jsonwebtoken = "10.3" +jsonwebtoken = { version = "10.3", features = ["rust_crypto"] } db = { path = "../db" } cache = { path = "../cache" } storage = { path = "../storage" }