feat(events): handle free tickets in the purchase flow #131

Merged
padreug merged 1 commit from feat/free-tickets-client into dev 2026-06-20 09:58:39 +00:00
Owner

Client companion to aiolabs/events#31 (backend free-ticket issuance).

Problem

Free events (price 0 / 100%-off promo) were unbuyable. The backend returned an invoice-less response, and the composable's guard if (invoice.isFiat || !invoice.paymentRequest) treated any missing paymentRequest as fiat and threw "This event uses fiat checkout".

Change

With #31, a free purchase returns paid: true + ticketIds inline and no payment_request — tickets are already issued and marked paid, nothing to settle.

  • TicketPurchaseInvoice gains paid + ticketIds; TicketApiService maps them from the response.
  • purchaseTicketForEvent short-circuits on invoice.paid: skip the QR / payment-poll and jump straight to the ticket-QR success state with the inline ids. The fiat-checkout error now only fires for an actual fiat (not-paid) response.
  • The ticket-QR success rendering (refresh owned tickets → one QR per row → toast) is extracted into a shared finalizePurchasedTickets() used by both the Lightning-poll path and the free path, so they render identically.
  • PurchaseTicketDialog: for free events, drop the payment-method selector + price line, show "Free", and label the CTA "Get ticket".

Notes / testing

  • The free path reuses the exact existing ticket-QR success state (showTicketQR + ticketQRCodes + purchasedTicketIds), so it lands on the same screen the paid flow does — it just skips the invoice screen.
  • vue-tsc clean; existing vitest suite green (19). The purchase composable is DI-heavy (no isolated unit harness, matching the existing untested flow); verify end-to-end against a dev LNbits with events#31 deployed: buy a free ticket → no invoice screen, ticket QR shows immediately, appears in My Tickets, scannable at the door.
  • Depends on events#31 being deployed to the target instance for the response shape.

🤖 Generated with Claude Code

Client companion to **aiolabs/events#31** (backend free-ticket issuance). ## Problem Free events (`price 0` / 100%-off promo) were unbuyable. The backend returned an invoice-less response, and the composable's guard `if (invoice.isFiat || !invoice.paymentRequest)` treated *any* missing `paymentRequest` as fiat and threw **"This event uses fiat checkout"**. ## Change With #31, a free purchase returns `paid: true` + `ticketIds` inline and no `payment_request` — tickets are already issued and marked paid, nothing to settle. - `TicketPurchaseInvoice` gains `paid` + `ticketIds`; `TicketApiService` maps them from the response. - `purchaseTicketForEvent` short-circuits on `invoice.paid`: skip the QR / payment-poll and jump straight to the ticket-QR success state with the inline ids. The fiat-checkout error now only fires for an actual fiat (not-paid) response. - The ticket-QR success rendering (refresh owned tickets → one QR per row → toast) is extracted into a shared `finalizePurchasedTickets()` used by **both** the Lightning-poll path and the free path, so they render identically. - `PurchaseTicketDialog`: for free events, drop the payment-method selector + price line, show **"Free"**, and label the CTA **"Get ticket"**. ## Notes / testing - The free path reuses the exact existing ticket-QR success state (`showTicketQR` + `ticketQRCodes` + `purchasedTicketIds`), so it lands on the same screen the paid flow does — it just skips the invoice screen. - `vue-tsc` clean; existing vitest suite green (19). The purchase composable is DI-heavy (no isolated unit harness, matching the existing untested flow); verify end-to-end against a dev LNbits with events#31 deployed: buy a free ticket → no invoice screen, ticket QR shows immediately, appears in My Tickets, scannable at the door. - **Depends on events#31** being deployed to the target instance for the response shape. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Companion to aiolabs/events#31. Free events (price 0 / 100%-off promo)
now come back from POST /tickets/{event_id} as paid=true with the row
ids inline and no payment_request — the backend issued them
already-paid, no invoice to settle.

Previously the composable's `!paymentRequest` guard treated any
invoice-less response as fiat and threw "This event uses fiat
checkout", so free tickets were unbuyable.

- TicketPurchaseInvoice gains `paid` + `ticketIds`; TicketApiService
  maps them.
- purchaseTicketForEvent short-circuits on `invoice.paid`: skip the QR /
  payment-poll and go straight to the ticket-QR success state. The fiat
  error now only fires for an actual fiat (not-paid) response.
- The ticket-QR rendering (refresh owned tickets, one QR per row, toast)
  is extracted into a shared finalizePurchasedTickets() used by both the
  Lightning-poll path and the free path.
- PurchaseTicketDialog: for free events drop the payment-method selector
  and price line, show "Free", and label the CTA "Get ticket".

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign in to join this conversation.
No description provided.