""" Orders service s-expression page components. Each function renders a complete page section (full page, OOB, or pagination) using shared s-expression components. Called from route handlers in place of ``render_template()``. """ from __future__ import annotations import os from typing import Any from shared.sx.jinja_bridge import load_service_components from shared.sx.helpers import ( call_url, sx_call, SxExpr, ) from shared.infrastructure.urls import market_product_url, cart_url # Load orders-specific .sx components + handlers at import time load_service_components(os.path.dirname(os.path.dirname(__file__)), service_name="orders") # --------------------------------------------------------------------------- # Header helpers (shared auth + orders-specific) — sx-native # --------------------------------------------------------------------------- def _auth_nav_sx(ctx: dict) -> str: """Auth section desktop nav items as sx.""" parts = [ sx_call("nav-link", href=call_url(ctx, "account_url", "/newsletters/"), label="newsletters", select_colours=ctx.get("select_colours", ""), ), ] account_nav = ctx.get("account_nav") if account_nav: parts.append(str(account_nav)) return "(<> " + " ".join(parts) + ")" def _auth_header_sx(ctx: dict, *, oob: bool = False) -> str: """Build the account section header row as sx.""" return sx_call( "menu-row-sx", id="auth-row", level=1, colour="sky", link_href=call_url(ctx, "account_url", "/"), link_label="account", icon="fa-solid fa-user", nav=SxExpr(_auth_nav_sx(ctx)), child_id="auth-header-child", oob=oob, ) def _orders_header_sx(ctx: dict, list_url: str) -> str: """Build the orders section header row as sx.""" return sx_call( "menu-row-sx", id="orders-row", level=2, colour="sky", link_href=list_url, link_label="Orders", icon="fa fa-gbp", child_id="orders-header-child", ) # --------------------------------------------------------------------------- # Orders list rendering # --------------------------------------------------------------------------- def _status_pill_cls(status: str) -> str: """Return Tailwind classes for order status pill.""" sl = status.lower() if sl == "paid": return "border-emerald-300 bg-emerald-50 text-emerald-700" if sl in ("failed", "cancelled"): return "border-rose-300 bg-rose-50 text-rose-700" return "border-stone-300 bg-stone-50 text-stone-700" def _order_row_data(order: Any, detail_url: str) -> dict: """Extract display data from an order model object.""" status = order.status or "pending" pill = _status_pill_cls(status) created = order.created_at.strftime("%-d %b %Y, %H:%M") if order.created_at else "\u2014" total = f"{order.currency or 'GBP'} {order.total_amount or 0:.2f}" return dict( oid=f"#{order.id}", created=created, desc=order.description or "", total=total, pill_desktop=f"inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] sm:text-xs {pill}", pill_mobile=f"inline-flex items-center rounded-full border px-2 py-0.5 text-[11px] {pill}", status=status, url=detail_url, ) def _orders_rows_sx(orders: list, page: int, total_pages: int, url_for_fn: Any, qs_fn: Any) -> str: """S-expression wire format for order rows (client renders).""" from shared.utils import route_prefix pfx = route_prefix() parts = [] for o in orders: d = _order_row_data(o, pfx + url_for_fn("orders.defpage_order_detail", order_id=o.id)) parts.append(sx_call("order-row-desktop", oid=d["oid"], created=d["created"], desc=d["desc"], total=d["total"], pill=d["pill_desktop"], status=d["status"], url=d["url"])) parts.append(sx_call("order-row-mobile", oid=d["oid"], created=d["created"], total=d["total"], pill=d["pill_mobile"], status=d["status"], url=d["url"])) if page < total_pages: next_url = pfx + url_for_fn("orders.orders_rows") + qs_fn(page=page + 1) parts.append(sx_call("infinite-scroll", url=next_url, page=page, total_pages=total_pages, id_prefix="orders", colspan=5)) else: parts.append(sx_call("order-end-row")) return "(<> " + " ".join(parts) + ")" def _orders_main_panel_sx(orders: list, rows_sx: str) -> str: """Main panel with table or empty state (sx).""" if not orders: return sx_call("order-empty-state") return sx_call("order-table", rows=SxExpr(rows_sx)) def _orders_summary_sx(ctx: dict) -> str: """Filter section for orders list (sx).""" return sx_call("order-list-header", search_mobile=SxExpr(search_mobile_sx(ctx))) # --------------------------------------------------------------------------- # Public API: orders list # --------------------------------------------------------------------------- # --------------------------------------------------------------------------- # Single order detail # --------------------------------------------------------------------------- def _order_items_sx(order: Any) -> str: """Render order items list as sx.""" if not order or not order.items: return "" items = [] for item in order.items: prod_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") items.append(sx_call( "order-item-row", href=prod_url, img=SxExpr(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 = "(<> " + " ".join(items) + ")" return sx_call("order-items-panel", items=SxExpr(items_sx)) def _calendar_items_sx(calendar_entries: list | None) -> str: """Render calendar bookings for an order as sx.""" if not calendar_entries: return "" items = [] 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')}" items.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}", )) items_sx = "(<> " + " ".join(items) + ")" return sx_call("order-calendar-section", items=SxExpr(items_sx)) def _order_main_sx(order: Any, calendar_entries: list | None) -> str: """Main panel for single order detail (sx).""" 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 = _order_items_sx(order) calendar = _calendar_items_sx(calendar_entries) return sx_call( "order-detail-panel", summary=SxExpr(summary), items=SxExpr(items) if items else None, calendar=SxExpr(calendar) if calendar else None, ) def _order_filter_sx(order: Any, list_url: str, recheck_url: str, pay_url: str, csrf_token: str) -> str: """Filter section for single order detail (sx).""" created = order.created_at.strftime("%-d %b %Y, %H:%M") if order.created_at else "\u2014" status = order.status or "pending" pay = "" if status != "paid": pay = sx_call("order-pay-btn", url=pay_url) return sx_call( "order-detail-filter", info=f"Placed {created} \u00b7 Status: {status}", list_url=list_url, recheck_url=recheck_url, csrf=csrf_token, pay=SxExpr(pay) if pay else None, ) # --------------------------------------------------------------------------- # Public API: Checkout error # --------------------------------------------------------------------------- def _checkout_error_filter_sx() -> str: return sx_call("checkout-error-header") def _checkout_error_content_sx(error: str | None, order: Any | None) -> str: err_msg = error or "Unexpected error while creating the hosted checkout session." order_sx = "" if order: order_sx = sx_call("checkout-error-order-id", oid=f"#{order.id}") back_url = cart_url("/") return sx_call( "checkout-error-content", msg=err_msg, order=SxExpr(order_sx) if order_sx else None, back_url=back_url, ) async def render_checkout_error_page(ctx: dict, error: str | None = None, order: Any | None = None) -> str: """Full page: checkout error (sx wire format).""" hdr = root_header_sx(ctx) inner = _auth_header_sx(ctx) hdr = "(<> " + hdr + " " + header_child_sx(inner) + ")" filt = _checkout_error_filter_sx() content = _checkout_error_content_sx(error, order) return full_page_sx(ctx, header_rows=hdr, filter=filt, content=content) # --------------------------------------------------------------------------- # Public API: Checkout return # --------------------------------------------------------------------------- def _ticket_items_sx(order_tickets: list | None) -> str: """Render ticket items for an order as sx.""" if not order_tickets: return "" items = [] 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" – {tk.entry_end_at.strftime('%-d %b %Y, %H:%M')}" items.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"£{tk.price or 0:.2f}", )) items_sx = "(<> " + " ".join(items) + ")" return sx_call("checkout-return-tickets", items=SxExpr(items_sx)) async def render_checkout_return_page(ctx: dict, order: Any | None, status: str, calendar_entries: list | None = None, order_tickets: list | None = None) -> str: """Full page: checkout return after SumUp payment (sx wire format).""" 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 = _order_items_sx(order) calendar = _calendar_items_sx(calendar_entries) tickets = _ticket_items_sx(order_tickets) 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=SxExpr(summary), items=SxExpr(items) if items else None, calendar=SxExpr(calendar) if calendar else None, tickets=SxExpr(tickets) if tickets else None, status_message=SxExpr(status_msg) if status_msg else None, ) hdr = root_header_sx(ctx) inner = _auth_header_sx(ctx) hdr = "(<> " + hdr + " " + header_child_sx(inner) + ")" return full_page_sx(ctx, header_rows=hdr, filter=filt, content=content)