From c8d927bf72e47710da4d2192433be758bca54f71 Mon Sep 17 00:00:00 2001 From: giles Date: Tue, 10 Feb 2026 20:49:45 +0000 Subject: [PATCH] feat: per-page SumUp credentials in checkout flow (Phase 3) - Add resolve_page_config() to determine PageConfig from cart/calendar context - Set page_config_id on Order during checkout - Pass page_config to SumUp create_checkout and build_sumup_reference - check_sumup_status uses order.page_config for per-page credential resolution - Fix: use session.flush() instead of g.s.flush() in check_sumup_status Co-Authored-By: Claude Opus 4.6 --- bp/cart/routes.py | 19 +++++++++- bp/cart/services/check_sumup_status.py | 9 +++-- bp/cart/services/checkout.py | 51 ++++++++++++++++++++++++-- 3 files changed, 70 insertions(+), 9 deletions(-) diff --git a/bp/cart/routes.py b/bp/cart/routes.py index 489b576..902cba5 100644 --- a/bp/cart/routes.py +++ b/bp/cart/routes.py @@ -21,6 +21,7 @@ from .services import ( from .services.checkout import ( find_or_create_cart_item, create_order_from_cart, + resolve_page_config, build_sumup_description, build_sumup_reference, build_webhook_url, @@ -102,6 +103,17 @@ def register(url_prefix: str) -> Blueprint: if cart_total <= 0: return redirect(url_for("cart.view_cart")) + # Resolve per-page credentials + try: + page_config = await resolve_page_config(g.s, cart, calendar_entries) + except ValueError as e: + html = await render_template( + "_types/cart/checkout_error.html", + order=None, + error=str(e), + ) + return await make_response(html, 400) + # Create order from cart ident = current_cart_identity() order = await create_order_from_cart( @@ -114,9 +126,13 @@ def register(url_prefix: str) -> Blueprint: calendar_amount, ) + # Set page_config on order if resolved + if page_config: + order.page_config_id = page_config.id + # Build SumUp checkout details redirect_url = url_for("cart.checkout_return", order_id=order.id, _external=True) - order.sumup_reference = build_sumup_reference(order.id) + order.sumup_reference = build_sumup_reference(order.id, page_config=page_config) description = build_sumup_description(cart, order.id) webhook_base_url = url_for("cart.checkout_webhook", order_id=order.id, _external=True) @@ -127,6 +143,7 @@ def register(url_prefix: str) -> Blueprint: redirect_url=redirect_url, webhook_url=webhook_url, description=description, + page_config=page_config, ) await clear_cart_for_order(g.s, order) diff --git a/bp/cart/services/check_sumup_status.py b/bp/cart/services/check_sumup_status.py index afbf2cb..61b9f67 100644 --- a/bp/cart/services/check_sumup_status.py +++ b/bp/cart/services/check_sumup_status.py @@ -1,10 +1,12 @@ from suma_browser.app.payments.sumup import get_checkout as sumup_get_checkout from sqlalchemy import update -from models.calendars import CalendarEntry # NEW +from models.calendars import CalendarEntry async def check_sumup_status(session, order): - checkout_data = await sumup_get_checkout(order.sumup_checkout_id) + # Use order's page_config for per-page SumUp credentials + page_config = getattr(order, "page_config", None) + checkout_data = await sumup_get_checkout(order.sumup_checkout_id, page_config=page_config) order.sumup_status = checkout_data.get("status") or order.sumup_status sumup_status = (order.sumup_status or "").upper() @@ -26,10 +28,9 @@ async def check_sumup_status(session, order): .where(*filters) .values(state="provisional") ) - # also clear cart for this user/session if it wasn't already elif sumup_status == "FAILED": order.status = "failed" else: order.status = sumup_status.lower() or order.status - await g.s.flush() + await session.flush() diff --git a/bp/cart/services/checkout.py b/bp/cart/services/checkout.py index 3762ce2..1ca165d 100644 --- a/bp/cart/services/checkout.py +++ b/bp/cart/services/checkout.py @@ -9,7 +9,9 @@ from sqlalchemy.orm import selectinload from models.market import Product, CartItem from models.order import Order, OrderItem -from models.calendars import CalendarEntry +from models.calendars import CalendarEntry, Calendar +from models.page_config import PageConfig +from models.market_place import MarketPlace from config import config @@ -57,6 +59,44 @@ async def find_or_create_cart_item( return cart_item +async def resolve_page_config( + session: AsyncSession, + cart: list[CartItem], + calendar_entries: list[CalendarEntry], +) -> Optional["PageConfig"]: + """Determine the PageConfig for this order. + + Returns PageConfig or None (use global credentials). + Raises ValueError if items span multiple pages. + """ + post_ids: set[int] = set() + + # From cart items via market_place + for ci in cart: + if ci.market_place_id: + mp = await session.get(MarketPlace, ci.market_place_id) + if mp: + post_ids.add(mp.post_id) + + # From calendar entries via calendar + for entry in calendar_entries: + cal = await session.get(Calendar, entry.calendar_id) + if cal and cal.post_id: + post_ids.add(cal.post_id) + + if len(post_ids) > 1: + raise ValueError("Cannot checkout items from multiple pages") + + if not post_ids: + return None # global credentials + + post_id = post_ids.pop() + pc = (await session.execute( + select(PageConfig).where(PageConfig.post_id == post_id) + )).scalar_one_or_none() + return pc + + async def create_order_from_cart( session: AsyncSession, cart: list[CartItem], @@ -139,10 +179,13 @@ def build_sumup_description(cart: list[CartItem], order_id: int) -> str: return f"Order {order_id} ({item_count} item{'s' if item_count != 1 else ''}): {summary}" -def build_sumup_reference(order_id: int) -> str: +def build_sumup_reference(order_id: int, page_config=None) -> str: """Build a SumUp reference with configured prefix.""" - sumup_cfg = config().get("sumup", {}) or {} - prefix = sumup_cfg.get("checkout_reference_prefix", "") + if page_config and page_config.sumup_checkout_prefix: + prefix = page_config.sumup_checkout_prefix + else: + sumup_cfg = config().get("sumup", {}) or {} + prefix = sumup_cfg.get("checkout_reference_prefix", "") return f"{prefix}{order_id}"