From e5de05dd798fcc8813a446448fc7fe70efab9300 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 21 Feb 2026 21:22:59 +0000 Subject: [PATCH] Add startup reconciliation for stale pending orders On boot, check SumUp API for any orders stuck in 'pending' that have a checkout ID. This handles missed webhooks (service down, CSRF rejection, etc). Runs once via before_serving hook, limited to 50 orders older than 2 minutes. Co-Authored-By: Claude Opus 4.6 --- app.py | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/app.py b/app.py index 4084af0..e3e1122 100644 --- a/app.py +++ b/app.py @@ -168,6 +168,58 @@ def create_app() -> "Quart": url_prefix="/", ) + # --- Reconcile stale pending orders on startup --- + @app.before_serving + async def _reconcile_pending_orders(): + """Check SumUp status for orders stuck in 'pending' with a checkout ID. + + Handles the case where SumUp webhooks fired while the service was down + or were rejected (e.g. CSRF). Runs once on boot. + """ + import logging + from datetime import datetime, timezone, timedelta + from sqlalchemy import select + from sqlalchemy.orm import selectinload + from shared.db.session import get_session + from shared.models.order import Order + from bp.cart.services.check_sumup_status import check_sumup_status + + log = logging.getLogger("cart.reconcile") + + try: + async with get_session() as sess: + async with sess.begin(): + # Orders that are pending, have a SumUp checkout, and are + # older than 2 minutes (avoid racing with in-flight checkouts) + cutoff = datetime.now(timezone.utc) - timedelta(minutes=2) + result = await sess.execute( + select(Order) + .where( + Order.status == "pending", + Order.sumup_checkout_id.isnot(None), + Order.created_at < cutoff, + ) + .options(selectinload(Order.page_config)) + .limit(50) + ) + stale_orders = result.scalars().all() + + if not stale_orders: + return + + log.info("Reconciling %d stale pending orders", len(stale_orders)) + for order in stale_orders: + try: + await check_sumup_status(sess, order) + log.info( + "Order %d reconciled: %s", + order.id, order.status, + ) + except Exception: + log.exception("Failed to reconcile order %d", order.id) + except Exception: + log.exception("Order reconciliation failed") + return app