from __future__ import annotations import path_setup # noqa: F401 # adds shared/ to sys.path import sx.sx_components as sx_components # noqa: F401 # ensure Hypercorn --reload watches this file from pathlib import Path from types import SimpleNamespace from quart import g, abort, request from jinja2 import FileSystemLoader, ChoiceLoader from shared.infrastructure.factory import create_base_app from bp import ( register_orders, register_order, register_checkout, register_fragments, register_actions, register_data, ) async def orders_context() -> dict: """Orders app context processor.""" from shared.infrastructure.context import base_context from shared.infrastructure.cart_identity import current_cart_identity from shared.infrastructure.fragments import fetch_fragments ctx = await base_context() ctx["menu_items"] = [] user = getattr(g, "user", None) ident = current_cart_identity() cart_params = {} if ident["user_id"] is not None: cart_params["user_id"] = ident["user_id"] if ident["session_id"] is not None: cart_params["session_id"] = ident["session_id"] cart_mini, auth_menu, nav_tree = await fetch_fragments([ ("cart", "cart-mini", cart_params or None), ("account", "auth-menu", {"email": user.email} if user else None), ("blog", "nav-tree", {"app_name": "orders", "path": request.path}), ]) ctx["cart_mini"] = cart_mini ctx["auth_menu"] = auth_menu ctx["nav_tree"] = nav_tree return ctx def _make_page_config(raw: dict) -> SimpleNamespace: """Convert a page-config JSON dict to a namespace for SumUp helpers.""" return SimpleNamespace(**raw) def create_app() -> "Quart": from services import register_domain_services app = create_base_app( "orders", context_fn=orders_context, domain_services_fn=register_domain_services, ) # App-specific templates override shared templates app_templates = str(Path(__file__).resolve().parent / "templates") app.jinja_loader = ChoiceLoader([ FileSystemLoader(app_templates), app.jinja_loader, ]) # Load orders-specific s-expression components from sx.sx_components import load_orders_components load_orders_components() app.register_blueprint(register_fragments()) app.register_blueprint(register_actions()) app.register_blueprint(register_data()) # Orders list at / app.register_blueprint(register_orders(url_prefix="/")) # Checkout webhook + return app.register_blueprint(register_checkout()) # --- 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.""" import logging from datetime import datetime, timezone, timedelta from sqlalchemy import select as sel from shared.db.session import get_session from shared.models.order import Order from services.check_sumup_status import check_sumup_status log = logging.getLogger("orders.reconcile") try: async with get_session() as sess: async with sess.begin(): cutoff = datetime.now(timezone.utc) - timedelta(minutes=2) result = await sess.execute( sel(Order) .where( Order.status == "pending", Order.sumup_checkout_id.isnot(None), Order.created_at < cutoff, ) .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 app = create_app()