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 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-10 20:49:45 +00:00
parent 6729b0f158
commit c8d927bf72
3 changed files with 70 additions and 9 deletions

View File

@@ -21,6 +21,7 @@ from .services import (
from .services.checkout import ( from .services.checkout import (
find_or_create_cart_item, find_or_create_cart_item,
create_order_from_cart, create_order_from_cart,
resolve_page_config,
build_sumup_description, build_sumup_description,
build_sumup_reference, build_sumup_reference,
build_webhook_url, build_webhook_url,
@@ -102,6 +103,17 @@ def register(url_prefix: str) -> Blueprint:
if cart_total <= 0: if cart_total <= 0:
return redirect(url_for("cart.view_cart")) 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 # Create order from cart
ident = current_cart_identity() ident = current_cart_identity()
order = await create_order_from_cart( order = await create_order_from_cart(
@@ -114,9 +126,13 @@ def register(url_prefix: str) -> Blueprint:
calendar_amount, calendar_amount,
) )
# Set page_config on order if resolved
if page_config:
order.page_config_id = page_config.id
# Build SumUp checkout details # Build SumUp checkout details
redirect_url = url_for("cart.checkout_return", order_id=order.id, _external=True) 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) description = build_sumup_description(cart, order.id)
webhook_base_url = url_for("cart.checkout_webhook", order_id=order.id, _external=True) 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, redirect_url=redirect_url,
webhook_url=webhook_url, webhook_url=webhook_url,
description=description, description=description,
page_config=page_config,
) )
await clear_cart_for_order(g.s, order) await clear_cart_for_order(g.s, order)

View File

@@ -1,10 +1,12 @@
from suma_browser.app.payments.sumup import get_checkout as sumup_get_checkout from suma_browser.app.payments.sumup import get_checkout as sumup_get_checkout
from sqlalchemy import update from sqlalchemy import update
from models.calendars import CalendarEntry # NEW from models.calendars import CalendarEntry
async def check_sumup_status(session, order): 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 order.sumup_status = checkout_data.get("status") or order.sumup_status
sumup_status = (order.sumup_status or "").upper() sumup_status = (order.sumup_status or "").upper()
@@ -26,10 +28,9 @@ async def check_sumup_status(session, order):
.where(*filters) .where(*filters)
.values(state="provisional") .values(state="provisional")
) )
# also clear cart for this user/session if it wasn't already
elif sumup_status == "FAILED": elif sumup_status == "FAILED":
order.status = "failed" order.status = "failed"
else: else:
order.status = sumup_status.lower() or order.status order.status = sumup_status.lower() or order.status
await g.s.flush() await session.flush()

View File

@@ -9,7 +9,9 @@ from sqlalchemy.orm import selectinload
from models.market import Product, CartItem from models.market import Product, CartItem
from models.order import Order, OrderItem 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 from config import config
@@ -57,6 +59,44 @@ async def find_or_create_cart_item(
return 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( async def create_order_from_cart(
session: AsyncSession, session: AsyncSession,
cart: list[CartItem], 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}" 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.""" """Build a SumUp reference with configured prefix."""
sumup_cfg = config().get("sumup", {}) or {} if page_config and page_config.sumup_checkout_prefix:
prefix = sumup_cfg.get("checkout_reference_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}" return f"{prefix}{order_id}"