diff --git a/app.py b/app.py index 393202a..bc23b18 100644 --- a/app.py +++ b/app.py @@ -21,10 +21,13 @@ from bp.cart.services import ( 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, ) @@ -53,27 +56,32 @@ async def cart_context() -> dict: # 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) - ctx["cart_total"] = (total(all_cart) or 0) + (calendar_total(all_cal) or 0) + ctx["cart_count"] = cart_qty + len(all_cal) + len(all_tickets) + ctx["cart_total"] = (total(all_cart) or 0) + (calendar_total(all_cal) or 0) + (ticket_total(all_tickets) or 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["total"] = total ctx["calendar_total"] = calendar_total + ctx["ticket_total"] = ticket_total ctx["menu_items"] = await get_navigation_tree(g.s) diff --git a/bp/cart/global_routes.py b/bp/cart/global_routes.py index 41d688e..fccbe6a 100644 --- a/bp/cart/global_routes.py +++ b/bp/cart/global_routes.py @@ -16,6 +16,8 @@ from .services import ( clear_cart_for_order, get_calendar_cart_entries, calendar_total, + get_ticket_cart_entries, + ticket_total, check_sumup_status, ) from .services.checkout import ( @@ -107,19 +109,21 @@ def register(url_prefix: str) -> Blueprint: """Legacy global checkout (for orphan items without page scope).""" 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: + if not cart and not calendar_entries and not tickets: return redirect(url_for("cart_overview.overview")) product_total = total(cart) or 0 calendar_amount = calendar_total(calendar_entries) or 0 - cart_total = product_total + calendar_amount + ticket_amount = ticket_total(tickets) or 0 + cart_total = product_total + calendar_amount + ticket_amount if cart_total <= 0: return redirect(url_for("cart_overview.overview")) try: - page_config = await resolve_page_config(g.s, cart, calendar_entries) + page_config = await resolve_page_config(g.s, cart, calendar_entries, tickets) except ValueError as e: html = await render_template( "_types/cart/checkout_error.html", @@ -137,6 +141,7 @@ def register(url_prefix: str) -> Blueprint: ident.get("session_id"), product_total, calendar_amount, + ticket_total=ticket_amount, ) if page_config: @@ -144,7 +149,7 @@ def register(url_prefix: str) -> Blueprint: redirect_url = url_for("cart_global.checkout_return", order_id=order.id, _external=True) order.sumup_reference = build_sumup_reference(order.id, page_config=page_config) - description = build_sumup_description(cart, order.id) + description = build_sumup_description(cart, order.id, ticket_count=len(tickets)) webhook_base_url = url_for("cart_global.checkout_webhook", order_id=order.id, _external=True) webhook_url = build_webhook_url(webhook_base_url) @@ -253,6 +258,7 @@ def register(url_prefix: str) -> Blueprint: status = (order.status or "pending").lower() calendar_entries = await services.calendar.get_entries_for_order(g.s, order.id) + order_tickets = await services.calendar.get_tickets_for_order(g.s, order.id) await g.s.flush() html = await render_template( @@ -260,6 +266,7 @@ def register(url_prefix: str) -> Blueprint: order=order, status=status, calendar_entries=calendar_entries, + order_tickets=order_tickets, ) return await make_response(html) diff --git a/bp/cart/page_routes.py b/bp/cart/page_routes.py index 3a19057..69e20b0 100644 --- a/bp/cart/page_routes.py +++ b/bp/cart/page_routes.py @@ -11,8 +11,9 @@ from .services import ( total, clear_cart_for_order, calendar_total, + ticket_total, ) -from .services.page_cart import get_cart_for_page, get_calendar_entries_for_page +from .services.page_cart import get_cart_for_page, get_calendar_entries_for_page, get_tickets_for_page from .services.checkout import ( create_order_from_cart, build_sumup_description, @@ -30,14 +31,17 @@ def register(url_prefix: str) -> Blueprint: post = g.page_post cart = await get_cart_for_page(g.s, post.id) cal_entries = await get_calendar_entries_for_page(g.s, post.id) + page_tickets = await get_tickets_for_page(g.s, post.id) tpl_ctx = dict( page_post=post, page_config=getattr(g, "page_config", None), cart=cart, calendar_cart_entries=cal_entries, + ticket_cart_entries=page_tickets, total=total, calendar_total=calendar_total, + ticket_total=ticket_total, ) if not is_htmx_request(): @@ -53,13 +57,15 @@ def register(url_prefix: str) -> Blueprint: cart = await get_cart_for_page(g.s, post.id) cal_entries = await get_calendar_entries_for_page(g.s, post.id) + page_tickets = await get_tickets_for_page(g.s, post.id) - if not cart and not cal_entries: + if not cart and not cal_entries and not page_tickets: return redirect(url_for("page_cart.page_view")) product_total = total(cart) or 0 calendar_amount = calendar_total(cal_entries) or 0 - cart_total = product_total + calendar_amount + ticket_amount = ticket_total(page_tickets) or 0 + cart_total = product_total + calendar_amount + ticket_amount if cart_total <= 0: return redirect(url_for("page_cart.page_view")) @@ -74,6 +80,7 @@ def register(url_prefix: str) -> Blueprint: ident.get("session_id"), product_total, calendar_amount, + ticket_total=ticket_amount, page_post_id=post.id, ) @@ -84,7 +91,7 @@ def register(url_prefix: str) -> Blueprint: # Build SumUp checkout details — webhook/return use global routes redirect_url = url_for("cart_global.checkout_return", order_id=order.id, _external=True) order.sumup_reference = build_sumup_reference(order.id, page_config=page_config) - description = build_sumup_description(cart, order.id) + description = build_sumup_description(cart, order.id, ticket_count=len(page_tickets)) webhook_base_url = url_for("cart_global.checkout_webhook", order_id=order.id, _external=True) webhook_url = build_webhook_url(webhook_base_url) diff --git a/bp/cart/services/__init__.py b/bp/cart/services/__init__.py index 16e4278..8ba68b4 100644 --- a/bp/cart/services/__init__.py +++ b/bp/cart/services/__init__.py @@ -2,11 +2,12 @@ from .get_cart import get_cart from .identity import current_cart_identity from .total import total from .clear_cart_for_order import clear_cart_for_order -from .calendar_cart import get_calendar_cart_entries, calendar_total +from .calendar_cart import get_calendar_cart_entries, calendar_total, get_ticket_cart_entries, ticket_total from .check_sumup_status import check_sumup_status from .page_cart import ( get_cart_for_page, get_calendar_entries_for_page, + get_tickets_for_page, get_cart_grouped_by_page, ) diff --git a/bp/cart/services/calendar_cart.py b/bp/cart/services/calendar_cart.py index 57960cc..82b5c86 100644 --- a/bp/cart/services/calendar_cart.py +++ b/bp/cart/services/calendar_cart.py @@ -26,3 +26,18 @@ def calendar_total(entries) -> float: for e in entries if e.cost is not None ) + + +async def get_ticket_cart_entries(session): + """Return all reserved tickets (as TicketDTOs) for the current identity.""" + ident = current_cart_identity() + return await services.calendar.pending_tickets( + session, + user_id=ident["user_id"], + session_id=ident["session_id"], + ) + + +def ticket_total(tickets) -> float: + """Total cost of reserved tickets.""" + return sum(float(t.price or 0) for t in tickets) diff --git a/bp/cart/services/check_sumup_status.py b/bp/cart/services/check_sumup_status.py index fec2a2c..722d631 100644 --- a/bp/cart/services/check_sumup_status.py +++ b/bp/cart/services/check_sumup_status.py @@ -16,6 +16,7 @@ async def check_sumup_status(session, order): await services.calendar.confirm_entries_for_order( session, order.id, order.user_id, order.session_id ) + await services.calendar.confirm_tickets_for_order(session, order.id) await emit_event(session, "order.paid", "order", order.id, { "order_id": order.id, "user_id": order.user_id, diff --git a/bp/cart/services/checkout.py b/bp/cart/services/checkout.py index ef2843b..b032526 100644 --- a/bp/cart/services/checkout.py +++ b/bp/cart/services/checkout.py @@ -65,6 +65,7 @@ async def resolve_page_config( session: AsyncSession, cart: list[CartItem], calendar_entries: list[CalendarEntryDTO], + tickets=None, ) -> Optional["PageConfig"]: """Determine the PageConfig for this order. @@ -85,6 +86,11 @@ async def resolve_page_config( if entry.calendar_container_id: post_ids.add(entry.calendar_container_id) + # From tickets via calendar_container_id + for tk in (tickets or []): + if tk.calendar_container_id: + post_ids.add(tk.calendar_container_id) + if len(post_ids) > 1: raise ValueError("Cannot checkout items from multiple pages") @@ -110,16 +116,17 @@ async def create_order_from_cart( product_total: float, calendar_total: float, *, + ticket_total: float = 0, page_post_id: int | None = None, ) -> Order: """ - Create an Order and OrderItems from the current cart + calendar entries. + Create an Order and OrderItems from the current cart + calendar entries + tickets. - When *page_post_id* is given, only calendar entries whose calendar + When *page_post_id* is given, only calendar entries/tickets whose calendar belongs to that page are marked as "ordered". Otherwise all pending entries are updated (legacy behaviour). """ - cart_total = product_total + calendar_total + cart_total = product_total + calendar_total + ticket_total # Determine currency from first product first_product = cart[0].product if cart else None @@ -154,6 +161,11 @@ async def create_order_from_cart( session, order.id, user_id, session_id, page_post_id ) + # Claim reserved tickets for this order + await services.calendar.claim_tickets_for_order( + session, order.id, user_id, session_id, page_post_id + ) + await emit_event(session, "order.created", "order", order.id, { "order_id": order.id, "user_id": user_id, @@ -163,20 +175,24 @@ async def create_order_from_cart( return order -def build_sumup_description(cart: list[CartItem], order_id: int) -> str: +def build_sumup_description(cart: list[CartItem], order_id: int, *, ticket_count: int = 0) -> str: """Build a human-readable description for SumUp checkout.""" titles = [ci.product.title for ci in cart if ci.product and ci.product.title] item_count = sum(ci.quantity for ci in cart) + parts = [] if titles: if len(titles) <= 3: - summary = ", ".join(titles) + parts.append(", ".join(titles)) else: - summary = ", ".join(titles[:3]) + f" + {len(titles) - 3} more" - else: - summary = "order items" + parts.append(", ".join(titles[:3]) + f" + {len(titles) - 3} more") + if ticket_count: + parts.append(f"{ticket_count} ticket{'s' if ticket_count != 1 else ''}") - return f"Order {order_id} ({item_count} item{'s' if item_count != 1 else ''}): {summary}" + summary = ", ".join(parts) if parts else "order items" + total_count = item_count + ticket_count + + return f"Order {order_id} ({total_count} item{'s' if total_count != 1 else ''}): {summary}" def build_sumup_reference(order_id: int, page_config=None) -> str: diff --git a/bp/cart/services/page_cart.py b/bp/cart/services/page_cart.py index 7626ebd..ce59113 100644 --- a/bp/cart/services/page_cart.py +++ b/bp/cart/services/page_cart.py @@ -57,6 +57,16 @@ async def get_calendar_entries_for_page(session, post_id: int): ) +async def get_tickets_for_page(session, post_id: int): + """Return reserved tickets (DTOs) scoped to a specific page.""" + ident = current_cart_identity() + return await services.calendar.tickets_for_page( + session, post_id, + user_id=ident["user_id"], + session_id=ident["session_id"], + ) + + async def get_cart_grouped_by_page(session) -> list[dict]: """ Load all cart items + calendar entries for the current identity, @@ -80,12 +90,13 @@ async def get_cart_grouped_by_page(session) -> list[dict]: Items without a market_place go in an orphan bucket (post=None). """ from .get_cart import get_cart - from .calendar_cart import get_calendar_cart_entries + from .calendar_cart import get_calendar_cart_entries, get_ticket_cart_entries from .total import total as calc_product_total - from .calendar_cart import calendar_total as calc_calendar_total + from .calendar_cart import calendar_total as calc_calendar_total, ticket_total as calc_ticket_total cart_items = await get_cart(session) cal_entries = await get_calendar_cart_entries(session) + all_tickets = await get_ticket_cart_entries(session) # Group cart items by market_place_id market_groups: dict[int | None, dict] = {} @@ -97,6 +108,7 @@ async def get_cart_grouped_by_page(session) -> list[dict]: "post_id": ci.market_place.container_id if ci.market_place else None, "cart_items": [], "calendar_entries": [], + "tickets": [], } market_groups[mp_id]["cart_items"].append(ci) @@ -121,11 +133,31 @@ async def get_cart_grouped_by_page(session) -> list[dict]: "post_id": pid, "cart_items": [], "calendar_entries": [], + "tickets": [], } if pid is not None: page_to_market[pid] = key market_groups[key]["calendar_entries"].append(ce) + # Attach tickets to page groups (via calendar_container_id) + for tk in all_tickets: + pid = tk.calendar_container_id or None + if pid in page_to_market: + market_groups[page_to_market[pid]]["tickets"].append(tk) + else: + key = ("tk", pid) + if key not in market_groups: + market_groups[key] = { + "market_place": None, + "post_id": pid, + "cart_items": [], + "calendar_entries": [], + "tickets": [], + } + if pid is not None: + page_to_market[pid] = key + market_groups[key]["tickets"].append(tk) + # Batch-load Post DTOs and PageConfig objects post_ids = list({ grp["post_id"] for grp in market_groups.values() @@ -155,8 +187,10 @@ async def get_cart_grouped_by_page(session) -> list[dict]: ): items = grp["cart_items"] entries = grp["calendar_entries"] + tks = grp["tickets"] prod_total = calc_product_total(items) or 0 cal_total = calc_calendar_total(entries) or 0 + tk_total = calc_ticket_total(tks) or 0 pid = grp["post_id"] result.append({ @@ -165,11 +199,14 @@ async def get_cart_grouped_by_page(session) -> list[dict]: "market_place": grp["market_place"], "cart_items": items, "calendar_entries": entries, + "tickets": tks, "product_count": sum(ci.quantity for ci in items), "product_total": prod_total, "calendar_count": len(entries), "calendar_total": cal_total, - "total": prod_total + cal_total, + "ticket_count": len(tks), + "ticket_total": tk_total, + "total": prod_total + cal_total + tk_total, }) return result diff --git a/shared b/shared index 71729ff..7ee8638 160000 --- a/shared +++ b/shared @@ -1 +1 @@ -Subproject commit 71729ffb2830bb80d1bc591b08e693a2625c2ad7 +Subproject commit 7ee8638d6e41de1f58aadd1f108cd7de8e920d07 diff --git a/templates/_types/cart/_cart.html b/templates/_types/cart/_cart.html index 22427ca..d564c0d 100644 --- a/templates/_types/cart/_cart.html +++ b/templates/_types/cart/_cart.html @@ -1,7 +1,7 @@ {% macro show_cart(oob=False) %}
{# Empty cart #} - {% if not cart and not calendar_cart_entries %} + {% if not cart and not calendar_cart_entries and not ticket_cart_entries %}
@@ -60,8 +60,41 @@
{% endif %} + {% if ticket_cart_entries %} +
+

+ Event tickets +

+ +
    + {% for tk in ticket_cart_entries %} +
  • +
    +
    + {{ tk.entry_name }} +
    + {% if tk.ticket_type_name %} +
    + {{ tk.ticket_type_name }} +
    + {% endif %} +
    + {{ tk.entry_start_at.strftime('%-d %b %Y, %H:%M') }} + {% if tk.entry_end_at %} + – {{ tk.entry_end_at.strftime('%-d %b %Y, %H:%M') }} + {% endif %} +
    +
    +
    + £{{ "%.2f"|format(tk.price or 0) }} +
    +
  • + {% endfor %} +
+
+ {% endif %} - {{summary(cart, total, calendar_total, calendar_cart_entries,)}} + {{summary(cart, total, calendar_total, calendar_cart_entries, ticket_total, ticket_cart_entries)}}
@@ -70,7 +103,7 @@ {% endmacro %} -{% macro summary(cart, total, calendar_total, calendar_cart_entries, oob=False) %} +{% macro summary(cart, total, calendar_total, calendar_cart_entries, ticket_total, ticket_cart_entries, oob=False) %}