feat(import): generic transactional profile-import service + CLI

Core performs a GAME-AGNOSTIC transactional profile import; all FIFA17 semantics
(manifest parse, wire ids, resourceId/nextItemId, squad extension v1,
CardDefinitionId/OwnedItemId choice) stay in openfut-import-fifa17. Core sees
only opaque ids and opaque extension bytes.

services::import::apply_profile_import(pool, card_db, ProfileImportRequest):
- ONE SQLite transaction installs profile + club + all owned cards + canonical
  squad + one opaque game extension; commits together or not at all.
- Definition preflight (pre-tx): every owned card_id MUST resolve in loaded
  production content, so ownership never points at absent content.
- Squad all-or-nothing (pre-tx): every active-squad OwnedItemId MUST be in the
  imported ownership set.
- OwnedItemId uniqueness + generic extension bounds enforced pre-tx.
- Core computes the canonical squad fingerprint itself (never adapter-supplied)
  and persists the extension atomically, exactly as the live squad-write path.

Rerun identity via profiles.import_fingerprint (migration 0018, nullable):
- identical source_fingerprint on an already-imported game -> idempotent no-op;
- differing token -> fail (needs explicit update mode);
- pre-existing non-imported profile -> never clobbered.
So a crash after identity-seeding re-runs cleanly with no cleanup/reminting.

CLI: 'openfut-core import <request.json>' loads production content packs, parses
a generic request, applies. squad_fingerprint made pub(crate) for reuse.

8 import-service tests (happy path, idempotent rerun, fingerprint mismatch,
missing-definition no-write, squad-not-owned, dup OwnedItemId, non-imported
clobber guard, empty-owned). clippy -D warnings clean; full suite 159 green.
This commit is contained in:
funman300
2026-08-12 20:01:27 +00:00
parent 352ad11bc4
commit 9f3c545c46
6 changed files with 614 additions and 4 deletions
+1 -1
View File
@@ -497,7 +497,7 @@ pub async fn replace_squad_with_extension(
/// computed; non-cryptographic (FNV-1a-64) — a stale-extension guard, not a
/// security boundary. The encoding is sorted + delimited so it never depends on
/// row/iteration order.
fn squad_fingerprint<'a>(
pub(crate) fn squad_fingerprint<'a>(
squad_id: &str,
formation: &str,
slots: impl Iterator<Item = (i64, &'a str, bool, bool)>,