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>
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: QueryTicketwhereuser_idmatches,state="pending", eager-load entry→calendar + ticket_type. Map to TicketDTO with cost fromticket_type.costorentry.ticket_price.claim_tickets_for_order: UPDATE Ticket SET state="reserved", order_id=? WHERE user_id=? AND state="pending". Ifpage_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_countandticket_totalto each group dict
Step 9: Modify ticket buy route
File: events/bp/tickets/routes.py — buy_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.py — get_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.py — checkout()
- Fetch pending tickets:
get_ticket_cart_entries(g.s) - Include ticket total in cart_total calculation
- Include
not ticket_entriesin empty check - Pass tickets to
create_order_from_cart()(or claim separately after)
File: cart/bp/cart/page_routes.py — page_checkout()
Same changes, scoped to page.
File: cart/bp/cart/services/checkout.py — create_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_configpage 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.py — checkout_return()
- Also fetch tickets for order:
services.calendar.user_tickets()filtered by order_id (or add aget_tickets_for_ordermethod)
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, CartSummaryDTOshared/contracts/protocols.py— add 3 methods to CalendarServiceshared/services/calendar_impl.py— implement 3 new methods, update _ticket_to_dtoshared/services/stubs.py— add stubsshared/services/cart_impl.py— include tickets in cart_summary
Cart app:
cart/bp/cart/api.py— add ticket info to summary APIcart/bp/cart/services/calendar_cart.py— add ticket functionscart/bp/cart/services/__init__.py— export new functionscart/bp/cart/services/page_cart.py— include tickets in grouped viewcart/bp/cart/global_routes.py— include tickets in checkout + returncart/bp/cart/page_routes.py— include tickets in page checkoutcart/bp/cart/services/checkout.py— include ticket total in ordercart/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 countevents/templates/_types/tickets/_buy_form.html— login gateevents/templates/_types/tickets/_buy_result.html— "added to cart" messaging
Templates (shared):
shared/browser/templates/_types/cart/_cart.html— ticket section + totalsshared/browser/templates/_types/cart/_mini.html— ticket count in badgecart/templates/_types/cart/overview/_main_panel.html— ticket badgeshared/browser/templates/_types/order/_calendar_items.html— ticket section
Verification
- Go to an event entry with tickets configured (state="confirmed", ticket_price set)
- Click "Buy Tickets" while not logged in → should see "sign in" prompt
- Log in, click "Buy Tickets" → ticket created with state="pending"
- Navigate to cart → ticket appears alongside any products/bookings
- Proceed to checkout → SumUp payment page
- Complete payment → ticket state becomes "confirmed"
- Check cart mini badge shows ticket count
- Verify availability count doesn't include pending tickets