"""Refractor service layer — shared orchestration across router endpoints. Provides ``ensure_variant_cards`` and ``evaluate_and_boost`` so that both the evaluate-game endpoint and the manual card-evaluate endpoint share the same boost orchestration logic without requiring HTTP round-trips in tests. """ import logging import os logger = logging.getLogger(__name__) def ensure_variant_cards( player_id: int, team_id: int, target_tier: int | None = None, card_type: str | None = None, *, _state_model=None, ) -> dict: """Ensure variant cards exist for all tiers up to target_tier. Idempotent — safe to call multiple times. If a variant card already exists for a given tier it is skipped. Partial failures are tolerated: lower tiers that were committed are reported even if a higher tier fails. Args: player_id: Player primary key. team_id: Team primary key. target_tier: Highest tier to ensure variants for. If None, uses ``state.current_tier`` (backfill mode — creates missing variants for tiers already recorded in the DB). card_type: One of 'batter', 'sp', 'rp'. If None, derived from ``state.track.card_type``. _state_model: Dependency-injection override for RefractorCardState (used in tests). Returns: Dict with keys: - ``current_tier`` (int): highest tier confirmed in the DB after this call (equal to state.current_tier if no new tiers created). - ``variants_created`` (list[int]): variant hashes newly created. - ``boost_results`` (list[dict]): raw return values from apply_tier_boost for each newly created variant. """ if _state_model is None: from app.db_engine import RefractorCardState as _state_model # noqa: PLC0415 state = _state_model.get_or_none( (_state_model.player_id == player_id) & (_state_model.team_id == team_id) ) if state is None: return {"current_tier": 0, "variants_created": [], "boost_results": []} if target_tier is None: target_tier = state.current_tier if target_tier == 0: return { "current_tier": state.current_tier, "variants_created": [], "boost_results": [], } # Resolve card_type from track if not provided. resolved_card_type = card_type if resolved_card_type is None: resolved_card_type = state.track.card_type if state.track else None if resolved_card_type is None: logger.warning( "ensure_variant_cards: no card_type for player=%s team=%s — skipping", player_id, team_id, ) return { "current_tier": state.current_tier, "variants_created": [], "boost_results": [], } # Respect kill switch. boost_enabled = os.environ.get("REFRACTOR_BOOST_ENABLED", "true").lower() != "false" if not boost_enabled: return { "current_tier": state.current_tier, "variants_created": [], "boost_results": [], } from app.services.refractor_boost import ( # noqa: PLC0415 apply_tier_boost, compute_variant_hash, ) is_batter = resolved_card_type == "batter" if is_batter: from app.db_engine import BattingCard as CardModel # noqa: PLC0415 else: from app.db_engine import PitchingCard as CardModel # noqa: PLC0415 variants_created = [] boost_results = [] last_successful_tier = state.current_tier for tier in range(1, target_tier + 1): variant_hash = compute_variant_hash(player_id, tier) existing = CardModel.get_or_none( (CardModel.player == player_id) & (CardModel.variant == variant_hash) ) if existing is not None: # Already exists — idempotent skip. continue try: boost_result = apply_tier_boost( player_id, team_id, tier, resolved_card_type ) variants_created.append(variant_hash) boost_results.append(boost_result) last_successful_tier = tier except Exception as exc: logger.warning( "ensure_variant_cards: boost failed for player=%s team=%s tier=%s: %s", player_id, team_id, tier, exc, ) # Don't attempt higher tiers if a lower one failed; the missing # lower-tier variant would cause apply_tier_boost to fail for T+1 # anyway (it reads from the previous tier's variant as its source). break return { "current_tier": last_successful_tier, "variants_created": variants_created, "boost_results": boost_results, } def evaluate_and_boost( player_id: int, team_id: int, *, _state_model=None, _stats_model=None, ) -> dict: """Full evaluation: recompute tier from career stats, then ensure variant cards exist. Combines evaluate_card (dry_run=True) with ensure_variant_cards so that both tier computation and variant-card creation happen in a single call. Handles both tier-up cases (computed_tier > stored) and backfill cases (tier already in DB but variant card was never created). Args: player_id: Player primary key. team_id: Team primary key. _state_model: DI override for RefractorCardState (used in tests). _stats_model: DI override for stats model passed to evaluate_card. Returns: Dict containing all fields from evaluate_card plus: - ``current_tier`` (int): highest tier confirmed in the DB after this call (may be higher than eval_result["current_tier"] if a tier-up was committed here). - ``variants_created`` (list[int]): variant hashes newly created. - ``boost_results`` (list[dict]): raw boost result dicts. Raises: ValueError: If no RefractorCardState exists for (player_id, team_id). """ from app.services.refractor_evaluator import evaluate_card # noqa: PLC0415 eval_kwargs: dict = {"dry_run": True} if _state_model is not None: eval_kwargs["_state_model"] = _state_model if _stats_model is not None: eval_kwargs["_stats_model"] = _stats_model eval_result = evaluate_card(player_id, team_id, **eval_kwargs) computed_tier = eval_result.get("computed_tier", 0) stored_tier = eval_result.get("current_tier", 0) # target_tier is the higher of formula result and stored value: # - tier-up case: computed > stored, creates new variant(s) # - backfill case: stored > computed (stale), creates missing variants target_tier = max(computed_tier, stored_tier) ensure_kwargs: dict = {"target_tier": target_tier} if _state_model is not None: ensure_kwargs["_state_model"] = _state_model ensure_result = ensure_variant_cards(player_id, team_id, **ensure_kwargs) return { **eval_result, "current_tier": ensure_result["current_tier"], "variants_created": ensure_result["variants_created"], "boost_results": ensure_result["boost_results"], }