Closes #105 Replace the last_game FK guard in update_season_stats() with an atomic INSERT into a new processed_game ledger table. The old guard only blocked same-game immediate replay; it was silently bypassed if game G+1 was processed first (last_game already overwritten). The ledger is keyed on game_id so any re-delivery — including out-of-order — is caught reliably. Changes: - app/db_engine.py: add ProcessedGame model (game FK PK + processed_at) - app/services/season_stats.py: replace last_game check with ProcessedGame.get_or_create(); import ProcessedGame; update docstrings - migrations/2026-03-18_add_processed_game.sql: CREATE TABLE IF NOT EXISTS processed_game with FK to stratgame ON DELETE CASCADE - tests/conftest.py: add ProcessedGame to imports and _TEST_MODELS list - tests/test_season_stats_update.py: add test_out_of_order_replay_prevented; update test_double_count_prevention docstring Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
27 lines
904 B
PL/PgSQL
27 lines
904 B
PL/PgSQL
-- Migration: Add processed_game ledger for full update_season_stats() idempotency
|
|
-- Date: 2026-03-18
|
|
-- Issue: #105
|
|
-- Purpose: Replace the last_game FK check in update_season_stats() with an
|
|
-- atomic INSERT into processed_game. This prevents out-of-order
|
|
-- re-delivery (game G re-delivered after G+1 was already processed)
|
|
-- from bypassing the guard and double-counting stats.
|
|
|
|
BEGIN;
|
|
|
|
CREATE TABLE IF NOT EXISTS processed_game (
|
|
game_id INTEGER PRIMARY KEY REFERENCES stratgame(id) ON DELETE CASCADE,
|
|
processed_at TIMESTAMP NOT NULL DEFAULT NOW()
|
|
);
|
|
|
|
COMMIT;
|
|
|
|
-- ============================================
|
|
-- VERIFICATION QUERIES
|
|
-- ============================================
|
|
-- \d processed_game
|
|
|
|
-- ============================================
|
|
-- ROLLBACK (if needed)
|
|
-- ============================================
|
|
-- DROP TABLE IF EXISTS processed_game;
|