feat(economy): purchase_items primitive + entitlement import seeding
purchase_items: generic atomic debit + mint of several items (fail-closed) for open-on-buy Store packs (Store BUY returns items immediately) + POST /economy/purchase-items route. Import: add optional entitlements[] to ProfileImportRequest, seeding unconsumed packs rows in the same transaction (so a source's unopened packs become Core entitlements); idempotency via the existing import fingerprint. Tests: 2 purchase_items unit + endpoint + entitlement-seed import. Core matrix 45 lib + 116 integration + 9 import green; clippy clean.
This commit is contained in:
@@ -4,8 +4,8 @@
|
||||
|
||||
use openfut_core::services::card_db::CardDb;
|
||||
use openfut_core::services::import::{
|
||||
apply_profile_import, ImportClub, ImportExtension, ImportOwnedCard, ImportProfile, ImportSlot,
|
||||
ImportSquad, ProfileImportRequest,
|
||||
apply_profile_import, ImportClub, ImportEntitlement, ImportExtension, ImportOwnedCard,
|
||||
ImportProfile, ImportSlot, ImportSquad, ProfileImportRequest,
|
||||
};
|
||||
|
||||
async fn fresh_pool() -> sqlx::SqlitePool {
|
||||
@@ -79,6 +79,7 @@ fn request(
|
||||
},
|
||||
owned,
|
||||
squad,
|
||||
entitlements: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +129,33 @@ async fn imports_profile_club_owned_and_squad_in_one_shot() {
|
||||
assert_eq!(stored_import_fp, "fp-happy");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn imports_entitlements_seeds_unopened_packs() {
|
||||
let pool = fresh_pool().await;
|
||||
let db = CardDb::load("data").unwrap();
|
||||
let ids = valid_ids(2);
|
||||
let ow = owned(&ids);
|
||||
let mut req = request("g_ent", "fp-ent", ow, None);
|
||||
req.entitlements = vec![
|
||||
ImportEntitlement {
|
||||
definition_id: "70".into(),
|
||||
},
|
||||
ImportEntitlement {
|
||||
definition_id: "70".into(),
|
||||
},
|
||||
];
|
||||
apply_profile_import(&pool, &db, &req)
|
||||
.await
|
||||
.expect("import");
|
||||
// Two unconsumed entitlements seeded into packs (opened = 0).
|
||||
assert_eq!(count(&pool, "packs").await, 2);
|
||||
let unopened: i64 = sqlx::query_scalar("SELECT COUNT(*) FROM packs WHERE opened = 0")
|
||||
.fetch_one(&pool)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(unopened, 2);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn rerun_same_fingerprint_is_idempotent_noop() {
|
||||
let pool = fresh_pool().await;
|
||||
|
||||
@@ -2776,3 +2776,25 @@ async fn test_economy_insufficient_funds_fail_closed() {
|
||||
.iter()
|
||||
.any(|e| e["definition_id"] == "pack-x"));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_economy_purchase_items_debits_and_mints_all() {
|
||||
let app = build_test_app().await;
|
||||
auth(&app, "econ-e").await;
|
||||
let (st, buy) = json_post(
|
||||
&app,
|
||||
"/economy/purchase-items",
|
||||
serde_json::json!({
|
||||
"cost": 800,
|
||||
"items": [
|
||||
{"item_id": "bx-1", "card_id": "d-1"},
|
||||
{"item_id": "bx-2", "card_id": "d-2"}
|
||||
]
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(st, StatusCode::OK);
|
||||
assert_eq!(buy["balance"], 4200);
|
||||
let (_, bal) = json_get(&app, "/economy/balance").await;
|
||||
assert_eq!(bal["balance"], 4200);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user