//! Generic economy HTTP boundary. //! //! Exposes [`crate::services::economy`] over the same game-scoped active-profile //! resolution every other Core route uses ([`GameId`] header → active profile → //! club). The caller (a game host) never supplies a club id; Core maps the game //! to its authoritative club, so there is no cross-club economy access. Every //! op is a single durable SQLite transaction in the service layer. //! //! This surface is deliberately game-neutral: no currency names, pack ids, or //! wire semantics — those live in the game host/adapter. use axum::{extract::State, Json}; use serde::{Deserialize, Serialize}; use crate::{ app::AppState, error::AppResult, extractors::GameId, services::{club as club_svc, economy, economy::GrantedItem, profile as profile_svc}, }; /// Resolve the game-scoped active profile's club id. async fn resolve_club(state: &AppState, game: &GameId) -> AppResult { let profile = profile_svc::get_active_profile(&state.pool, game.as_str()).await?; let club = club_svc::get_club_by_profile(&state.pool, &profile.id).await?; Ok(club.id) } #[derive(Serialize)] pub struct BalanceResponse { pub balance: i64, } /// `GET /economy/balance` — the club's currency balance. pub async fn get_balance( State(state): State, game: GameId, ) -> AppResult> { let club = resolve_club(&state, &game).await?; let balance = economy::balance(&state.pool, &club).await?; Ok(Json(BalanceResponse { balance })) } /// `GET /economy/entitlements` — the club's unconsumed entitlements. pub async fn get_entitlements( State(state): State, game: GameId, ) -> AppResult>> { let club = resolve_club(&state, &game).await?; Ok(Json( economy::list_unopened_entitlements(&state.pool, &club).await?, )) } #[derive(Deserialize)] pub struct PurchaseEntitlementRequest { pub cost: i64, pub definition_id: String, } /// `POST /economy/purchase-entitlement` — atomic debit + grant. pub async fn post_purchase_entitlement( State(state): State, game: GameId, Json(req): Json, ) -> AppResult> { let club = resolve_club(&state, &game).await?; Ok(Json( economy::purchase_entitlement(&state.pool, &club, req.cost, &req.definition_id).await?, )) } #[derive(Deserialize)] pub struct RedeemEntitlementRequest { pub entitlement_id: String, pub items: Vec, } #[derive(Serialize)] pub struct RedeemEntitlementResponse { pub definition_id: String, } /// `POST /economy/redeem-entitlement` — atomic consume-once + add items. pub async fn post_redeem_entitlement( State(state): State, game: GameId, Json(req): Json, ) -> AppResult> { let club = resolve_club(&state, &game).await?; let definition_id = economy::redeem_entitlement(&state.pool, &club, &req.entitlement_id, &req.items).await?; Ok(Json(RedeemEntitlementResponse { definition_id })) } #[derive(Deserialize)] pub struct SellItemRequest { pub item_id: String, pub price: i64, } /// `POST /economy/sell-item` — atomic remove + credit. pub async fn post_sell_item( State(state): State, game: GameId, Json(req): Json, ) -> AppResult> { let club = resolve_club(&state, &game).await?; let balance = economy::sell_item(&state.pool, &club, &req.item_id, req.price).await?; Ok(Json(BalanceResponse { balance })) } #[derive(Deserialize)] pub struct GrantRewardRequest { pub amount: i64, } /// `POST /economy/grant-reward` — atomic credit. pub async fn post_grant_reward( State(state): State, game: GameId, Json(req): Json, ) -> AppResult> { let club = resolve_club(&state, &game).await?; let balance = economy::grant_reward(&state.pool, &club, req.amount).await?; Ok(Json(BalanceResponse { balance })) } #[derive(Deserialize)] pub struct PurchaseItemRequest { pub cost: i64, pub item_id: String, pub card_id: String, } /// `POST /economy/purchase-item` — atomic debit + mint item. pub async fn post_purchase_item( State(state): State, game: GameId, Json(req): Json, ) -> AppResult> { let club = resolve_club(&state, &game).await?; let balance = economy::purchase_item(&state.pool, &club, req.cost, &req.item_id, &req.card_id).await?; Ok(Json(BalanceResponse { balance })) } #[derive(Deserialize)] pub struct PurchaseItemsRequest { pub cost: i64, pub items: Vec, } /// `POST /economy/purchase-items` — atomic debit + mint several items. pub async fn post_purchase_items( State(state): State, game: GameId, Json(req): Json, ) -> AppResult> { let club = resolve_club(&state, &game).await?; let balance = economy::purchase_items(&state.pool, &club, req.cost, &req.items).await?; Ok(Json(BalanceResponse { balance })) }