From 37bf797254ad47e597dd7d183fe7d9406a332acf Mon Sep 17 00:00:00 2001 From: Cal Corum Date: Mon, 22 Dec 2025 14:15:26 -0600 Subject: [PATCH] Fix critical week rollover bugs causing 60x freeze message spam MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three bugs identified and fixed: 1. Deduplication logic tracked wrong week (transaction_freeze.py:216-219) - Saved freeze_from_week BEFORE _begin_freeze() modifies current.week - Prevents re-execution when API returns stale data 2. _run_transactions() bypassed service layer (transaction_freeze.py:350-394) - Added get_regular_transactions_by_week() to transaction_service.py - Now properly filters frozen=false and cancelled=false - Uses Transaction model objects instead of raw dict access 3. CRITICAL: Hardcoded current_id=1 (league_service.py:88-106) - Current table has one row PER SEASON, not a single row - Was patching Season 3 (id=1) instead of Season 13 (id=11) - Now fetches actual current state ID before patching Root cause: The hardcoded ID caused every PATCH to update the wrong season's record, so freeze was never actually set to True on the current season. This caused the dedup check to pass 60 times (once per minute during hour 0). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- services/league_service.py | 15 +++++++++---- services/transaction_service.py | 37 +++++++++++++++++++++++++++++++++ tasks/transaction_freeze.py | 37 +++++++++++++++------------------ 3 files changed, 65 insertions(+), 24 deletions(-) diff --git a/services/league_service.py b/services/league_service.py index 49c96e5..2dd4ed3 100644 --- a/services/league_service.py +++ b/services/league_service.py @@ -85,17 +85,24 @@ class LeagueService(BaseService[Current]): logger.warning("update_current_state called with no updates") return await self.get_current_state() - # Current state always has ID of 1 (single record) - current_id = 1 + # Get the current state to find its actual ID + # (Current table has one row per season, NOT a single row with id=1) + current = await self.get_current_state() + if not current: + logger.error("Cannot update current state - unable to fetch current state") + return None + + current_id = current.id + logger.debug(f"Updating current state id={current_id} (season {current.season})") # Use BaseService patch method updated_current = await self.patch(current_id, update_data) if updated_current: - logger.info(f"Updated current state: {update_data}") + logger.info(f"Updated current state id={current_id}: {update_data}") return updated_current else: - logger.error("Failed to update current state - patch returned None") + logger.error(f"Failed to update current state id={current_id} - patch returned None") return None except Exception as e: diff --git a/services/transaction_service.py b/services/transaction_service.py index b3663f4..4d103d8 100644 --- a/services/transaction_service.py +++ b/services/transaction_service.py @@ -384,6 +384,43 @@ class TransactionService(BaseService[Transaction]): logger.error(f"Error getting frozen transactions for weeks {week_start}-{week_end}: {e}") return [] + async def get_regular_transactions_by_week( + self, + season: int, + week: int + ) -> List[Transaction]: + """ + Get non-frozen, non-cancelled transactions for a specific week. + + This is used during freeze begin to process regular transactions + that were submitted during the non-freeze period and should take + effect immediately when the new week starts. + + Args: + season: Season number + week: Week number to get transactions for + + Returns: + List of regular (non-frozen, non-cancelled) transactions for the week + """ + try: + params = [ + ('season', str(season)), + ('week_start', str(week)), + ('week_end', str(week)), + ('frozen', 'false'), + ('cancelled', 'false') + ] + + transactions = await self.get_all_items(params=params) + + logger.debug(f"Retrieved {len(transactions)} regular transactions for week {week}") + return transactions + + except Exception as e: + logger.error(f"Error getting regular transactions for week {week}: {e}") + return [] + async def get_contested_transactions(self, season: int, week: int) -> List[Transaction]: """ Get transactions that may be contested (multiple teams want same player). diff --git a/tasks/transaction_freeze.py b/tasks/transaction_freeze.py index 349f586..721f34d 100644 --- a/tasks/transaction_freeze.py +++ b/tasks/transaction_freeze.py @@ -213,9 +213,10 @@ class TransactionFreezeTask: # Only run if we haven't already frozen this week # Track the week we're freezing FROM (before increment) if self.last_freeze_week != current.week: + freeze_from_week = current.week # Save BEFORE _begin_freeze modifies it self.logger.info("Triggering freeze begin", current_week=current.week) await self._begin_freeze(current) - self.last_freeze_week = current.week # Track the week we froze (before increment) + self.last_freeze_week = freeze_from_week # Track the week we froze FROM self.error_notification_sent = False # Reset error flag for new cycle else: self.logger.debug("Freeze already executed for week", week=current.week) @@ -341,26 +342,22 @@ class TransactionFreezeTask: async def _run_transactions(self, current: Current): """ - Process regular (non-frozen) transactions for the current week. + Process regular (non-frozen, non-cancelled) transactions for the current week. - These are transactions that take effect immediately. + These are transactions that were submitted during the non-freeze period + and should take effect immediately when the new week starts. """ try: - # Get all non-frozen transactions for current week - client = await transaction_service.get_client() - params = [ - ('season', str(current.season)), - ('week_start', str(current.week)), - ('week_end', str(current.week)) - ] + # Get non-frozen, non-cancelled transactions for current week via service + transactions = await transaction_service.get_regular_transactions_by_week( + season=current.season, + week=current.week + ) - response = await client.get('transactions', params=params) - - if not response or response.get('count', 0) == 0: + if not transactions: self.logger.info(f"No regular transactions to process for week {current.week}") return - transactions = response.get('transactions', []) self.logger.info(f"Processing {len(transactions)} regular transactions for week {current.week}") # Execute player roster updates for all transactions @@ -371,9 +368,9 @@ class TransactionFreezeTask: try: # Update player's team via PATCH /players/{player_id}?team_id={new_team_id} await self._execute_player_update( - player_id=transaction['player']['id'], - new_team_id=transaction['newteam']['id'], - player_name=transaction['player']['name'] + player_id=transaction.player.id, + new_team_id=transaction.newteam.id, + player_name=transaction.player.name ) success_count += 1 @@ -382,9 +379,9 @@ class TransactionFreezeTask: except Exception as e: self.logger.error( - f"Failed to execute transaction for {transaction['player']['name']}", - player_id=transaction['player']['id'], - new_team_id=transaction['newteam']['id'], + f"Failed to execute transaction for {transaction.player.name}", + player_id=transaction.player.id, + new_team_id=transaction.newteam.id, error=str(e) ) failure_count += 1