"""Checkout webhook + return routes (moved from cart/bp/cart/global_routes.py).""" from __future__ import annotations from quart import Blueprint, g, request, make_response from sqlalchemy import select from shared.models.order import Order from shared.browser.app.csrf import csrf_exempt from services.checkout import validate_webhook_secret, get_order_with_details from services.check_sumup_status import check_sumup_status async def _render_checkout_return(ctx: dict, order=None, status: str = "", calendar_entries=None, order_tickets=None) -> str: """Render checkout return page — replaces sx_components helper.""" from shared.sx.helpers import ( sx_call, root_header_sx, header_child_sx, full_page_sx, call_url, ) from shared.sx.parser import SxExpr from shared.infrastructure.urls import market_product_url filt = sx_call("checkout-return-header", status=status) if not order: content = sx_call("checkout-return-missing") else: summary = sx_call("order-summary-card", order_id=order.id, created_at=order.created_at.strftime("%-d %b %Y, %H:%M") if order.created_at else None, description=order.description, status=order.status, currency=order.currency, total_amount=f"{order.total_amount:.2f}" if order.total_amount else None, ) items = "" if order.items: item_parts = [] for item in order.items: product_url = market_product_url(item.product_slug) if item.product_image: img = sx_call("order-item-image", src=item.product_image, alt=item.product_title or "Product image") else: img = sx_call("order-item-no-image") item_parts.append(sx_call("order-item-row", href=product_url, img=img, title=item.product_title or "Unknown product", pid=f"Product ID: {item.product_id}", qty=f"Qty: {item.quantity}", price=f"{item.currency or order.currency or 'GBP'} {item.unit_price or 0:.2f}", )) items = sx_call("order-items-panel", items=SxExpr("(<> " + " ".join(item_parts) + ")")) calendar = "" if calendar_entries: cal_parts = [] for e in calendar_entries: st = e.state or "" pill = ( "bg-emerald-100 text-emerald-800" if st == "confirmed" else "bg-amber-100 text-amber-800" if st == "provisional" else "bg-blue-100 text-blue-800" if st == "ordered" else "bg-stone-100 text-stone-700" ) ds = e.start_at.strftime("%-d %b %Y, %H:%M") if e.start_at else "" if e.end_at: ds += f" \u2013 {e.end_at.strftime('%-d %b %Y, %H:%M')}" cal_parts.append(sx_call("order-calendar-entry", name=e.name, pill=f"inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium {pill}", status=st.capitalize(), date_str=ds, cost=f"\u00a3{e.cost or 0:.2f}", )) calendar = sx_call("order-calendar-section", items=SxExpr("(<> " + " ".join(cal_parts) + ")")) tickets = "" if order_tickets: tk_parts = [] for tk in order_tickets: st = tk.state or "" pill = ( "bg-emerald-100 text-emerald-800" if st == "confirmed" else "bg-amber-100 text-amber-800" if st == "reserved" else "bg-blue-100 text-blue-800" if st == "checked_in" else "bg-stone-100 text-stone-700" ) pill_cls = f"inline-flex items-center rounded-full px-2 py-0.5 text-[11px] font-medium {pill}" ds = tk.entry_start_at.strftime("%-d %b %Y, %H:%M") if tk.entry_start_at else "" if tk.entry_end_at: ds += f" \u2013 {tk.entry_end_at.strftime('%-d %b %Y, %H:%M')}" tk_parts.append(sx_call("checkout-return-ticket", name=tk.entry_name, pill=pill_cls, state=st.replace("_", " ").capitalize(), type_name=tk.ticket_type_name or None, date_str=ds, code=tk.code, price=f"\u00a3{tk.price or 0:.2f}", )) tickets = sx_call("checkout-return-tickets", items=SxExpr("(<> " + " ".join(tk_parts) + ")")) status_msg = "" if order.status == "failed": status_msg = sx_call("checkout-return-failed", order_id=order.id) elif order.status == "paid": status_msg = sx_call("checkout-return-paid") content = sx_call("checkout-return-content", summary=summary, items=items or None, calendar=calendar or None, tickets=tickets or None, status_message=status_msg or None, ) account_url = call_url(ctx, "account_url", "") auth_hdr = sx_call("auth-header-row", account_url=account_url) hdr = "(<> " + await root_header_sx(ctx) + " " + await header_child_sx(auth_hdr) + ")" return await full_page_sx(ctx, header_rows=hdr, filter=filt, content=content) def register() -> Blueprint: bp = Blueprint("checkout", __name__, url_prefix="/checkout") @csrf_exempt @bp.post("/webhook//") async def checkout_webhook(order_id: int): """Webhook endpoint for SumUp CHECKOUT_STATUS_CHANGED events.""" if not validate_webhook_secret(request.args.get("token")): return "", 204 try: payload = await request.get_json() except Exception: payload = None if not isinstance(payload, dict): return "", 204 if payload.get("event_type") != "CHECKOUT_STATUS_CHANGED": return "", 204 checkout_id = payload.get("id") if not checkout_id: return "", 204 result = await g.s.execute(select(Order).where(Order.id == order_id)) order = result.scalar_one_or_none() if not order: return "", 204 if order.sumup_checkout_id and order.sumup_checkout_id != checkout_id: return "", 204 try: await check_sumup_status(g.s, order) except Exception: pass return "", 204 @bp.get("/return//") async def checkout_return(order_id: int): """Handle the browser returning from SumUp after payment.""" from shared.sx.page import get_template_context order = await get_order_with_details(g.s, order_id) if not order: tctx = await get_template_context() html = await _render_checkout_return(tctx, order=None, status="missing") return await make_response(html) if order.page_config_id: from shared.infrastructure.data_client import fetch_data from shared.contracts.dtos import CalendarEntryDTO, TicketDTO, dto_from_dict raw_pc = await fetch_data("blog", "page-config-by-id", params={"id": order.page_config_id}, required=False) post = await fetch_data("blog", "post-by-id", params={"id": raw_pc["container_id"]}, required=False) if raw_pc else None if post: g.page_slug = post["slug"] mps = await fetch_data( "market", "marketplaces-for-container", params={"type": "page", "id": post["id"]}, required=False, ) or [] if mps: g.market_slug = mps[0].get("slug") if order.sumup_checkout_id: try: await check_sumup_status(g.s, order) except Exception: pass status = (order.status or "pending").lower() from shared.infrastructure.data_client import fetch_data from shared.contracts.dtos import CalendarEntryDTO, TicketDTO, dto_from_dict raw_entries = await fetch_data("events", "entries-for-order", params={"order_id": order.id}, required=False) or [] calendar_entries = [dto_from_dict(CalendarEntryDTO, e) for e in raw_entries] raw_tickets = await fetch_data("events", "tickets-for-order", params={"order_id": order.id}, required=False) or [] order_tickets = [dto_from_dict(TicketDTO, t) for t in raw_tickets] await g.s.flush() tctx = await get_template_context() html = await _render_checkout_return( tctx, order=order, status=status, calendar_entries=calendar_entries, order_tickets=order_tickets, ) return await make_response(html) return bp