From 40edba8a8d92573b6ed995b6a69720b4557460f3 Mon Sep 17 00:00:00 2001 From: Padreug Date: Sat, 23 May 2026 22:20:51 +0200 Subject: [PATCH] fix(activities): count seats by extra.quantity across all UI surfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Earlier commit landed the backend storing N seats on one row via extra.quantity (one invoice, one payment, one ticket row), but the UI kept counting rows instead of seats. A 5-ticket purchase showed: Dialog header: "Purchase a ticket for X for 100 sats" ← lied Success modal: "Ticket purchased!" / one ticket ID ← lied My Tickets / badges: "1 paid ticket" ← lied even though the buyer correctly paid 500 sats and 5 seats were sold (DB verified: extra.quantity=5, sats_paid=500, event.sold incremented by 5). The bolt11 invoice amount is cryptographic so the wallet charge was always right — only the labels were wrong. Fixes: - ActivityTicketExtra grows `quantity?: number` (the field already on the wire from the backend; just adding it to the type). - useOwnedTickets exposes `seatsOnRow(ticket)` and `paidCount` sums seats (extra.quantity) across rows instead of counting rows. ActivityCard's "You have N tickets" badge now reflects actual seat ownership. - useUserTickets.groupedTickets sums seats into paidCount / pendingCount / registeredCount so MyTicketsPage groups read correctly. - ActivityDetailPage owned-tickets section adds a `×N` chip on rows that represent multiple seats so the buyer can see which row covers how many. - PurchaseTicketDialog header + DialogDescription reflect the selected quantity ("Purchase 5 tickets" / "5 tickets for X · 500 sats"). The success modal switches to "5 tickets purchased!" and re-labels the ticket id "Purchase ID (covers all tickets)" so the buyer doesn't expect 5 separate ids. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../components/PurchaseTicketDialog.vue | 27 +++++++++++++++---- .../activities/composables/useOwnedTickets.ts | 12 ++++++++- .../activities/composables/useUserTickets.ts | 10 ++++--- src/modules/activities/types/ticket.ts | 4 +++ .../activities/views/ActivityDetailPage.vue | 13 ++++++--- 5 files changed, 54 insertions(+), 12 deletions(-) diff --git a/src/modules/activities/components/PurchaseTicketDialog.vue b/src/modules/activities/components/PurchaseTicketDialog.vue index b162a45..c746d13 100644 --- a/src/modules/activities/components/PurchaseTicketDialog.vue +++ b/src/modules/activities/components/PurchaseTicketDialog.vue @@ -255,10 +255,16 @@ onUnmounted(() => { - Purchase Ticket + {{ quantity > 1 ? `Purchase ${quantity} tickets` : 'Purchase ticket' }} - Purchase a ticket for {{ event.name }} for {{ formatEventPrice(event.price_per_ticket, event.currency) }} + + {{ quantity }} tickets for {{ event.name }} · + {{ formatEventPrice(totalPrice, event.currency) }} + + + Purchase a ticket for {{ event.name }} for {{ formatEventPrice(event.price_per_ticket, event.currency) }} + @@ -540,9 +546,18 @@ onUnmounted(() => {
-

Ticket Purchased Successfully!

+

+ {{ quantity > 1 ? `${quantity} tickets purchased!` : 'Ticket purchased!' }} +

- Your ticket has been purchased and is now available in your tickets area. + + {{ quantity }} tickets are now in your tickets area on a + single purchase row. + + + Your ticket has been purchased and is now available in + your tickets area. +

@@ -552,7 +567,9 @@ onUnmounted(() => {
-

Ticket ID

+

+ {{ quantity > 1 ? 'Purchase ID (covers all tickets)' : 'Ticket ID' }} +

{{ purchasedTicketId }}

diff --git a/src/modules/activities/composables/useOwnedTickets.ts b/src/modules/activities/composables/useOwnedTickets.ts index e540fb3..82b4246 100644 --- a/src/modules/activities/composables/useOwnedTickets.ts +++ b/src/modules/activities/composables/useOwnedTickets.ts @@ -74,8 +74,17 @@ function getTickets(activityId: string): ActivityTicket[] { return ticketsByActivity.value.get(activityId) ?? [] } +/** Seats represented by a single ticket row. One row carries N + * seats via extra.quantity; default 1 for legacy single-purchase rows. */ +function seatsOnRow(ticket: ActivityTicket): number { + return Math.max(1, ticket.extra?.quantity ?? 1) +} + +/** Total paid seats across all rows for one activity (sums extra.quantity). */ function paidCount(activityId: string): number { - return getTickets(activityId).filter(t => t.paid).length + return getTickets(activityId) + .filter(t => t.paid) + .reduce((sum, t) => sum + seatsOnRow(t), 0) } export function useOwnedTickets() { @@ -116,6 +125,7 @@ export function useOwnedTickets() { ownedActivityIds, getTickets, paidCount, + seatsOnRow, refresh: fetchTickets, isLoading, error, diff --git a/src/modules/activities/composables/useUserTickets.ts b/src/modules/activities/composables/useUserTickets.ts index 2803100..d562086 100644 --- a/src/modules/activities/composables/useUserTickets.ts +++ b/src/modules/activities/composables/useUserTickets.ts @@ -84,14 +84,18 @@ export function useUserTickets() { const group = groups.get(eventKey)! group.tickets.push(ticket) + // A ticket row represents N seats via extra.quantity (default 1 + // for legacy single-purchase rows). Counters sum seats, not + // rows, so the user sees the actual ticket count they bought. + const seats = Math.max(1, ticket.extra?.quantity ?? 1) if (ticket.paid) { - group.paidCount++ + group.paidCount += seats } else { - group.pendingCount++ + group.pendingCount += seats } if (ticket.registered) { - group.registeredCount++ + group.registeredCount += seats } }) diff --git a/src/modules/activities/types/ticket.ts b/src/modules/activities/types/ticket.ts index 85ead95..164e370 100644 --- a/src/modules/activities/types/ticket.ts +++ b/src/modules/activities/types/ticket.ts @@ -37,6 +37,10 @@ export interface ActivityTicketExtra { email_notification_sent: boolean nostr_notification_sent: boolean refunded: boolean + /** Number of seats represented by this ticket row (1..10). One + * row per purchase keeps the schema unchanged; callers must + * multiply by this when surfacing seat counts. */ + quantity?: number } export interface ActivityTicket { diff --git a/src/modules/activities/views/ActivityDetailPage.vue b/src/modules/activities/views/ActivityDetailPage.vue index c347363..e026e2f 100644 --- a/src/modules/activities/views/ActivityDetailPage.vue +++ b/src/modules/activities/views/ActivityDetailPage.vue @@ -100,7 +100,7 @@ function goBack() { // --- Ticket purchase + owned-tickets surface ---------------------- -const { getTickets, paidCount, refresh: refreshOwnedTickets } = useOwnedTickets() +const { getTickets, paidCount, seatsOnRow, refresh: refreshOwnedTickets } = useOwnedTickets() const ownedTicketsForActivity = computed(() => getTickets(activityId)) const ownedPaidCount = computed(() => paidCount(activityId)) @@ -290,9 +290,16 @@ function goToMyTickets() {
- {{ ticket.id }} + + ×{{ seatsOnRow(ticket) }} + + {{ ticket.id }}