feat(economy): expose transactional economy service over HTTP
Add generic /economy/* routes (balance, entitlements, purchase-entitlement, redeem-entitlement, sell-item, grant-reward, purchase-item) resolving the club server-side via the same game-scoped active-profile mechanism as /collection — callers never supply a club id, so there is no cross-club access. Add list_unopened_entitlements + Entitlement for the reader side. Game-neutral: no currency names or wire semantics. 4 endpoint integration tests (balance+reward, purchase+redeem, purchase-item+sell fail-closed, insufficient-funds fail-closed); full matrix 43 lib + 115 integration green.
This commit is contained in:
@@ -194,6 +194,27 @@ pub async fn balance(pool: &Pool, club_id: &str) -> AppResult<i64> {
|
||||
.ok_or_else(|| AppError::NotFound(format!("club not found: {club_id}")))
|
||||
}
|
||||
|
||||
/// One unopened entitlement a club owns.
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct Entitlement {
|
||||
pub id: String,
|
||||
pub definition_id: String,
|
||||
}
|
||||
|
||||
/// List a club's unconsumed entitlements (opened = 0), oldest first.
|
||||
pub async fn list_unopened_entitlements(pool: &Pool, club_id: &str) -> AppResult<Vec<Entitlement>> {
|
||||
let rows = sqlx::query_as::<_, (String, String)>(
|
||||
"SELECT id, definition_id FROM packs WHERE club_id = ? AND opened = 0 ORDER BY created_at ASC, id ASC",
|
||||
)
|
||||
.bind(club_id)
|
||||
.fetch_all(pool)
|
||||
.await?;
|
||||
Ok(rows
|
||||
.into_iter()
|
||||
.map(|(id, definition_id)| Entitlement { id, definition_id })
|
||||
.collect())
|
||||
}
|
||||
|
||||
/// Debit `cost` and grant one entitlement, atomically. Fail-closed: if the club
|
||||
/// cannot afford `cost`, nothing is debited and no entitlement is created.
|
||||
pub async fn purchase_entitlement(
|
||||
|
||||
Reference in New Issue
Block a user