diff options
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/server/src/billing.rs | 17 | ||||
| -rw-r--r-- | crates/server/src/cosmetics.rs | 22 |
2 files changed, 27 insertions, 12 deletions
diff --git a/crates/server/src/billing.rs b/crates/server/src/billing.rs index fda42c7..2852b0d 100644 --- a/crates/server/src/billing.rs +++ b/crates/server/src/billing.rs @@ -80,6 +80,15 @@ async fn create_session( amount_cents: i32, ) -> Result<String, AppError> { let stripe = require_stripe(state)?; + + // Nothing is ever sold for nothing. A zero here would mean a price that + // failed to decode or a catalogue row that was never given one, and the + // result would be a session that grants the item without charging. + if amount_cents <= 0 { + tracing::error!("refusing to create a checkout session for {name} at {amount_cents} cents"); + return Err(AppError::Internal(anyhow::anyhow!("could not start checkout"))); + } + let purchase_id = uuid::Uuid::new_v4().to_string(); let success = format!("{}/?purchase=done", state.frontend_origin); @@ -161,8 +170,8 @@ async fn checkout_cosmetic( .fetch_optional(&state.db) .await? .ok_or(AppError::NotFound)?; - let name: String = row.try_get("name").unwrap_or_default(); - let price: i32 = row.try_get("price_cents").unwrap_or(0); + let name: String = row.try_get("name")?; + let price: i32 = row.try_get("price_cents")?; // Already owned: charging again would be taking money for nothing. let owned = sqlx::query("SELECT 1 FROM user_cosmetics WHERE user_id = $1 AND cosmetic_id = $2") @@ -190,8 +199,8 @@ async fn checkout_bundle( .fetch_optional(&state.db) .await? .ok_or(AppError::NotFound)?; - let name: String = row.try_get("name").unwrap_or_default(); - let price: i32 = row.try_get("price_cents").unwrap_or(0); + let name: String = row.try_get("name")?; + let price: i32 = row.try_get("price_cents")?; // A bundle whose every item is already owned has nothing to sell. let remaining: i64 = sqlx::query_scalar( diff --git a/crates/server/src/cosmetics.rs b/crates/server/src/cosmetics.rs index b4532a8..de70950 100644 --- a/crates/server/src/cosmetics.rs +++ b/crates/server/src/cosmetics.rs @@ -25,7 +25,7 @@ struct Cosmetic { id: String, name: String, category: String, - price_cents: i64, + price_cents: i32, value: String, } @@ -33,16 +33,22 @@ async fn list_catalog(State(state): State<Arc<AppState>>) -> Result<impl IntoRes let rows = sqlx::query("SELECT id, name, category, price_cents, value FROM cosmetics ORDER BY category, price_cents") .fetch_all(&state.db) .await?; + // Decode errors are returned, not defaulted away. price_cents was typed + // i64 against an INTEGER column, so sqlx refused every decode and + // unwrap_or_default turned the whole catalogue into $0.00 with nothing + // logged. A wrong price is worse than an error page. let items: Vec<Cosmetic> = rows .into_iter() - .map(|row| Cosmetic { - id: row.try_get("id").unwrap_or_default(), - name: row.try_get("name").unwrap_or_default(), - category: row.try_get("category").unwrap_or_default(), - price_cents: row.try_get("price_cents").unwrap_or_default(), - value: row.try_get("value").unwrap_or_default(), + .map(|row| { + Ok(Cosmetic { + id: row.try_get("id")?, + name: row.try_get("name")?, + category: row.try_get("category")?, + price_cents: row.try_get("price_cents")?, + value: row.try_get("value")?, + }) }) - .collect(); + .collect::<Result<_, sqlx::Error>>()?; Ok(Json(items)) } |