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.
The FIFA17 economy audit proved the transfer market is synthetic-seller:
buy-now mints a new owned item and debits the buyer; no real counterparty,
no sale-credit/expiry/fee. The generic primitive that models this is an
atomic debit + inventory add (purchase_item), NOT a two-party
transfer_item_with_payment (which would be unused). Fail-closed: an
unaffordable purchase debits nothing and mints nothing. 2 unit tests.
Add a game-agnostic economy service that exposes atomic, fail-closed
operations over Core's existing durable tables rather than forking a
parallel persistence stack:
* currency ledger -> clubs.coins
* owned inventory -> owned_cards
* entitlements -> packs (opaque definition_id, consume-once `opened`)
Compound operations run inside a single SQLite transaction, closing the
atomicity gap in the pool-scoped club::{spend,add}_coins helpers whose
read/modify/write spans multiple round-trips. Public ops:
balance, purchase_entitlement (debit+grant), redeem_entitlement
(consume-once + add items, all-or-nothing), sell_item (remove+credit),
grant_reward (credit).
Deliberately game-neutral: currency names, entitlement/pack ids, and
per-save item-id sequences stay in the per-game adapter that drives these
primitives. 9 unit tests cover debit/credit fail-closed rollback,
consume-once, partial-redeem rollback, and non-negative guards.