//! Cart API: add/remove items, get count. use axum::extract::{Path, State}; use axum::response::IntoResponse; use axum::Json; use crate::{ auth::AuthUser, db::{self, ItemId}, error::{AppError, Result}, AppState, }; /// Toggle an item's cart status. Returns the new state. #[tracing::instrument(skip_all, name = "cart::toggle")] pub(super) async fn toggle_cart( State(state): State, AuthUser(user): AuthUser, Path(item_id): Path, ) -> Result { // Single-query pre-flight: item existence, visibility, ownership, purchase, cart status let pf = db::cart::toggle_cart_preflight(&state.db, user.id, item_id) .await? .ok_or(AppError::NotFound)?; if !pf.is_public { return Err(AppError::NotFound); } if !pf.listed { // Unlisted items are bundle-only — `item.rs:47-49` enforces this on the // single-item checkout path; the cart flow must enforce the same gate // or the bundle-only restriction is bypassable by any UUID guesser. return Err(AppError::BadRequest( "This item is only available through its bundle.".to_string(), )); } if pf.is_owner { return Err(AppError::BadRequest( "You can't add your own items to your cart.".to_string(), )); } if pf.has_purchased { return Err(AppError::BadRequest( "You already own this item.".to_string(), )); } if pf.in_cart { db::cart::remove_from_cart(&state.db, user.id, item_id).await?; } else { db::cart::add_to_cart(&state.db, user.id, item_id).await?; } Ok(Json(serde_json::json!({ "in_cart": !pf.in_cart }))) } /// Remove an item from the cart explicitly. Returns 204. #[tracing::instrument(skip_all, name = "cart::remove")] pub(super) async fn remove_from_cart( State(state): State, AuthUser(user): AuthUser, Path(item_id): Path, ) -> Result { db::cart::remove_from_cart(&state.db, user.id, item_id).await?; Ok(axum::http::StatusCode::NO_CONTENT) } /// Update the PWYW amount for a cart item. #[tracing::instrument(skip_all, name = "cart::update_amount")] pub(super) async fn update_cart_amount( State(state): State, AuthUser(user): AuthUser, Path(item_id): Path, Json(body): Json, ) -> Result { // Verify item exists and is PWYW let item = db::items::get_item_by_id(&state.db, item_id) .await? .ok_or(AppError::NotFound)?; if !item.pwyw_enabled { return Err(AppError::BadRequest( "This item does not use pay-what-you-want pricing.".to_string(), )); } // Validate amount against minimum let min = item.pwyw_min_cents.unwrap_or(0); if body.amount_cents < min { return Err(AppError::BadRequest(format!( "Amount must be at least ${}.{:02}.", min / 100, min % 100 ))); } // Cap at $10,000 if body.amount_cents > 1_000_000 { return Err(AppError::BadRequest( "Amount cannot exceed $10,000.".to_string(), )); } let updated = db::cart::update_cart_amount( &state.db, user.id, item_id, Some(body.amount_cents), ) .await?; if !updated { return Err(AppError::NotFound); } Ok(Json(serde_json::json!({ "amount_cents": body.amount_cents }))) } #[derive(Debug, serde::Deserialize)] pub(super) struct UpdateCartAmountRequest { pub amount_cents: i32, } /// Get the number of items in the cart (for nav badge). #[tracing::instrument(skip_all, name = "cart::count")] pub(super) async fn cart_count( State(state): State, AuthUser(user): AuthUser, ) -> Result { let count = db::cart::get_cart_count(&state.db, user.id).await?; Ok(Json(serde_json::json!({ "count": count }))) }