Files
mono/.claude/plans/rippling-tumbling-cocke.md
giles 094b6c55cd 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

150 lines
6.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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.py``inject_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.py``page_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.html` — `summary` 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)