"""Cart page data service — provides serialized dicts for .sx defpages.""" from __future__ import annotations from typing import Any def _serialize_cart_item(item: Any) -> dict: from quart import url_for from shared.infrastructure.urls import market_product_url p = item.product if hasattr(item, "product") else item slug = p.slug if hasattr(p, "slug") else "" unit_price = getattr(p, "special_price", None) or getattr(p, "regular_price", None) currency = getattr(p, "regular_price_currency", "GBP") or "GBP" return { "slug": slug, "title": p.title if hasattr(p, "title") else "", "image": p.image if hasattr(p, "image") else None, "brand": getattr(p, "brand", None), "is_deleted": getattr(item, "is_deleted", False), "unit_price": float(unit_price) if unit_price else None, "special_price": float(p.special_price) if getattr(p, "special_price", None) else None, "regular_price": float(p.regular_price) if getattr(p, "regular_price", None) else None, "currency": currency, "quantity": item.quantity, "product_id": p.id, "product_url": market_product_url(slug), "qty_url": url_for("cart_global.update_quantity", product_id=p.id), } def _serialize_cal_entry(e: Any) -> dict: name = getattr(e, "name", None) or getattr(e, "calendar_name", "") start = e.start_at if hasattr(e, "start_at") else "" end = getattr(e, "end_at", None) cost = getattr(e, "cost", 0) or 0 end_str = f" \u2013 {end}" if end else "" return { "name": name, "date_str": f"{start}{end_str}", "cost": float(cost), } def _serialize_ticket_group(tg: Any) -> dict: name = tg.entry_name if hasattr(tg, "entry_name") else tg.get("entry_name", "") tt_name = tg.ticket_type_name if hasattr(tg, "ticket_type_name") else tg.get("ticket_type_name", "") price = tg.price if hasattr(tg, "price") else tg.get("price", 0) quantity = tg.quantity if hasattr(tg, "quantity") else tg.get("quantity", 0) line_total = tg.line_total if hasattr(tg, "line_total") else tg.get("line_total", 0) entry_id = tg.entry_id if hasattr(tg, "entry_id") else tg.get("entry_id", "") tt_id = tg.ticket_type_id if hasattr(tg, "ticket_type_id") else tg.get("ticket_type_id", "") start_at = tg.entry_start_at if hasattr(tg, "entry_start_at") else tg.get("entry_start_at") end_at = tg.entry_end_at if hasattr(tg, "entry_end_at") else tg.get("entry_end_at") date_str = start_at.strftime("%-d %b %Y, %H:%M") if start_at else "" if end_at: date_str += f" \u2013 {end_at.strftime('%-d %b %Y, %H:%M')}" return { "entry_name": name, "ticket_type_name": tt_name or None, "price": float(price or 0), "quantity": quantity, "line_total": float(line_total or 0), "entry_id": entry_id, "ticket_type_id": tt_id or None, "date_str": date_str, } def _serialize_page_group(grp: Any) -> dict | None: post = grp.get("post") if isinstance(grp, dict) else getattr(grp, "post", None) cart_items = grp.get("cart_items", []) if isinstance(grp, dict) else getattr(grp, "cart_items", []) cal_entries = grp.get("calendar_entries", []) if isinstance(grp, dict) else getattr(grp, "calendar_entries", []) tickets = grp.get("tickets", []) if isinstance(grp, dict) else getattr(grp, "tickets", []) if not cart_items and not cal_entries and not tickets: return None post_data = None if post: post_data = { "slug": post.slug if hasattr(post, "slug") else post.get("slug", ""), "title": post.title if hasattr(post, "title") else post.get("title", ""), "feature_image": post.feature_image if hasattr(post, "feature_image") else post.get("feature_image"), } market_place = grp.get("market_place") if isinstance(grp, dict) else getattr(grp, "market_place", None) mp_data = None if market_place: mp_data = {"name": market_place.name if hasattr(market_place, "name") else market_place.get("name", "")} return { "post": post_data, "product_count": grp.get("product_count", 0) if isinstance(grp, dict) else getattr(grp, "product_count", 0), "calendar_count": grp.get("calendar_count", 0) if isinstance(grp, dict) else getattr(grp, "calendar_count", 0), "ticket_count": grp.get("ticket_count", 0) if isinstance(grp, dict) else getattr(grp, "ticket_count", 0), "total": float(grp.get("total", 0) if isinstance(grp, dict) else getattr(grp, "total", 0)), "market_place": mp_data, } class CartPageService: """Service for cart page data, callable via (service "cart-page" ...).""" async def overview_data(self, session, **kw): from shared.infrastructure.urls import cart_url from bp.cart.services import get_cart_grouped_by_page page_groups = await get_cart_grouped_by_page(session) grp_dicts = [d for d in (_serialize_page_group(grp) for grp in page_groups) if d] return { "page_groups": grp_dicts, "cart_url_base": cart_url(""), } async def page_cart_data(self, session, **kw): from quart import g, request, url_for from shared.infrastructure.urls import login_url from shared.utils import route_prefix from bp.cart.services import total, calendar_total, ticket_total from bp.cart.services.page_cart import ( get_cart_for_page, get_calendar_entries_for_page, get_tickets_for_page, ) from bp.cart.services.ticket_groups import group_tickets post = g.page_post cart = await get_cart_for_page(session, post.id) cal_entries = await get_calendar_entries_for_page(session, post.id) page_tickets = await get_tickets_for_page(session, post.id) ticket_groups = group_tickets(page_tickets) # Build summary data product_qty = sum(ci.quantity for ci in cart) if cart else 0 ticket_qty = len(page_tickets) if page_tickets else 0 item_count = product_qty + ticket_qty product_total = total(cart) or 0 cal_total = calendar_total(cal_entries) or 0 tk_total = ticket_total(page_tickets) or 0 grand = float(product_total) + float(cal_total) + float(tk_total) symbol = "\u00a3" if cart and hasattr(cart[0], "product") and getattr(cart[0].product, "regular_price_currency", None): cur = cart[0].product.regular_price_currency symbol = "\u00a3" if cur == "GBP" else cur user = getattr(g, "user", None) page_post = getattr(g, "page_post", None) summary = { "item_count": item_count, "grand_total": grand, "symbol": symbol, "is_logged_in": bool(user), } if user: if page_post: action = url_for("page_cart.page_checkout") else: action = url_for("cart_global.checkout") summary["checkout_action"] = route_prefix() + action summary["user_email"] = user.email else: summary["login_href"] = login_url(request.url) return { "cart_items": [_serialize_cart_item(i) for i in cart], "cal_entries": [_serialize_cal_entry(e) for e in cal_entries], "ticket_groups": [_serialize_ticket_group(tg) for tg in ticket_groups], "summary": summary, } async def admin_data(self, session, **kw): """Populate post context for cart-admin layout headers.""" from quart import g from shared.infrastructure.fragments import fetch_fragments post = g.page_post slug = post.slug if post else "" post_id = post.id if post else None # Fetch container_nav for post header container_nav = "" if post_id: nav_params = { "container_type": "page", "container_id": str(post_id), "post_slug": slug, } events_nav, market_nav = await fetch_fragments([ ("events", "container-nav", nav_params), ("market", "container-nav", nav_params), ], required=False) container_nav = events_nav + market_nav return { "post": { "id": post_id, "slug": slug, "title": (post.title if post else "")[:160], "feature_image": getattr(post, "feature_image", None), }, "container_nav": container_nav, } async def payments_admin_data(self, session, **kw): """Admin data + payments data combined for cart-payments page.""" admin = await self.admin_data(session) payments = await self.payments_data(session) return {**admin, **payments} async def payments_data(self, session, **kw): from shared.sx.page import get_template_context ctx = await get_template_context() page_config = ctx.get("page_config") pc_data = None if page_config: pc_data = { "sumup_api_key": bool(getattr(page_config, "sumup_api_key", None)), "sumup_merchant_code": getattr(page_config, "sumup_merchant_code", None) or "", "sumup_checkout_prefix": getattr(page_config, "sumup_checkout_prefix", None) or "", } return {"page_config": pc_data}