From afb57a391826bedc7f9c07009b3f6fd0bdbfe7e9 Mon Sep 17 00:00:00 2001 From: Padreug Date: Sat, 20 Jun 2026 09:09:46 +0200 Subject: [PATCH] feat(events): handle free tickets in the purchase flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../components/PurchaseTicketDialog.vue | 20 ++++- .../events/composables/useTicketPurchase.ts | 85 +++++++++++-------- .../events/services/TicketApiService.ts | 4 + src/modules/events/types/ticket.ts | 14 ++- 4 files changed, 81 insertions(+), 42 deletions(-) diff --git a/src/modules/events/components/PurchaseTicketDialog.vue b/src/modules/events/components/PurchaseTicketDialog.vue index 6617496..0d13601 100644 --- a/src/modules/events/components/PurchaseTicketDialog.vue +++ b/src/modules/events/components/PurchaseTicketDialog.vue @@ -70,6 +70,11 @@ function increaseQuantity() { const totalPrice = computed(() => props.event.price_per_ticket * quantity.value) +// Free events (price 0): no invoice is minted — the backend issues the +// tickets already-paid. Drop the payment-method selector and price line +// and label the CTA "Get ticket" instead of "Proceed". +const isFree = computed(() => props.event.price_per_ticket <= 0) + async function copyInvoice() { if (!paymentRequest.value) return try { @@ -259,7 +264,10 @@ onUnmounted(() => { {{ quantity }} tickets for {{ event.name }} · - {{ formatEventPrice(totalPrice, event.currency) }} + {{ isFree ? 'Free' : formatEventPrice(totalPrice, event.currency) }} + + + Get a free ticket for {{ event.name }} Purchase a ticket for {{ event.name }} for {{ formatEventPrice(event.price_per_ticket, event.currency) }} @@ -375,9 +383,9 @@ onUnmounted(() => {
- {{ quantity > 1 ? `${quantity} × ${formatEventPrice(event.price_per_ticket, event.currency)}` : 'Price' }} + {{ quantity > 1 && !isFree ? `${quantity} × ${formatEventPrice(event.price_per_ticket, event.currency)}` : 'Price' }} - {{ formatEventPrice(totalPrice, event.currency) }} + {{ isFree ? 'Free' : formatEventPrice(totalPrice, event.currency) }}
{ Lightning rather than collapsing into a single "Fiat" catch-all. Hidden entirely for Lightning-only events to keep the dialog uncluttered. --> -
+
Payment method

Both methods charge the same amount via different rails. @@ -437,6 +445,10 @@ onUnmounted(() => { class="w-full" > +