Files
rose-ash/.claude/plans/rippling-tumbling-cocke.md
giles 094b6c55cd
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m10s
Fix AP blueprint cross-DB queries + harden Ghost sync init
AP blueprints (activitypub.py, ap_social.py) were querying federation
tables (ap_actor_profiles etc.) on g.s which points to the app's own DB
after the per-app split. Now uses g._ap_s backed by get_federation_session()
for non-federation apps.

Also hardens Ghost sync before_app_serving to catch/rollback on failure
instead of crashing the Hypercorn worker.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 14:06:42 +00:00

6.9 KiB
Raw Permalink Blame History

Ticket UX Improvements: +/- Buttons, Sold Count, Cart Grouping

Context

The entry page currently uses a numeric input + "Buy Tickets" button, which replaces itself with a confirmation after purchase. The cart lists each ticket individually. The user wants the ticket UX to match the product pattern: +/- buttons, "in basket" count, tickets grouped by event on cart.

Requirements

  1. Entry page: Show tickets sold count + current user's "in basket" count
  2. Entry page: Replace qty input with "Add to basket" / +/- buttons (product pattern)
  3. Entry page: Keep form active after adding (don't replace with confirmation)
  4. Cart page: Group tickets by event (entry_id + ticket_type), show quantity with +/- buttons

1. Add ticket_type_id to TicketDTO

File: shared/contracts/dtos.py

  • Add ticket_type_id: int | None = None field to TicketDTO

File: shared/services/calendar_impl.py

  • In _ticket_to_dto(), populate ticket_type_id=ticket.ticket_type_id

Sync: Copy to all 4 app submodule copies.

2. New ticket service functions

File: events/bp/tickets/services/tickets.py

  • Add get_user_reserved_count(session, entry_id, user_id, session_id, ticket_type_id=None) -> int
    • Counts reserved tickets for this user+entry+type
  • Add get_sold_ticket_count(session, entry_id) -> int
    • Counts all non-cancelled tickets for this entry
  • Add cancel_latest_reserved_ticket(session, entry_id, user_id, session_id, ticket_type_id=None) -> bool
    • Finds the most recently created reserved ticket for this user+entry+type, sets state='cancelled'. Returns True if one was cancelled.

3. Add adjust_quantity route to events tickets blueprint

File: events/bp/tickets/routes.py

  • New route: POST /tickets/adjust/
  • Form fields: entry_id, ticket_type_id (optional), count (target quantity)
  • Logic:
    • Get current user reserved count for this entry/type
    • If count > current: create (count - current) tickets via create_ticket()
    • If count < current: cancel (current - count) tickets via cancel_latest_reserved_ticket() in a loop
    • If count == 0: cancel all
    • Check availability before adding (like existing buy_tickets)
  • Response: re-render _buy_form.html (HTMX swap replaces form, keeps it active)
  • Include OOB cart-mini update: {{ mini(oob='true') }}

4. Inject ticket counts into entry page context

File: events/bp/calendar_entry/routes.pyinject_root context processor

  • Add ticket_sold_count: total non-cancelled tickets for entry (via get_sold_ticket_count)
  • Add user_ticket_count: current user's reserved count (via get_user_reserved_count)
  • For multi-type entries, add user_ticket_counts_by_type: dict mapping ticket_type_id → count

5. Rewrite entry page buy form

File: events/templates/_types/tickets/_buy_form.html

  • Show "X sold" (from ticket_sold_count) alongside "X remaining"
  • Show "X in basket" for current user

For single-price entries (no ticket types):

  • If user_ticket_count == 0: show "Add to basket" button (posts to /tickets/adjust/ with count=1)
  • If user_ticket_count > 0: show [-] [count badge] [+] buttons
    • Minus: posts count=user_ticket_count-1
    • Plus: posts count=user_ticket_count+1
  • All forms: hx-post, hx-target="#ticket-buy-{{ entry.id }}", hx-swap="outerHTML"

For multi-type entries:

  • Same pattern per ticket type row, using user_ticket_counts_by_type[tt.id]

Style: match product pattern exactly — emerald circular buttons, w-8 h-8, cart icon with badge.

6. Add ticket quantity route to cart app

File: cart/bp/cart/global_routes.py

  • New route: POST /cart/ticket-quantity/
  • Form fields: entry_id, ticket_type_id (optional), count (target quantity)
  • Logic: call into CalendarService or directly use ticket functions
    • Since cart app uses service contracts, add adjust_ticket_quantity to CalendarService protocol

File: shared/contracts/protocols.py — CalendarService

  • Add: adjust_ticket_quantity(session, entry_id, count, *, user_id, session_id, ticket_type_id=None) -> int

File: shared/services/calendar_impl.py

  • Implement adjust_ticket_quantity:
    • Same logic as events adjust route (create/cancel to match target count)
    • Return new count

File: shared/services/stubs.py

  • Add stub: returns 0

Response: HX-Refresh: true (same as product quantity route).

7. Cart page: group tickets by event with +/- buttons

File: cart/templates/_types/cart/_cart.html — ticket section (lines 63-95)

  • Replace individual ticket list with grouped display
  • Group ticket_cart_entries by (entry_id, ticket_type_id):
    • Use Jinja groupby on entry_id first, then sub-group by ticket_type_name
    • Or pre-group in the route handler and pass as a dict

Approach: Pre-group in the route handler for cleaner templates.

File: cart/bp/cart/page_routes.pypage_view

  • After getting page_tickets, group them into a list of dicts:
    [{"entry_name": ..., "entry_id": ..., "ticket_type_name": ..., "ticket_type_id": ...,
      "entry_start_at": ..., "entry_end_at": ..., "price": ..., "quantity": N}]
    
  • Pass as ticket_groups to template

File: cart/bp/cart/global_routes.py — overview/checkout routes

  • Same grouping for global cart view if tickets appear there

Cart ticket group template: Each group shows:

  • Event name + ticket type (if any)
  • Date/time
  • Price per ticket
  • - [qty] + buttons (posting to /cart/ticket-quantity/)
  • Line total (price × qty)

Match product cart_item macro style (article card with quantity controls).

8. Cart summary update

File: cart/templates/_types/cart/_cart.htmlsummary macro

  • Update Items count: include ticket quantities in total (currently just product quantities)

Files to modify (summary)

  • shared/contracts/dtos.py — add ticket_type_id to TicketDTO
  • shared/contracts/protocols.py — add adjust_ticket_quantity to CalendarService
  • shared/services/calendar_impl.py — implement adjust_ticket_quantity, update _ticket_to_dto
  • shared/services/stubs.py — add stub
  • events/bp/tickets/services/tickets.py — add count/cancel functions
  • events/bp/tickets/routes.py — add adjust route
  • events/bp/calendar_entry/routes.py — inject sold/user counts
  • events/templates/_types/tickets/_buy_form.html — rewrite with +/- pattern
  • cart/bp/cart/global_routes.py — add ticket-quantity route
  • cart/bp/cart/page_routes.py — group tickets
  • cart/templates/_types/cart/_cart.html — grouped ticket display with +/-
  • All 4 app shared/ submodule copies synced

Verification

  1. Visit entry page → see "X sold", "X in basket", "Add to basket" button
  2. Click "Add to basket" → form stays, shows - [1] +, basket count shows "1 in basket"
  3. Click + → count increases, sold count increases
  4. Click - → count decreases, ticket cancelled
  5. Visit cart page → tickets grouped by event, +/- buttons work
  6. Checkout flow still works (existing tests)