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 <noreply@anthropic.com>
This commit is contained in:
jtricerolph 2026-07-21 16:50:09 +00:00
parent 4bad66c91a
commit 455396c965
2 changed files with 7 additions and 1 deletions

View file

@ -384,8 +384,11 @@ async def get_revenue_forecast(
covers_by_date = {c["date"]: c for c in covers_data.get("data", [])} covers_by_date = {c["date"]: c for c in covers_data.get("data", [])}
except Exception as e: except Exception as e:
logger.warning(f"Covers forecast failed: {e}") logger.warning(f"Covers forecast failed: {e}")
await db.rollback()
covers_by_date = {} 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 averages from recent 8 weeks — fallback when covers gives zero for future dates
dow_result = await db.execute( dow_result = await db.execute(

View file

@ -619,6 +619,7 @@ async def forecast_covers_for_date(
} }
except Exception as e: except Exception as e:
logger.warning(f"Pickupv2 forecast failed for {night_before}: {e}") logger.warning(f"Pickupv2 forecast failed for {night_before}: {e}")
await db.rollback()
# Fallback to prior year pattern # Fallback to prior year pattern
breakfast_pickup = max(0, hotel_guests_prior - hotel_guests_otb) breakfast_pickup = max(0, hotel_guests_prior - hotel_guests_otb)
breakfast_forecast = breakfast_otb + breakfast_pickup 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) pickup_rooms = pickupv2_dinner.get('pickup_rooms_total', 0)
except Exception as e: except Exception as e:
logger.warning(f"Pickupv2 forecast failed for dinner {target_date}: {e}") logger.warning(f"Pickupv2 forecast failed for dinner {target_date}: {e}")
await db.rollback()
# Calculate forecasted hotel guests (OTB + pickup) # Calculate forecasted hotel guests (OTB + pickup)
pickup_guests = pickup_rooms * guests_per_room pickup_guests = pickup_rooms * guests_per_room
@ -840,6 +842,7 @@ async def forecast_covers_range(
forecasts.append(day_forecast) forecasts.append(day_forecast)
except Exception as e: except Exception as e:
logger.warning(f"Failed to forecast covers for {current}: {e}") logger.warning(f"Failed to forecast covers for {current}: {e}")
await db.rollback()
current += timedelta(days=1) current += timedelta(days=1)