use crate::{ db::Pool, error::AppResult, models::{card::Quality, club::Club, pack::PackDefinition}, services::{card_db::CardDb, club as club_svc, pack as pack_svc, profile as profile_svc}, }; use serde::Serialize; use std::collections::BTreeMap; use tracing::info; /// The game whose dev content + inventory this seeds. pub const FIFA17_GAME: &str = "fifa17"; /// Deterministic owned-instance id prefix, so re-running the seed is idempotent /// (INSERT OR IGNORE on a stable id) rather than minting duplicate ownership. const DEV_OWNED_PREFIX: &str = "fdev-"; /// Fixed grant timestamp — the seed is deterministic, not wall-clock dependent. const DEV_ACQUIRED_AT: &str = "2026-08-11T00:00:00Z"; /// The client's My Squad page size (evidence: request `count=11`). const MY_SQUAD_PAGE: usize = 11; /// Seeds the market with NPC listings if empty. pub async fn maybe_seed(_pool: &Pool) -> AppResult<()> { // Any one-time startup seeds go here. // Currently we just ensure the market gets listings on first run. Ok(()) } /// Grants the starter pack to a newly created club. pub async fn grant_starter_pack( pool: &Pool, club_id: &str, pack_defs: &[PackDefinition], ) -> AppResult<()> { // Look for the gold starter pack first; fall back to the first available definition. let starter_def = pack_defs .iter() .find(|p| p.id == "gold_pack") .or_else(|| pack_defs.first()); if let Some(def) = starter_def { info!("Granting starter pack '{}' to club {}", def.id, club_id); pack_svc::grant_pack(pool, club_id, &def.id).await?; } else { tracing::warn!("No pack definitions loaded; skipping starter pack grant"); } Ok(()) } // ───────────────────────────── FIFA 17 dev seed ───────────────────────────── /// Coverage of the seeded FIFA 17 development inventory. Game-independent: it /// counts quality tiers, positions and distinct entities, and whether the Gold /// filter spans more than one page — everything the retail `/club` UI must /// exercise. It carries NO FIFA wire ids (those are the adapter/host's runtime /// concern; the seed never allocates them). #[derive(Debug, Serialize)] pub struct DevSeedReport { pub game_id: String, /// True if the fifa17 club already owned dev cards (no new grants made). pub already_seeded: bool, pub definitions_available: usize, pub owned_total: usize, pub unique_definitions: usize, pub gold: usize, pub silver: usize, pub bronze: usize, pub positions: BTreeMap, pub distinct_nations: usize, pub distinct_leagues: usize, pub distinct_clubs: usize, pub max_same_club: usize, /// Gold owned items exceed one page → the client must request a 2nd page. pub gold_over_one_page: bool, } /// Opt-in development seed: create (if absent) a `game_id=fifa17` profile + club /// and grant Core-owned instances of every dev-pack `CardDefinition` (ids /// `fifa17_*`), plus one deliberate duplicate of a single definition (to exercise /// two-copies-of-one-card identity later). /// /// **Ownership only — no FIFA wire ids.** The FIFA 17 integer item id is minted /// lazily by `Fifa17IdentityResolver` at request time, never here. This keeps the /// boundary clean: Core owns "this profile owns this card"; the adapter owns /// "this owned item is wire id N". /// /// Idempotent: owned ids are deterministic (`fdev-`), inserted with /// `INSERT OR IGNORE`, so re-running grants nothing new. The default profile /// (`fifa23`/no-header) and any existing synthetic inventory are never touched. pub async fn seed_fifa17_dev(pool: &Pool, card_db: &CardDb) -> AppResult { // The dev definitions are exactly the game-namespaced ids in the catalog. let mut defs: Vec<&crate::models::card::CardDefinition> = card_db .cards .values() .filter(|c| c.id.starts_with("fifa17_")) .collect(); defs.sort_by(|a, b| a.id.cmp(&b.id)); // Ensure the fifa17-scoped profile + club exist (single-profile-per-game). let profile = match profile_svc::get_active_profile(pool, FIFA17_GAME).await { Ok(p) => p, Err(_) => profile_svc::create_profile(pool, "OpenFUT Dev (FIFA17)", FIFA17_GAME).await?, }; let club = match club_svc::get_club_by_profile(pool, &profile.id).await { Ok(c) => c, Err(_) => { let c = Club::new(&profile.id, "OpenFUT Dev FC", 100_000); club_svc::create_club(pool, &c).await?; c } }; let prior: i64 = sqlx::query_scalar( "SELECT COUNT(*) FROM owned_cards WHERE club_id = ? AND card_id LIKE 'fifa17_%'", ) .bind(&club.id) .fetch_one(pool) .await?; let already_seeded = prior > 0; // Grant one instance per definition; INSERT OR IGNORE keeps reruns idempotent. for def in &defs { grant_owned( pool, &format!("{DEV_OWNED_PREFIX}{}", def.id), &club.id, &def.id, ) .await?; } // One deliberate duplicate of the first (lexicographic) definition → two // owned copies of one card sharing a definition but distinct owned ids. if let Some(first) = defs.first() { grant_owned( pool, &format!("{DEV_OWNED_PREFIX}{}-b", first.id), &club.id, &first.id, ) .await?; } let report = dev_coverage(pool, card_db, &club.id, already_seeded, defs.len()).await?; info!( "seeded fifa17 dev inventory: {} owned ({} gold) over club {}", report.owned_total, report.gold, club.id ); Ok(report) } async fn grant_owned(pool: &Pool, owned_id: &str, club_id: &str, card_id: &str) -> AppResult<()> { sqlx::query( "INSERT OR IGNORE INTO owned_cards \ (id, club_id, card_id, is_loan, loan_matches_remaining, acquired_at) \ VALUES (?, ?, ?, 0, NULL, ?)", ) .bind(owned_id) .bind(club_id) .bind(card_id) .bind(DEV_ACQUIRED_AT) .execute(pool) .await?; Ok(()) } /// Build the coverage report from the club's owned dev cards joined to `card_db`. async fn dev_coverage( pool: &Pool, card_db: &CardDb, club_id: &str, already_seeded: bool, definitions_available: usize, ) -> AppResult { let card_ids: Vec = sqlx::query_scalar( "SELECT card_id FROM owned_cards WHERE club_id = ? AND card_id LIKE 'fifa17_%'", ) .bind(club_id) .fetch_all(pool) .await?; let (mut gold, mut silver, mut bronze) = (0usize, 0usize, 0usize); let mut positions: BTreeMap = BTreeMap::new(); let mut nations = std::collections::BTreeSet::new(); let mut leagues = std::collections::BTreeSet::new(); let mut club_counts: BTreeMap = BTreeMap::new(); let mut unique = std::collections::BTreeSet::new(); for card_id in &card_ids { unique.insert(card_id.clone()); if let Some(def) = card_db.get(card_id) { match Quality::from_overall(def.overall) { Quality::Gold => gold += 1, Quality::Silver => silver += 1, Quality::Bronze => bronze += 1, } *positions.entry(def.position.clone()).or_default() += 1; nations.insert(def.nation.clone()); leagues.insert(def.league.clone()); *club_counts.entry(def.club.clone()).or_default() += 1; } } Ok(DevSeedReport { game_id: FIFA17_GAME.to_string(), already_seeded, definitions_available, owned_total: card_ids.len(), unique_definitions: unique.len(), gold, silver, bronze, positions, distinct_nations: nations.len(), distinct_leagues: leagues.len(), distinct_clubs: club_counts.len(), max_same_club: club_counts.values().copied().max().unwrap_or(0), gold_over_one_page: gold > MY_SQUAD_PAGE, }) }