fix(events): stop subtracting sold from amount_tickets #174

Merged
padreug merged 1 commit from fix/my-events-availability-arithmetic into dev 2026-09-27 21:09:35 +00:00
Owner

Closes #173

amount_tickets is the remaining count — the backend decrements it on every sale and increments sold, so the two describe the same tickets. MyEventsPage.vue subtracted one from the other, removing each sale twice.

- {{ event.amount_tickets - event.sold }}      <!-- :176 -->
+ {{ event.amount_tickets }}

- event.amount_tickets <= event.sold ||        <!-- :190 -->
+ event.amount_tickets < 1 ||

This is disabling the buy button in production

Because remaining + sold is the original capacity, amount_tickets <= sold first becomes true at exactly the halfway point. The purchase button dies once an event sells half its seats.

The backend half shipped as aiolabs/events#59 in v1.6.1-aio.17, which makes the mismatch worse: the server now sells correctly while the webapp refuses. On aio-demo:

Tech Meetup            9 remaining,  11 sold  ->  shows -2,  button DISABLED
pups leash training    2 remaining,   3 sold  ->  shows -1,  button DISABLED
Art therapy            5 remaining,   5 sold  ->  shows  0,  button DISABLED

The API disagrees with all three — POST /events/api/v1/tickets/KVhCGUrGFP6UyBuEaRA5Zd with quantity 10 returns "Only 9 ticket(s) remaining", not sold out.

Swept the rest of the module: these were the only two occurrences. EventDetailPage.vue and EventCard.vue read ticketInfo.available straight from the tickets_available NIP-52 tag and are correct — this page reads the REST Event shape instead, which is how it diverged.

Capacity is now required, minimum 1

A capacity of 0 meant "unlimited" in this form. Nothing on the backend agreed — api_get_event and api_ticket_create both read amount_tickets < 1 as sold out, so a 0 produced an event that advertised "Unlimited tickets" on the card and returned 410 to every purchase. Three such events exist on demo right now. aiolabs/events#62 stops publishing the lie; this stops the form creating it.

Four coordinated edits:

  • zod .min(1) with a real message, .default(0) dropped so the field is genuinely required
  • initial value undefined — the organiser chooses rather than inheriting a broken default
  • the edit path uses || undefined, so opening an existing zero-capacity event presents an empty field
  • FormDescription no longer says 0 = unlimited; input min="1"

The one-line change that unblocks the backend

- if (formValues.amount_tickets) eventData.amount_tickets = ...
+ if (formValues.amount_tickets !== undefined) eventData.amount_tickets = ...

The falsy check dropped 0 from the request entirely. aiolabs/events wants amount_tickets required (ge=1) — the last piece of events#34 — and that would have 422'd this dialog for exactly the case users were picking. Ordering is: this ships, then the backend tightens.

Testing

vue-tsc --noEmit exit 0 · vitest run 51 passed · npm run build exit 0.

Not included

types/event.ts:100 derives total = available + sold, which is the 10 of 17 inflation from the original events#34 report — correct only while capacity is never edited, since an edit re-baselines remaining. That's the display half of events#34, parked for the v1.6.8 rebase, and it reads the NIP-52 tag rather than the REST shape. Different code path; not widening this PR to reach it.

Release path

Per the webapp flow this targets dev. Wants a webapp-dev flake.lock bump and a smoke on aio-demo before main — the three stuck events there are a ready-made check: their buy buttons should come alive.

Closes #173 `amount_tickets` **is** the remaining count — the backend decrements it on every sale *and* increments `sold`, so the two describe the same tickets. `MyEventsPage.vue` subtracted one from the other, removing each sale twice. ```vue - {{ event.amount_tickets - event.sold }} <!-- :176 --> + {{ event.amount_tickets }} - event.amount_tickets <= event.sold || <!-- :190 --> + event.amount_tickets < 1 || ``` ## This is disabling the buy button in production Because `remaining + sold` is the original capacity, `amount_tickets <= sold` first becomes true at exactly the **halfway point**. The purchase button dies once an event sells half its seats. The backend half shipped as `aiolabs/events#59` in `v1.6.1-aio.17`, which makes the mismatch *worse*: the server now sells correctly while the webapp refuses. On aio-demo: ``` Tech Meetup 9 remaining, 11 sold -> shows -2, button DISABLED pups leash training 2 remaining, 3 sold -> shows -1, button DISABLED Art therapy 5 remaining, 5 sold -> shows 0, button DISABLED ``` The API disagrees with all three — `POST /events/api/v1/tickets/KVhCGUrGFP6UyBuEaRA5Zd` with quantity 10 returns *"Only 9 ticket(s) remaining"*, not sold out. Swept the rest of the module: these were the only two occurrences. `EventDetailPage.vue` and `EventCard.vue` read `ticketInfo.available` straight from the `tickets_available` NIP-52 tag and are correct — this page reads the REST `Event` shape instead, which is how it diverged. ## Capacity is now required, minimum 1 A capacity of `0` meant "unlimited" in this form. Nothing on the backend agreed — `api_get_event` and `api_ticket_create` both read `amount_tickets < 1` as sold out, so a `0` produced an event that advertised **"Unlimited tickets"** on the card and returned 410 to every purchase. Three such events exist on demo right now. `aiolabs/events#62` stops publishing the lie; this stops the form creating it. Four coordinated edits: - zod `.min(1)` with a real message, `.default(0)` dropped so the field is genuinely required - initial value `undefined` — the organiser chooses rather than inheriting a broken default - the edit path uses `|| undefined`, so opening an existing zero-capacity event presents an empty field - `FormDescription` no longer says `0 = unlimited`; input `min="1"` ## The one-line change that unblocks the backend ```js - if (formValues.amount_tickets) eventData.amount_tickets = ... + if (formValues.amount_tickets !== undefined) eventData.amount_tickets = ... ``` The falsy check dropped `0` from the request entirely. `aiolabs/events` wants `amount_tickets` required (`ge=1`) — the last piece of events#34 — and that would have **422'd this dialog** for exactly the case users were picking. Ordering is: this ships, then the backend tightens. ## Testing `vue-tsc --noEmit` exit 0 · `vitest run` 51 passed · `npm run build` exit 0. ## Not included `types/event.ts:100` derives `total = available + sold`, which is the `10 of 17` inflation from the original events#34 report — correct only while capacity is never edited, since an edit re-baselines remaining. That's the display half of events#34, parked for the v1.6.8 rebase, and it reads the NIP-52 tag rather than the REST shape. Different code path; not widening this PR to reach it. ## Release path Per the webapp flow this targets `dev`. Wants a `webapp-dev` flake.lock bump and a smoke on aio-demo before `main` — the three stuck events there are a ready-made check: their buy buttons should come alive.
`amount_tickets` IS the remaining count — the backend decrements it on
every sale and increments `sold`, so the two describe the same tickets.
MyEventsPage subtracted one from the other, removing each sale twice.

Because remaining + sold is the original capacity, `amount_tickets <=
sold` first becomes true at exactly the halfway point. So the purchase
button disabled itself once an event had sold half its seats, and
"Tickets Available" under-reported all the way there.

The backend half of this shipped as aiolabs/events#59 in v1.6.1-aio.17,
which makes the mismatch worse rather than better: the server now sells
correctly while the webapp refuses to let anyone buy. On aio-demo three
events are past the threshold — "Tech Meetup" has 9 tickets left, shows
-2, and the button is dead. The API confirms they are sellable.

Also makes ticket capacity a required field, at least 1. A capacity of 0
used to mean "unlimited", but nothing on the backend agreed: both
api_get_event and api_ticket_create read `amount_tickets < 1` as sold
out, so a 0 produced an event that advertised unlimited tickets on the
card and returned 410 to every purchase. aiolabs/events#62 stops
publishing that lie; this stops the form creating it.

The payload check moves from falsy to `!== undefined`, which is what
unblocks the backend making the field required — a falsy check dropped
0 from the request entirely, so a server-side `ge=1` would have 422'd
this dialog for precisely the case users were picking.

Editing an existing zero-capacity event now opens with the field empty
rather than prefilled with 0, so the organiser has to choose.

Closes #173
padreug deleted branch fix/my-events-availability-arithmetic 2026-09-27 21:09:35 +00:00
Sign in to join this conversation.
No description provided.