From 455396c965a6afdced20c69be8f1986eb689db03 Mon Sep 17 00:00:00 2001 From: jtricerolph Date: Tue, 21 Jul 2026 16:50:09 +0000 Subject: [PATCH] Fix cascading InFailedSQLTransactionError in covers forecast When forecast_rooms_for_date throws inside covers_model, asyncpg aborts the whole transaction. The exception was being caught with a warning but no rollback, so every subsequent query in the same request failed. Added db.rollback() in all three exception handlers (breakfast pickupv2, dinner pickupv2, per-day loop) and moved public.py's rollback into a finally block so it always fires even when covers_model returns normally with a corrupted transaction. Co-Authored-By: Claude Sonnet 4.6 --- backend/api/public.py | 5 ++++- backend/services/forecasting/covers_model.py | 3 +++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/api/public.py b/backend/api/public.py index 330860c..9faef9a 100644 --- a/backend/api/public.py +++ b/backend/api/public.py @@ -384,8 +384,11 @@ async def get_revenue_forecast( covers_by_date = {c["date"]: c for c in covers_data.get("data", [])} except Exception as e: logger.warning(f"Covers forecast failed: {e}") - await db.rollback() covers_by_date = {} + finally: + # covers_model catches exceptions internally and may leave the transaction + # aborted without raising — always rollback so subsequent queries succeed + await db.rollback() # DOW averages from recent 8 weeks — fallback when covers gives zero for future dates dow_result = await db.execute( diff --git a/backend/services/forecasting/covers_model.py b/backend/services/forecasting/covers_model.py index 9f26646..2ca615b 100644 --- a/backend/services/forecasting/covers_model.py +++ b/backend/services/forecasting/covers_model.py @@ -619,6 +619,7 @@ async def forecast_covers_for_date( } except Exception as e: logger.warning(f"Pickupv2 forecast failed for {night_before}: {e}") + await db.rollback() # Fallback to prior year pattern breakfast_pickup = max(0, hotel_guests_prior - hotel_guests_otb) breakfast_forecast = breakfast_otb + breakfast_pickup @@ -748,6 +749,7 @@ async def forecast_covers_for_date( pickup_rooms = pickupv2_dinner.get('pickup_rooms_total', 0) except Exception as e: logger.warning(f"Pickupv2 forecast failed for dinner {target_date}: {e}") + await db.rollback() # Calculate forecasted hotel guests (OTB + pickup) pickup_guests = pickup_rooms * guests_per_room @@ -840,6 +842,7 @@ async def forecast_covers_range( forecasts.append(day_forecast) except Exception as e: logger.warning(f"Failed to forecast covers for {current}: {e}") + await db.rollback() current += timedelta(days=1)