from __future__ import annotations import path_setup # noqa: F401 # adds shared/ to sys.path from shared.sx.jinja_bridge import load_service_components # noqa: F401 from decimal import Decimal from pathlib import Path from types import SimpleNamespace from quart import g, abort, request from jinja2 import FileSystemLoader, ChoiceLoader from shared.infrastructure.factory import create_base_app from bp import ( register_cart_overview, register_page_cart, register_cart_global, register_page_admin, register_actions, register_data, register_inbox, ) from bp.cart.services import ( get_cart, total, get_calendar_cart_entries, calendar_total, get_ticket_cart_entries, 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 async def _load_cart(): """Load the full cart for the cart app (before each request).""" g.cart = await get_cart(g.s) async def cart_context() -> dict: """ Cart app context processor. - cart / calendar_cart_entries / total / calendar_total: direct DB (cart app owns this data) - cart_count: derived from cart + calendar entries (for _mini.html) - nav_tree: fetched from blog as fragment When g.page_post exists, cart and calendar_cart_entries are page-scoped. Global cart_count / cart_total stay global for cart-mini. """ from shared.infrastructure.context import base_context from shared.infrastructure.cart_identity import current_cart_identity from shared.infrastructure.fragments import fetch_fragments ctx = await base_context() # menu_nodes lives in db_blog; nav-tree fragment provides the real nav ctx["menu_items"] = [] # Pre-fetch cross-app HTML fragments concurrently user = getattr(g, "user", None) ident = current_cart_identity() cart_params = {} if ident["user_id"] is not None: cart_params["user_id"] = ident["user_id"] if ident["session_id"] is not None: cart_params["session_id"] = ident["session_id"] cart_mini, auth_menu, nav_tree = await fetch_fragments([ ("cart", "cart-mini", cart_params or None), ("account", "auth-menu", {"email": user.email} if user else None), ("blog", "nav-tree", {"app_name": "cart", "path": request.path}), ]) ctx["cart_mini"] = cart_mini ctx["auth_menu"] = auth_menu ctx["nav_tree"] = nav_tree # Cart app owns cart data — use g.cart from _load_cart all_cart = getattr(g, "cart", None) or [] all_cal = await get_calendar_cart_entries(g.s) all_tickets = await get_ticket_cart_entries(g.s) # Global counts for cart-mini (always global) cart_qty = sum(ci.quantity for ci in all_cart) if all_cart else 0 ctx["cart_count"] = cart_qty + len(all_cal) + len(all_tickets) ctx["cart_total"] = (total(all_cart) or Decimal(0)) + (calendar_total(all_cal) or Decimal(0)) + (ticket_total(all_tickets) or Decimal(0)) # Page-scoped data when viewing a page cart page_post = getattr(g, "page_post", None) if page_post: page_cart = await get_cart_for_page(g.s, page_post.id) page_cal = await get_calendar_entries_for_page(g.s, page_post.id) page_tickets = await get_tickets_for_page(g.s, page_post.id) ctx["cart"] = page_cart ctx["calendar_cart_entries"] = page_cal ctx["ticket_cart_entries"] = page_tickets ctx["page_post"] = page_post ctx["page_config"] = getattr(g, "page_config", None) else: ctx["cart"] = all_cart ctx["calendar_cart_entries"] = all_cal ctx["ticket_cart_entries"] = all_tickets ctx["ticket_groups"] = group_tickets(ctx.get("ticket_cart_entries", [])) ctx["total"] = total ctx["calendar_total"] = calendar_total ctx["ticket_total"] = ticket_total return ctx def _make_page_config(raw: dict) -> SimpleNamespace: """Convert a page-config JSON dict to a namespace for SumUp helpers.""" return SimpleNamespace(**raw) def create_app() -> "Quart": from services import register_domain_services app = create_base_app( "cart", context_fn=cart_context, before_request_fns=[_load_cart], domain_services_fn=register_domain_services, ) # App-specific templates override shared templates app_templates = str(Path(__file__).resolve().parent / "templates") app.jinja_loader = ChoiceLoader([ FileSystemLoader(app_templates), app.jinja_loader, ]) app.jinja_env.globals["cart_quantity_url"] = lambda product_id: f"/quantity/{product_id}/" app.jinja_env.globals["cart_delete_url"] = lambda product_id: f"/delete/{product_id}/" import os as _os load_service_components(_os.path.dirname(_os.path.abspath(__file__)), service_name="cart") from shared.sx.handlers import auto_mount_fragment_handlers auto_mount_fragment_handlers(app, "cart") app.register_blueprint(register_actions()) app.register_blueprint(register_data()) app.register_blueprint(register_inbox()) # --- Page slug hydration (follows events/market app pattern) --- @app.url_value_preprocessor def pull_page_slug(endpoint, values): if values and "page_slug" in values: g.page_slug = values.pop("page_slug") @app.url_defaults def inject_page_slug(endpoint, values): slug = g.get("page_slug") if slug and "page_slug" not in values: if app.url_map.is_endpoint_expecting(endpoint, "page_slug"): values["page_slug"] = slug @app.before_request async def hydrate_page(): from shared.infrastructure.data_client import fetch_data from shared.contracts.dtos import PostDTO, dto_from_dict slug = getattr(g, "page_slug", None) if not slug: return raw = await fetch_data("blog", "post-by-slug", params={"slug": slug}) if not raw: abort(404) post = dto_from_dict(PostDTO, raw) if not post or not post.is_page: abort(404) g.page_post = post raw_pc = await fetch_data( "blog", "page-config", params={"container_type": "page", "container_id": post.id}, required=False, ) g.page_config = _make_page_config(raw_pc) if raw_pc else None # Setup defpage routes from sxc.pages import setup_cart_pages setup_cart_pages() # --- Blueprint registration --- # Static prefixes first, dynamic (page_slug) last # Global routes (add, quantity, delete, checkout — specific paths under /) app.register_blueprint( register_cart_global(url_prefix="/"), url_prefix="/", ) # Cart overview blueprint (no defpage routes, just action endpoints) overview_bp = register_cart_overview(url_prefix="/") app.register_blueprint(overview_bp, url_prefix="/") # Page admin (PUT /payments/ etc.) admin_bp = register_page_admin() app.register_blueprint(admin_bp, url_prefix="//admin") # Page cart (POST /checkout/ etc.) page_cart_bp = register_page_cart(url_prefix="/") app.register_blueprint(page_cart_bp, url_prefix="/") # Auto-mount all defpages with absolute paths from shared.sx.pages import auto_mount_pages auto_mount_pages(app, "cart") return app app = create_app()