Files
rose-ash/.claude/plans/flickering-gathering-wilkes.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

9.3 KiB

Ticket Purchase Through Cart

Context

Tickets (Ticket model) are currently created with state="reserved" immediately when a user clicks "Buy" (POST /tickets/buy/). They bypass the cart and checkout entirely — no cart display, no SumUp payment, no order linkage. The user wants tickets to flow through the cart exactly like products and calendar bookings: appear in the cart, go through checkout, get confirmed on payment. Login required. No reservation — if the event sells out before payment completes, the user gets refunded (admin handles refund; we show a notice).

Current Flow vs Desired Flow

Now: Click Buy → Ticket created (state="reserved") → done (no cart, no payment)

Desired: Click Buy → Ticket created (state="pending", in cart) → Checkout → SumUp payment → Ticket confirmed

Approach

Mirror the CalendarEntry pattern: CalendarEntry uses state="pending" to mean "in cart". We add state="pending" for Ticket. Pending tickets don't count toward availability (not allocated). At checkout, pending→reserved + linked to order. On payment, reserved→confirmed.


Step 1: Update TicketDTO

File: shared/contracts/dtos.py

Add fields needed for cart display and page-grouping:

  • entry_id: int (for linking back)
  • cost: Decimal (ticket price — from ticket_type.cost or entry.ticket_price)
  • calendar_container_id: int | None (for page-grouping in cart)
  • calendar_container_type: str | None

Also add ticket_count and ticket_total to CartSummaryDTO.

Step 2: Add ticket methods to CalendarService protocol

File: shared/contracts/protocols.py

async def pending_tickets(
    self, session: AsyncSession, *, user_id: int,
) -> list[TicketDTO]: ...

async def claim_tickets_for_order(
    self, session: AsyncSession, order_id: int, user_id: int,
    page_post_id: int | None = None,
) -> None: ...

async def confirm_tickets_for_order(
    self, session: AsyncSession, order_id: int,
) -> None: ...

Step 3: Implement in SqlCalendarService

File: shared/services/calendar_impl.py

  • pending_tickets: Query Ticket where user_id matches, state="pending", eager-load entry→calendar + ticket_type. Map to TicketDTO with cost from ticket_type.cost or entry.ticket_price.
  • claim_tickets_for_order: UPDATE Ticket SET state="reserved", order_id=? WHERE user_id=? AND state="pending". If page_post_id, filter via entry→calendar→container.
  • confirm_tickets_for_order: UPDATE Ticket SET state="confirmed" WHERE order_id=? AND state="reserved".

Update _ticket_to_dto to populate the new fields (entry_id, cost, calendar_container_id/type).

Step 4: Add stubs

File: shared/services/stubs.py

Add no-op stubs returning []/None for the 3 new methods.

Step 5: Update SqlCartService

File: shared/services/cart_impl.py

In cart_summary(), also query pending tickets via services.calendar.pending_tickets() and include ticket_count + ticket_total in the returned CartSummaryDTO.

Step 6: Update cart internal API

File: cart/bp/cart/api.py

Add ticket_count and ticket_total to the JSON summary response. Query via services.calendar.pending_tickets().

Step 7: Add ticket cart service functions

File: cart/bp/cart/services/calendar_cart.py

Add:

async def get_ticket_cart_entries(session):
    ident = current_cart_identity()
    if ident["user_id"] is None:
        return []
    return await services.calendar.pending_tickets(session, user_id=ident["user_id"])

def ticket_total(tickets) -> float:
    return sum((t.cost or 0) for t in tickets if t.cost is not None)

File: cart/bp/cart/services/__init__.py — export the new functions.

Step 8: Update cart page grouping

File: cart/bp/cart/services/page_cart.py

In get_cart_grouped_by_page():

  • Fetch ticket cart entries via get_ticket_cart_entries()
  • Attach tickets to page groups by calendar_container_id (same pattern as calendar entries)
  • Add ticket_count and ticket_total to each group dict

Step 9: Modify ticket buy route

File: events/bp/tickets/routes.pybuy_tickets()

  • Require login: If ident["user_id"] is None, return error prompting sign-in
  • Create with state="pending" instead of "reserved"
  • Remove availability check at buy time (pending tickets not allocated)
  • Update response template to say "added to cart" instead of "reserved"

Step 10: Update availability count

File: events/bp/tickets/services/tickets.pyget_available_ticket_count()

Change from counting state != "cancelled" to counting state.in_(("reserved", "confirmed", "checked_in")). This excludes "pending" (in-cart) tickets from sold count.

Step 11: Update buy form template

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

  • If user not logged in, show "Sign in to buy tickets" link instead of buy form
  • Keep existing form for logged-in users

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

  • Change "reserved" messaging to "added to cart"
  • Add link to cart app
  • Add sold-out refund notice: "If the event sells out before payment, you will be refunded."

Step 12: Update cart display templates

File: shared/browser/templates/_types/cart/_cart.html

In show_cart() macro:

  • Add empty check: {% if not cart and not calendar_cart_entries and not ticket_cart_entries %}
  • Add tickets section after calendar bookings (same style)
  • Add sold-out notice under tickets section

In summary() and cart_grand_total() macros:

  • Include ticket_total in the grand total calculation

File: shared/browser/templates/_types/cart/_mini.html

  • Add ticket count to the badge total

Step 13: Update cart overview template

File: cart/templates/_types/cart/overview/_main_panel.html

  • Add ticket count badge alongside product and calendar count badges

Step 14: Update checkout flow

File: cart/bp/cart/global_routes.pycheckout()

  • Fetch pending tickets: get_ticket_cart_entries(g.s)
  • Include ticket total in cart_total calculation
  • Include not ticket_entries in empty check
  • Pass tickets to create_order_from_cart() (or claim separately after)

File: cart/bp/cart/page_routes.pypage_checkout()

Same changes, scoped to page.

File: cart/bp/cart/services/checkout.pycreate_order_from_cart()

  • Accept new param ticket_total: float (add to order total)
  • After claiming calendar entries, also claim tickets: services.calendar.claim_tickets_for_order()
  • Include tickets in resolve_page_config page detection

Step 15: Update payment confirmation

File: cart/bp/cart/services/check_sumup_status.py

When status == "PAID", also call services.calendar.confirm_tickets_for_order(session, order.id) alongside confirm_entries_for_order.

Step 16: Update checkout return page

File: cart/bp/cart/global_routes.pycheckout_return()

  • Also fetch tickets for order: services.calendar.user_tickets() filtered by order_id (or add a get_tickets_for_order method)

File: shared/browser/templates/_types/order/_calendar_items.html

  • Add a tickets section showing ordered/confirmed tickets.

Step 17: Sync shared files

Copy all changed shared files to blog/, cart/, events/, market/ submodules.


Files Modified (Summary)

Shared contracts/services:

  • shared/contracts/dtos.py — update TicketDTO, CartSummaryDTO
  • shared/contracts/protocols.py — add 3 methods to CalendarService
  • shared/services/calendar_impl.py — implement 3 new methods, update _ticket_to_dto
  • shared/services/stubs.py — add stubs
  • shared/services/cart_impl.py — include tickets in cart_summary

Cart app:

  • cart/bp/cart/api.py — add ticket info to summary API
  • cart/bp/cart/services/calendar_cart.py — add ticket functions
  • cart/bp/cart/services/__init__.py — export new functions
  • cart/bp/cart/services/page_cart.py — include tickets in grouped view
  • cart/bp/cart/global_routes.py — include tickets in checkout + return
  • cart/bp/cart/page_routes.py — include tickets in page checkout
  • cart/bp/cart/services/checkout.py — include ticket total in order
  • cart/bp/cart/services/check_sumup_status.py — confirm tickets on payment

Events app:

  • events/bp/tickets/routes.py — require login, state="pending"
  • events/bp/tickets/services/tickets.py — update availability count
  • events/templates/_types/tickets/_buy_form.html — login gate
  • events/templates/_types/tickets/_buy_result.html — "added to cart" messaging

Templates (shared):

  • shared/browser/templates/_types/cart/_cart.html — ticket section + totals
  • shared/browser/templates/_types/cart/_mini.html — ticket count in badge
  • cart/templates/_types/cart/overview/_main_panel.html — ticket badge
  • shared/browser/templates/_types/order/_calendar_items.html — ticket section

Verification

  1. Go to an event entry with tickets configured (state="confirmed", ticket_price set)
  2. Click "Buy Tickets" while not logged in → should see "sign in" prompt
  3. Log in, click "Buy Tickets" → ticket created with state="pending"
  4. Navigate to cart → ticket appears alongside any products/bookings
  5. Proceed to checkout → SumUp payment page
  6. Complete payment → ticket state becomes "confirmed"
  7. Check cart mini badge shows ticket count
  8. Verify availability count doesn't include pending tickets