Move payments admin from events to cart service
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m40s

Payments config (SumUp credentials per page) is a cart concern since all
checkouts go through the cart service. Moves it from events.rose-ash.com
to cart.rose-ash.com/<page_slug>/admin/payments/ and adds a cart admin
overview page at /<page_slug>/admin/.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 18:15:35 +00:00
parent 5957bd8941
commit ee41e30d5b
12 changed files with 1151 additions and 173 deletions

View File

@@ -16,6 +16,7 @@ from bp import (
register_cart_overview,
register_page_cart,
register_cart_global,
register_page_admin,
register_fragments,
register_actions,
register_data,
@@ -195,6 +196,12 @@ def create_app() -> "Quart":
url_prefix="/",
)
# Page admin at /<page_slug>/admin/ (before page_cart catch-all)
app.register_blueprint(
register_page_admin(),
url_prefix="/<page_slug>/admin",
)
# Page cart at /<page_slug>/ (dynamic, matched last)
app.register_blueprint(
register_page_cart(url_prefix="/"),

View File

@@ -1,6 +1,7 @@
from .cart.overview_routes import register as register_cart_overview
from .cart.page_routes import register as register_page_cart
from .cart.global_routes import register as register_cart_global
from .page_admin.routes import register as register_page_admin
from .fragments import register_fragments
from .actions import register_actions
from .data import register_data

View File

View File

@@ -0,0 +1,83 @@
from __future__ import annotations
from quart import (
make_response, Blueprint, g, request
)
from shared.infrastructure.actions import call_action
from shared.infrastructure.data_client import fetch_data
from shared.browser.app.authz import require_admin
from shared.browser.app.utils.htmx import is_htmx_request
def register():
bp = Blueprint("page_admin", __name__)
@bp.get("/")
@require_admin
async def admin(**kwargs):
from shared.sexp.page import get_template_context
from sexp.sexp_components import render_cart_admin_page, render_cart_admin_oob
ctx = await get_template_context()
page_post = getattr(g, "page_post", None)
if not is_htmx_request():
html = await render_cart_admin_page(ctx, page_post)
else:
html = await render_cart_admin_oob(ctx, page_post)
return await make_response(html)
@bp.get("/payments/")
@require_admin
async def payments(**kwargs):
from shared.sexp.page import get_template_context
from sexp.sexp_components import render_cart_payments_page, render_cart_payments_oob
ctx = await get_template_context()
page_post = getattr(g, "page_post", None)
if not is_htmx_request():
html = await render_cart_payments_page(ctx, page_post)
else:
html = await render_cart_payments_oob(ctx, page_post)
return await make_response(html)
@bp.put("/payments/")
@require_admin
async def update_sumup(**kwargs):
"""Update SumUp credentials for this page (writes to blog's db_blog)."""
page_post = getattr(g, "page_post", None)
if not page_post:
return await make_response("Page not found", 404)
form = await request.form
merchant_code = (form.get("merchant_code") or "").strip()
api_key = (form.get("api_key") or "").strip()
checkout_prefix = (form.get("checkout_prefix") or "").strip()
payload = {
"container_type": "page",
"container_id": page_post.id,
"sumup_merchant_code": merchant_code,
"sumup_checkout_prefix": checkout_prefix,
}
if api_key:
payload["sumup_api_key"] = api_key
await call_action("blog", "update-page-config", payload=payload)
# Re-fetch page config to get fresh data
from types import SimpleNamespace
raw_pc = await fetch_data(
"blog", "page-config",
params={"container_type": "page", "container_id": page_post.id},
required=False,
)
g.page_config = SimpleNamespace(**raw_pc) if raw_pc else None
from shared.sexp.page import get_template_context
from sexp.sexp_components import render_cart_payments_panel
ctx = await get_template_context()
html = render_cart_payments_panel(ctx)
return await make_response(html)
return bp

21
cart/sexp/payments.sexpr Normal file
View File

@@ -0,0 +1,21 @@
;; Cart payments components
(defcomp ~cart-payments-panel (&key update-url csrf merchant-code placeholder input-cls sumup-configured checkout-prefix)
(section :class "p-4 max-w-lg mx-auto"
(div :id "payments-panel" :class "space-y-4 p-4 bg-white rounded-lg border border-stone-200"
(h3 :class "text-lg font-semibold text-stone-800"
(i :class "fa fa-credit-card text-purple-600 mr-1") " SumUp Payment")
(p :class "text-xs text-stone-400" "Configure per-page SumUp credentials. Leave blank to use the global merchant account.")
(form :hx-put update-url :hx-target "#payments-panel" :hx-swap "outerHTML" :hx-select "#payments-panel" :class "space-y-3"
(input :type "hidden" :name "csrf_token" :value csrf)
(div (label :class "block text-xs font-medium text-stone-600 mb-1" "Merchant Code")
(input :type "text" :name "merchant_code" :value merchant-code :placeholder "e.g. ME4J6100" :class input-cls))
(div (label :class "block text-xs font-medium text-stone-600 mb-1" "API Key")
(input :type "password" :name "api_key" :value "" :placeholder placeholder :class input-cls)
(when sumup-configured (p :class "text-xs text-stone-400 mt-0.5" "Key is set. Leave blank to keep current key.")))
(div (label :class "block text-xs font-medium text-stone-600 mb-1" "Checkout Reference Prefix")
(input :type "text" :name "checkout_prefix" :value checkout-prefix :placeholder "e.g. ROSE-" :class input-cls))
(button :type "submit" :class "px-4 py-1.5 text-sm font-medium text-white bg-purple-600 rounded hover:bg-purple-700 focus:ring-2 focus:ring-purple-500"
"Save SumUp Settings")
(when sumup-configured (span :class "ml-2 text-xs text-green-600"
(i :class "fa fa-check-circle") " Connected"))))))

View File

@@ -721,3 +721,126 @@ async def render_checkout_error_page(ctx: dict, error: str | None = None, order:
filt = _checkout_error_filter_html()
content = _checkout_error_content_html(error, order)
return full_page(ctx, header_rows_html=hdr, filter_html=filt, content_html=content)
# ---------------------------------------------------------------------------
# Page admin (/<page_slug>/admin/)
# ---------------------------------------------------------------------------
def _cart_page_admin_header_html(ctx: dict, page_post: Any, *, oob: bool = False) -> str:
"""Build the page-level admin header row."""
from quart import url_for
link_href = url_for("page_admin.admin")
return render("menu-row", id="page-admin-row", level=2, colour="sky",
link_href=link_href, link_label="admin", icon="fa fa-cog",
child_id="page-admin-header-child", oob=oob)
def _cart_payments_header_html(ctx: dict, *, oob: bool = False) -> str:
"""Build the payments section header row."""
from quart import url_for
link_href = url_for("page_admin.payments")
return render("menu-row", id="payments-row", level=3, colour="sky",
link_href=link_href, link_label="Payments",
icon="fa fa-credit-card",
child_id="payments-header-child", oob=oob)
def _cart_admin_main_panel_html(ctx: dict) -> str:
"""Admin overview panel — links to sub-admin pages."""
from quart import url_for
payments_href = url_for("page_admin.payments")
return (
'<div id="main-panel">'
'<div class="flex items-center justify-between p-3 border-b">'
'<span class="font-medium"><i class="fa fa-credit-card text-purple-600 mr-1"></i> Payments</span>'
f'<a href="{payments_href}" class="text-sm underline">configure</a>'
'</div>'
'</div>'
)
def _cart_payments_main_panel_html(ctx: dict) -> str:
"""Render SumUp payment config form."""
from quart import url_for
csrf_token = ctx.get("csrf_token")
csrf = csrf_token() if callable(csrf_token) else (csrf_token or "")
page_config = ctx.get("page_config")
sumup_configured = bool(page_config and getattr(page_config, "sumup_api_key", None))
merchant_code = (getattr(page_config, "sumup_merchant_code", None) or "") if page_config else ""
checkout_prefix = (getattr(page_config, "sumup_checkout_prefix", None) or "") if page_config else ""
update_url = url_for("page_admin.update_sumup")
placeholder = "--------" if sumup_configured else "sup_sk_..."
input_cls = "w-full px-3 py-1.5 text-sm border border-stone-300 rounded focus:ring-purple-500 focus:border-purple-500"
return render("cart-payments-panel",
update_url=update_url, csrf=csrf,
merchant_code=merchant_code, placeholder=placeholder,
input_cls=input_cls, sumup_configured=sumup_configured,
checkout_prefix=checkout_prefix)
# ---------------------------------------------------------------------------
# Public API: Cart page admin
# ---------------------------------------------------------------------------
async def render_cart_admin_page(ctx: dict, page_post: Any) -> str:
"""Full page: cart page admin overview."""
content = _cart_admin_main_panel_html(ctx)
hdr = root_header_html(ctx)
child = _page_cart_header_html(ctx, page_post) + _cart_page_admin_header_html(ctx, page_post)
hdr += render("cart-header-child-nested",
outer_html=_cart_header_html(ctx), inner_html=child)
return full_page(ctx, header_rows_html=hdr, content_html=content)
async def render_cart_admin_oob(ctx: dict, page_post: Any) -> str:
"""OOB response: cart page admin overview."""
content = _cart_admin_main_panel_html(ctx)
oobs = (
_cart_page_admin_header_html(ctx, page_post, oob=True)
+ render("cart-header-child-oob",
inner_html=_page_cart_header_html(ctx, page_post)
+ _cart_page_admin_header_html(ctx, page_post))
+ _cart_header_html(ctx, oob=True)
+ root_header_html(ctx, oob=True)
)
return oob_page(ctx, oobs_html=oobs, content_html=content)
# ---------------------------------------------------------------------------
# Public API: Cart payments admin
# ---------------------------------------------------------------------------
async def render_cart_payments_page(ctx: dict, page_post: Any) -> str:
"""Full page: payments config."""
content = _cart_payments_main_panel_html(ctx)
hdr = root_header_html(ctx)
admin_hdr = _cart_page_admin_header_html(ctx, page_post)
payments_hdr = _cart_payments_header_html(ctx)
child = _page_cart_header_html(ctx, page_post) + admin_hdr + payments_hdr
hdr += render("cart-header-child-nested",
outer_html=_cart_header_html(ctx), inner_html=child)
return full_page(ctx, header_rows_html=hdr, content_html=content)
async def render_cart_payments_oob(ctx: dict, page_post: Any) -> str:
"""OOB response: payments config."""
content = _cart_payments_main_panel_html(ctx)
admin_hdr = _cart_page_admin_header_html(ctx, page_post)
payments_hdr = _cart_payments_header_html(ctx)
oobs = (
_cart_payments_header_html(ctx, oob=True)
+ render("cart-header-child-oob",
inner_html=_page_cart_header_html(ctx, page_post)
+ admin_hdr + payments_hdr)
+ _cart_header_html(ctx, oob=True)
+ root_header_html(ctx, oob=True)
)
return oob_page(ctx, oobs_html=oobs, content_html=content)
def render_cart_payments_panel(ctx: dict) -> str:
"""Render the payments config panel for PUT response."""
return _cart_payments_main_panel_html(ctx)