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:
OpenFUT Agent
2026-08-13 19:27:06 +00:00
parent d32dc6e3ae
commit bcc4f5104a
6 changed files with 150 additions and 2 deletions
+30 -2
View File
@@ -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;