feat(economy): add generic purchase_item (atomic debit + mint)
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.
This commit is contained in:
@@ -212,6 +212,25 @@ pub async fn purchase_entitlement(
|
||||
})
|
||||
}
|
||||
|
||||
/// Debit `cost` and mint one owned item, atomically. Fail-closed: if the club
|
||||
/// cannot afford `cost`, nothing is debited and no item is added. This is the
|
||||
/// "buy a specific item" primitive (a debit paired with an inventory add), for
|
||||
/// synthetic-seller markets where the purchased item is minted rather than
|
||||
/// transferred from another owner. Returns the post-debit balance.
|
||||
pub async fn purchase_item(
|
||||
pool: &Pool,
|
||||
club_id: &str,
|
||||
cost: i64,
|
||||
item_id: &str,
|
||||
card_id: &str,
|
||||
) -> AppResult<i64> {
|
||||
let mut tx = pool.begin().await?;
|
||||
let balance = debit(&mut tx, club_id, cost).await?;
|
||||
add_item(&mut tx, club_id, item_id, card_id).await?;
|
||||
tx.commit().await?;
|
||||
Ok(balance)
|
||||
}
|
||||
|
||||
/// Consume an entitlement once and add its granted items, atomically. If any
|
||||
/// item insert fails (e.g. a colliding instance id) the whole redemption rolls
|
||||
/// back — the entitlement stays unconsumed and no items are persisted.
|
||||
@@ -452,4 +471,27 @@ mod tests {
|
||||
assert_eq!(grant_reward(&pool, "club", 500).await.unwrap(), 1500);
|
||||
assert_eq!(balance(&pool, "club").await.unwrap(), 1500);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn purchase_item_debits_and_mints() {
|
||||
let pool = fixture().await;
|
||||
let bal = purchase_item(&pool, "club", 400, "item-new", "def-new")
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(bal, 600);
|
||||
assert_eq!(balance(&pool, "club").await.unwrap(), 600);
|
||||
assert_eq!(item_count(&pool, "item-new").await, 1);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn purchase_item_insufficient_funds_rolls_back() {
|
||||
let pool = fixture().await;
|
||||
let err = purchase_item(&pool, "club", 9000, "item-new", "def-new")
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert!(matches!(err, AppError::BadRequest(_)));
|
||||
// Nothing debited, no item minted.
|
||||
assert_eq!(balance(&pool, "club").await.unwrap(), 1000);
|
||||
assert_eq!(item_count(&pool, "item-new").await, 0);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user