from __future__ import annotations import json from dataclasses import dataclass, field from datetime import date, datetime, timezone from pathlib import Path from typing import Any, TypeVar from sqlalchemy import select from sqlalchemy.orm import Session from fan_passport import models ModelT = TypeVar("ModelT") @dataclass class ImportResult: counts: dict[str, int] = field(default_factory=dict) created: dict[str, int] = field(default_factory=dict) updated: dict[str, int] = field(default_factory=dict) def record(self, entity: str, *, created: bool) -> None: self.counts[entity] = self.counts.get(entity, 0) + 1 bucket = self.created if created else self.updated bucket[entity] = bucket.get(entity, 0) + 1 def as_dict(self) -> dict[str, dict[str, int]]: return {"counts": self.counts, "created": self.created, "updated": self.updated} def parse_datetime(value: Any) -> datetime | None: if value in {None, ""}: return None if isinstance(value, datetime): dt = value elif isinstance(value, str): text = value[:-1] + "+00:00" if value.endswith("Z") else value dt = datetime.fromisoformat(text) else: raise ValueError(f"Unsupported datetime value: {value!r}") if dt.tzinfo is None: return dt.replace(tzinfo=timezone.utc) return dt def parse_date(value: Any) -> date | None: if value in {None, ""}: return None if isinstance(value, date) and not isinstance(value, datetime): return value if isinstance(value, str): return date.fromisoformat(value) raise ValueError(f"Unsupported date value: {value!r}") def _upsert_by_primary_key( session: Session, model: type[ModelT], primary_key: str, values: dict[str, Any], ) -> tuple[ModelT, bool]: obj = session.get(model, primary_key) created = obj is None if created: obj = model(**values) # type: ignore[call-arg] session.add(obj) # type: ignore[arg-type] else: for key, value in values.items(): setattr(obj, key, value) return obj, created def _upsert_by_unique( session: Session, model: type[ModelT], unique_field: str, unique_value: str, values: dict[str, Any], ) -> tuple[ModelT, bool]: obj = session.scalar(select(model).where(getattr(model, unique_field) == unique_value)) created = obj is None if created: obj = model(**values) # type: ignore[call-arg] session.add(obj) # type: ignore[arg-type] else: values = dict(values) values.pop("id", None) for key, value in values.items(): setattr(obj, key, value) return obj, created def import_seed_file(session: Session, seed_file: str | Path) -> ImportResult: path = Path(seed_file) with path.open("r", encoding="utf-8") as handle: payload = json.load(handle) return import_seed_payload(session, payload) def import_seed_payload(session: Session, payload: dict[str, Any]) -> ImportResult: """Idempotently upsert competition content. The importer intentionally does not commit. Callers can wrap it in a wider transaction and attach audit logs. """ result = ImportResult() for item in payload.get("competitions", []): values = { "code": item["code"], "name": item["name"], "season": item.get("season", "2026"), "governing_body": item.get("governing_body"), "starts_at": parse_datetime(item.get("starts_at")), "ends_at": parse_datetime(item.get("ends_at")), "active": item.get("active", True), "metadata_json": item.get("metadata_json", {}), } _, created = _upsert_by_primary_key(session, models.Competition, values["code"], values) result.record("competitions", created=created) for item in payload.get("teams", []): values = { "id": item["id"], "competition_code": item["competition_code"], "name": item["name"], "short_name": item.get("short_name"), "fifa_code": item.get("fifa_code", item["id"]), "country_code": item.get("country_code"), "confederation": item.get("confederation"), "group_name": item.get("group_name"), "active": item.get("active", True), "metadata_json": item.get("metadata_json", {}), } _, created = _upsert_by_primary_key(session, models.Team, values["id"], values) result.record("teams", created=created) for item in payload.get("stadiums", []): values = { "id": item["id"], "competition_code": item["competition_code"], "name": item["name"], "city": item["city"], "country": item["country"], "capacity": item.get("capacity"), "latitude": item.get("latitude"), "longitude": item.get("longitude"), "active": item.get("active", True), "metadata_json": item.get("metadata_json", {}), } _, created = _upsert_by_primary_key(session, models.Stadium, values["id"], values) result.record("stadiums", created=created) for item in payload.get("stickers", []): values = { "id": item["id"], "competition_code": item["competition_code"], "name": item["name"], "rarity": item.get("rarity", "common"), "team_id": item.get("team_id"), "image_url": item.get("image_url"), "active": item.get("active", True), "metadata_json": item.get("metadata_json", {}), } _, created = _upsert_by_primary_key(session, models.Sticker, values["id"], values) result.record("stickers", created=created) for item in payload.get("matches", []): values = { "id": item["id"], "competition_code": item["competition_code"], "fifa_match_no": item.get("fifa_match_no"), "stage": item.get("stage", "group"), "group_name": item.get("group_name"), "home_team_id": item.get("home_team_id"), "away_team_id": item.get("away_team_id"), "stadium_id": item.get("stadium_id"), "kickoff_at": parse_datetime(item.get("kickoff_at")), "status": item.get("status", "scheduled"), "home_score": item.get("home_score"), "away_score": item.get("away_score"), "winner_team_id": item.get("winner_team_id"), "is_giant_killing": item.get("is_giant_killing", False), "active": item.get("active", True), "metadata_json": item.get("metadata_json", {}), } _, created = _upsert_by_primary_key(session, models.Match, values["id"], values) result.record("matches", created=created) for item in payload.get("quiz_questions", []): values = { "id": item["id"], "competition_code": item["competition_code"], "slug": item["slug"], "prompt": item["prompt"], "options_json": item.get("options_json", []), "correct_option": item["correct_option"], "explanation": item.get("explanation"), "difficulty": item.get("difficulty", "medium"), "scheduled_date": parse_date(item.get("scheduled_date")), "active": item.get("active", True), "metadata_json": item.get("metadata_json", {}), } _, created = _upsert_by_primary_key(session, models.QuizQuestion, values["id"], values) result.record("quiz_questions", created=created) for item in payload.get("challenges", []): values = { "id": item.get("id"), "competition_code": item["competition_code"], "slug": item["slug"], "title": item["title"], "description": item.get("description", ""), "category": item.get("category", "general"), "points": item.get("points", 0), "repeatable": item.get("repeatable", False), "active": item.get("active", True), "rules_json": item.get("rules_json", {}), "metadata_json": item.get("metadata_json", {}), } if values["id"] is None: values.pop("id") _, created = _upsert_by_unique(session, models.Challenge, "slug", item["slug"], values) result.record("challenges", created=created) for item in payload.get("badges", []): values = { "id": item.get("id"), "competition_code": item["competition_code"], "slug": item["slug"], "title": item["title"], "description": item.get("description", ""), "tier": item.get("tier", "bronze"), "icon": item.get("icon"), "points": item.get("points", 0), "active": item.get("active", True), "criteria_json": item.get("criteria_json", {}), "metadata_json": item.get("metadata_json", {}), } if values["id"] is None: values.pop("id") _, created = _upsert_by_unique(session, models.Badge, "slug", item["slug"], values) result.record("badges", created=created) session.flush() return result