Merge pull request 'fix: prevent partial DB writes on scorecard submission failure' (#80) from fix/scorecard-submission-resilience-v2 into next-release
Some checks failed
Build Docker Image / build (push) Has been cancelled

Reviewed-on: #80
This commit is contained in:
cal 2026-03-11 16:27:28 +00:00
commit 514797b787
2 changed files with 56 additions and 33 deletions

View File

@ -210,17 +210,41 @@ class SubmitScorecardCommands(commands.Cog):
game_id = scheduled_game.id
# Phase 6: Read Scorecard Data
# Phase 6: Read ALL Scorecard Data (before any DB writes)
# Reading everything first prevents partial commits if the
# spreadsheet has formula errors (e.g. #N/A in pitching decisions)
await interaction.edit_original_response(
content="📊 Reading play-by-play data..."
content="📊 Reading scorecard data..."
)
plays_data = await self.sheets_service.read_playtable_data(scorecard)
box_score = await self.sheets_service.read_box_score(scorecard)
decisions_data = await self.sheets_service.read_pitching_decisions(
scorecard
)
# Add game_id to each play
for play in plays_data:
play["game_id"] = game_id
# Add game metadata to each decision
for decision in decisions_data:
decision["game_id"] = game_id
decision["season"] = current.season
decision["week"] = setup_data["week"]
decision["game_num"] = setup_data["game_num"]
# Validate WP and LP exist and fetch Player objects
wp, lp, sv, holders, _blown_saves = (
await decision_service.find_winning_losing_pitchers(decisions_data)
)
if wp is None or lp is None:
await interaction.edit_original_response(
content="❌ Your card is missing either a Winning Pitcher or Losing Pitcher"
)
return
# Phase 7: POST Plays
await interaction.edit_original_response(
content="💾 Submitting plays to database..."
@ -244,10 +268,7 @@ class SubmitScorecardCommands(commands.Cog):
)
return
# Phase 8: Read Box Score
box_score = await self.sheets_service.read_box_score(scorecard)
# Phase 9: PATCH Game
# Phase 8: PATCH Game
await interaction.edit_original_response(
content="⚾ Updating game result..."
)
@ -275,33 +296,7 @@ class SubmitScorecardCommands(commands.Cog):
)
return
# Phase 10: Read Pitching Decisions
decisions_data = await self.sheets_service.read_pitching_decisions(
scorecard
)
# Add game metadata to each decision
for decision in decisions_data:
decision["game_id"] = game_id
decision["season"] = current.season
decision["week"] = setup_data["week"]
decision["game_num"] = setup_data["game_num"]
# Validate WP and LP exist and fetch Player objects
wp, lp, sv, holders, _blown_saves = (
await decision_service.find_winning_losing_pitchers(decisions_data)
)
if wp is None or lp is None:
# Rollback
await game_service.wipe_game_data(game_id)
await play_service.delete_plays_for_game(game_id)
await interaction.edit_original_response(
content="❌ Your card is missing either a Winning Pitcher or Losing Pitcher"
)
return
# Phase 11: POST Decisions
# Phase 9: POST Decisions
await interaction.edit_original_response(
content="🎯 Submitting pitching decisions..."
)
@ -361,6 +356,30 @@ class SubmitScorecardCommands(commands.Cog):
# Success!
await interaction.edit_original_response(content="✅ You are all set!")
except SheetsException as e:
# Spreadsheet reading error - show the detailed message to the user
self.logger.error(
f"Spreadsheet error in scorecard submission: {e}", error=e
)
if rollback_state and game_id:
try:
if rollback_state == "GAME_PATCHED":
await game_service.wipe_game_data(game_id)
await play_service.delete_plays_for_game(game_id)
elif rollback_state == "PLAYS_POSTED":
await play_service.delete_plays_for_game(game_id)
except Exception:
pass # Best effort rollback
await interaction.edit_original_response(
content=(
f"❌ There's a problem with your scorecard:\n\n"
f"{str(e)}\n\n"
f"Please fix the issue in your spreadsheet and resubmit."
)
)
except Exception as e:
# Unexpected error - attempt rollback
self.logger.error(f"Unexpected error in scorecard submission: {e}", error=e)

View File

@ -415,6 +415,8 @@ class SheetsService:
self.logger.info(f"Read {len(pit_data)} valid pitching decisions")
return pit_data
except SheetsException:
raise
except Exception as e:
self.logger.error(f"Failed to read pitching decisions: {e}")
raise SheetsException("Unable to read pitching decisions") from e
@ -457,6 +459,8 @@ class SheetsService:
"home": [int(x) for x in score_table[1]], # [R, H, E]
}
except SheetsException:
raise
except Exception as e:
self.logger.error(f"Failed to read box score: {e}")
raise SheetsException("Unable to read box score") from e