fix payments runtime and jwt backend

This commit is contained in:
Tracewebstudio Dev 2026-06-09 22:52:30 +02:00
parent d48983ee21
commit 52e30a1b4b
5 changed files with 355 additions and 188 deletions

196
Cargo.lock generated
View file

@ -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"

View file

@ -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)
"#,
)

View file

@ -14,6 +14,7 @@ use uuid::Uuid;
pub struct PackageTypeQuery {
pub package_type: Option<String>,
pub applicable_role: Option<String>,
pub role: Option<String>,
pub active_only: Option<bool>,
}
@ -29,48 +30,37 @@ pub struct CreatePackageRequest {
pub name: String,
pub description: Option<String>,
pub package_type: String,
pub applicable_roles: Vec<String>,
pub role_key: Option<String>,
pub applicable_roles: Option<Vec<String>>,
pub tracecoins_amount: i32,
pub price: i32,
pub duration_days: Option<i32>,
pub valid_from: Option<chrono::DateTime<chrono::Utc>>,
pub valid_until: Option<chrono::DateTime<chrono::Utc>>,
pub is_promotional: Option<bool>,
pub price: Option<i32>,
pub price_inr: Option<i32>,
pub is_active: Option<bool>,
pub features: Option<serde_json::Value>,
}
#[derive(Debug, Deserialize)]
pub struct UpdatePackageRequest {
pub name: Option<String>,
pub description: Option<String>,
pub role_key: Option<String>,
pub applicable_roles: Option<Vec<String>>,
pub tracecoins_amount: Option<i32>,
pub price: Option<i32>,
pub duration_days: Option<i32>,
pub valid_from: Option<chrono::DateTime<chrono::Utc>>,
pub valid_until: Option<chrono::DateTime<chrono::Utc>>,
pub is_promotional: Option<bool>,
pub price_inr: Option<i32>,
pub is_active: Option<bool>,
pub features: Option<serde_json::Value>,
}
#[derive(Debug, FromRow)]
pub struct PricingPackageRow {
pub id: Uuid,
pub name: String,
pub description: Option<String>,
pub role_key: String,
pub package_type: String,
pub applicable_roles: Vec<String>,
pub tracecoins_amount: i32,
pub price: i32,
pub duration_days: Option<i32>,
pub valid_from: Option<chrono::DateTime<chrono::Utc>>,
pub valid_until: Option<chrono::DateTime<chrono::Utc>>,
pub is_promotional: bool,
pub price_inr: i32,
pub description: Option<String>,
pub is_active: bool,
pub features: Option<serde_json::Value>,
pub created_at: chrono::DateTime<chrono::Utc>,
pub updated_at: chrono::DateTime<chrono::Utc>,
}
#[derive(Debug, Serialize)]
@ -78,10 +68,12 @@ pub struct PricingPackageResponse {
pub id: Uuid,
pub name: String,
pub description: Option<String>,
pub package_type: String,
pub role_key: String,
pub applicable_roles: Vec<String>,
pub package_type: String,
pub tracecoins_amount: i32,
pub price: i32,
pub price_inr: i32,
pub duration_days: Option<i32>,
pub valid_from: Option<chrono::DateTime<chrono::Utc>>,
pub valid_until: Option<chrono::DateTime<chrono::Utc>>,
@ -96,33 +88,58 @@ pub struct PricingPackageResponse {
impl From<PricingPackageRow> 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<String>, applicable_roles: Option<Vec<String>>) -> Result<String, String> {
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<AppState> {
Router::new()
.route("/", get(list_packages))
@ -138,83 +155,67 @@ async fn list_packages(
State(state): State<AppState>,
Query(q): Query<PaginationQuery>,
) -> 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<PricingPackageResponse> = packages.into_iter().map(|p| p.into()).collect();
let packages: Vec<PricingPackageResponse> = 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<AppState>,
Path(id): Path<Uuid>,
) -> 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<AppState>,
Json(payload): Json<CreatePackageRequest>,
) -> 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<Uuid>,
Json(payload): Json<UpdatePackageRequest>,
) -> 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<AppState>,
Query(q): Query<PackageTypeQuery>,
) -> 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<PricingPackageResponse> = packages.into_iter().map(|p| p.into()).collect();
let packages: Vec<PricingPackageResponse> = 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<AppState>,
Query(q): Query<PackageTypeQuery>,
) -> 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<PricingPackageResponse> = packages.into_iter().map(|p| p.into()).collect();
let packages: Vec<PricingPackageResponse> = 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()
}

View file

@ -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 }

View file

@ -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" }