# bp/cart/global_routes.py — Global cart routes (add, quantity, delete, checkout) from __future__ import annotations from quart import Blueprint, g, request, redirect, url_for, make_response from sqlalchemy import select from shared.models.market import CartItem from shared.infrastructure.actions import call_action from .services import ( current_cart_identity, get_cart, total, get_calendar_cart_entries, calendar_total, get_ticket_cart_entries, ticket_total, ) from .services.checkout import ( find_or_create_cart_item, resolve_page_config, ) def register(url_prefix: str) -> Blueprint: bp = Blueprint("cart_global", __name__, url_prefix=url_prefix) @bp.post("/add//") async def add_to_cart(product_id: int): from shared.infrastructure.data_client import fetch_data ident = current_cart_identity() # Fetch product data from market service (cart DB doesn't have products) products_raw = await fetch_data( "market", "products-by-ids", params={"ids": str(product_id)}, required=False, ) or [] product_data = products_raw[0] if products_raw else None cart_item = await find_or_create_cart_item( g.s, product_id, ident["user_id"], ident["session_id"], product_title=product_data["title"] if product_data else None, product_slug=product_data["slug"] if product_data else None, product_image=product_data["image"] if product_data else None, product_regular_price=product_data["regular_price"] if product_data else None, product_special_price=product_data["special_price"] if product_data else None, ) if not cart_item: return await make_response("Product not found", 404) if request.headers.get("SX-Request") == "true" or request.headers.get("HX-Request") == "true": # Redirect to overview for HTMX return redirect(url_for("defpage_cart_overview")) return redirect(url_for("defpage_cart_overview")) @bp.post("/quantity//") async def update_quantity(product_id: int): ident = current_cart_identity() form = await request.form count = int(form.get("count", 0)) filters = [ CartItem.deleted_at.is_(None), CartItem.product_id == product_id, ] if ident["user_id"] is not None: filters.append(CartItem.user_id == ident["user_id"]) else: filters.append(CartItem.session_id == ident["session_id"]) existing = await g.s.scalar(select(CartItem).where(*filters)) if existing: existing.quantity = max(count, 0) await g.s.flush() resp = await make_response("", 200) resp.headers["HX-Refresh"] = "true" return resp @bp.post("/ticket-quantity/") async def update_ticket_quantity(): """Adjust reserved ticket count (+/- pattern, like products).""" ident = current_cart_identity() form = await request.form entry_id = int(form.get("entry_id", 0)) count = max(int(form.get("count", 0)), 0) tt_raw = (form.get("ticket_type_id") or "").strip() ticket_type_id = int(tt_raw) if tt_raw else None await call_action("events", "adjust-ticket-quantity", payload={ "entry_id": entry_id, "count": count, "user_id": ident["user_id"], "session_id": ident["session_id"], "ticket_type_id": ticket_type_id, }) resp = await make_response("", 200) resp.headers["HX-Refresh"] = "true" return resp @bp.post("/delete//") async def delete_item(product_id: int): ident = current_cart_identity() filters = [ CartItem.deleted_at.is_(None), CartItem.product_id == product_id, ] if ident["user_id"] is not None: filters.append(CartItem.user_id == ident["user_id"]) else: filters.append(CartItem.session_id == ident["session_id"]) existing = await g.s.scalar(select(CartItem).where(*filters)) if existing: await g.s.delete(existing) await g.s.flush() resp = await make_response("", 200) resp.headers["HX-Refresh"] = "true" return resp @bp.post("/checkout/") async def checkout(): """Global checkout — delegates order creation to orders service.""" cart = await get_cart(g.s) calendar_entries = await get_calendar_cart_entries(g.s) tickets = await get_ticket_cart_entries(g.s) if not cart and not calendar_entries and not tickets: return redirect(url_for("defpage_cart_overview")) product_total = total(cart) or 0 calendar_amount = calendar_total(calendar_entries) or 0 ticket_amount = ticket_total(tickets) or 0 cart_total = product_total + calendar_amount + ticket_amount if cart_total <= 0: return redirect(url_for("defpage_cart_overview")) try: page_config = await resolve_page_config(g.s, cart, calendar_entries, tickets) except ValueError as e: from shared.sx.page import get_template_context from sxc.pages.renders import render_checkout_error_page tctx = await get_template_context() html = await render_checkout_error_page(tctx, error=str(e)) return await make_response(html, 400) ident = current_cart_identity() # Serialize cart items for the orders service cart_items_data = [] for ci in cart: cart_items_data.append({ "product_id": ci.product_id, "product_title": ci.product_title, "product_slug": ci.product_slug, "product_image": ci.product_image, "product_regular_price": float(ci.product_regular_price) if ci.product_regular_price else None, "product_special_price": float(ci.product_special_price) if ci.product_special_price else None, "product_price_currency": ci.product_price_currency, "quantity": ci.quantity, }) # Serialize calendar entries and tickets cal_data = [] for e in calendar_entries: cal_data.append({ "id": e.id, "calendar_container_id": getattr(e, "calendar_container_id", None), }) ticket_data = [] for t in tickets: ticket_data.append({ "id": t.id, "calendar_container_id": getattr(t, "calendar_container_id", None), }) page_post_id = None if page_config: page_post_id = getattr(page_config, "container_id", None) result = await call_action("orders", "create-order", payload={ "cart_items": cart_items_data, "calendar_entries": cal_data, "tickets": ticket_data, "user_id": ident.get("user_id"), "session_id": ident.get("session_id"), "product_total": float(product_total), "calendar_total": float(calendar_amount), "ticket_total": float(ticket_amount), "page_post_id": page_post_id, }) # Update redirect/webhook URLs with real order_id order_id = result["order_id"] hosted_url = result.get("sumup_hosted_url") if not hosted_url: from shared.sx.page import get_template_context from sxc.pages.renders import render_checkout_error_page tctx = await get_template_context() html = await render_checkout_error_page(tctx, error="No hosted checkout URL returned from SumUp.") return await make_response(html, 500) return redirect(hosted_url) return bp