Fix AP blueprint cross-DB queries + harden Ghost sync init
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m10s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m10s
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>
This commit is contained in:
236
.claude/plans/flickering-gathering-wilkes.md
Normal file
236
.claude/plans/flickering-gathering-wilkes.md
Normal file
@@ -0,0 +1,236 @@
|
||||
# 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`
|
||||
|
||||
```python
|
||||
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:
|
||||
```python
|
||||
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.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_entries` in 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_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.py` — `checkout_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
|
||||
Reference in New Issue
Block a user