fix(nostr): always publish tickets_available, zero is not unlimited #62

Merged
padreug merged 1 commit from fix/zero-capacity-is-not-unlimited into main 2026-09-27 21:10:34 +00:00
Owner

Refs #34 — the first of its two remaining halves. Not Closes; see the blocked half below.

Omitting tickets_available meant "unlimited capacity". Nothing else in the codebase agreed. api_get_event:287 and api_ticket_create:678 both treat amount_tickets < 1 as sold out. So a zero-capacity event advertised "Unlimited tickets" on the card while both the detail page and the purchase returned 410.

Observed live on aio-demo

Three approved, publicly listed, free events in exactly that state. For PKBVuusKikfJFU4PYGtBTW ("Alpaca Therapy"):

relay        tickets_available absent      -> webapp renders "Unlimited tickets"
GET  /events/api/v1/events/PKBVu...        -> HTTP 410 "Event is sold out."
POST /events/api/v1/tickets/PKBVu...       -> HTTP 410 "Event is sold out."

Listed in /events/api/v1/events/public throughout, since get_public_events filters on status and canceled only.

The admin form was actively recruiting organizers into this state — min="0" with hint="0 = unlimited". Both corrected here.

The fix

Always emit the tag; zero reads as sold out, which is what every other reader already believed.

Clients that must handle an absent tickets_available — a NIP-52 calendar event from some other publisher — are unaffected, because we simply never omit it. The webapp's available === undefined → "Unlimited tickets" branch stays as the foreign-event fallback, as agreed on #34.

Testing

96 passed (91 + 5 new). Three of the five fail without the change. ruff and black clean.

What's still blocked, and why

Making amount_tickets required — the rest of #34 — cannot land yet. The webapp's create dialog omits the field on a falsy value:

// CreateEventDialog.vue:432
if (formValues.amount_tickets) eventData.amount_tickets = formValues.amount_tickets

A server-side ge=1 would therefore 422 the webapp for precisely the case users currently choose. Webapp first, then this.

Separately — the webapp has the #59 bug

Worth flagging loudly since it's live and #59 didn't reach it. MyEventsPage.vue still does the double-subtraction the backend just stopped doing:

{{ event.amount_tickets - event.sold }}      <!-- :176  under-reports availability -->
event.amount_tickets <= event.sold           <!-- :190  DISABLES the purchase button -->

So the backend now sells correctly while the webapp disables the buy button at half capacity. Filing that against aiolabs/webapp; it's more urgent than the required-capacity change.

Deploy note

Existing zero-capacity rows are not migrated — we cannot guess whether the organizer meant "unlimited" or forgot to set a number, and they are already unsellable. After deploying, they will correctly read as sold out once something republishes them; the #55 sweep will not flag them on its own, so /republish-all is the way to refresh. Organizers can then set a real capacity by editing the event.

Refs #34 — the first of its two remaining halves. Not `Closes`; see the blocked half below. Omitting `tickets_available` meant "unlimited capacity". **Nothing else in the codebase agreed.** `api_get_event:287` and `api_ticket_create:678` both treat `amount_tickets < 1` as sold out. So a zero-capacity event advertised "Unlimited tickets" on the card while both the detail page and the purchase returned 410. ## Observed live on aio-demo Three approved, publicly listed, free events in exactly that state. For `PKBVuusKikfJFU4PYGtBTW` ("Alpaca Therapy"): ``` relay tickets_available absent -> webapp renders "Unlimited tickets" GET /events/api/v1/events/PKBVu... -> HTTP 410 "Event is sold out." POST /events/api/v1/tickets/PKBVu... -> HTTP 410 "Event is sold out." ``` Listed in `/events/api/v1/events/public` throughout, since `get_public_events` filters on status and `canceled` only. The admin form was actively recruiting organizers into this state — `min="0"` with `hint="0 = unlimited"`. Both corrected here. ## The fix Always emit the tag; zero reads as sold out, which is what every other reader already believed. Clients that must handle an *absent* `tickets_available` — a NIP-52 calendar event from some other publisher — are unaffected, because we simply never omit it. The webapp's `available === undefined → "Unlimited tickets"` branch stays as the foreign-event fallback, as agreed on #34. ## Testing `96 passed` (91 + 5 new). Three of the five fail without the change. ruff and black clean. ## What's still blocked, and why Making `amount_tickets` **required** — the rest of #34 — cannot land yet. The webapp's create dialog omits the field on a falsy value: ```js // CreateEventDialog.vue:432 if (formValues.amount_tickets) eventData.amount_tickets = formValues.amount_tickets ``` A server-side `ge=1` would therefore 422 the webapp for precisely the case users currently choose. Webapp first, then this. ## Separately — the webapp has the #59 bug Worth flagging loudly since it's live and #59 didn't reach it. `MyEventsPage.vue` still does the double-subtraction the backend just stopped doing: ```vue {{ event.amount_tickets - event.sold }} <!-- :176 under-reports availability --> event.amount_tickets <= event.sold <!-- :190 DISABLES the purchase button --> ``` So the backend now sells correctly while the webapp disables the buy button at half capacity. Filing that against `aiolabs/webapp`; it's more urgent than the required-capacity change. ## Deploy note Existing zero-capacity rows are not migrated — we cannot guess whether the organizer meant "unlimited" or forgot to set a number, and they are already unsellable. After deploying, they will correctly read as sold out **once something republishes them**; the #55 sweep will not flag them on its own, so `/republish-all` is the way to refresh. Organizers can then set a real capacity by editing the event.
fix(nostr): always publish tickets_available, zero is not unlimited
Some checks failed
lint.yml / fix(nostr): always publish tickets_available, zero is not unlimited (pull_request) Failing after 0s
adfd4529f5
Omitting the tag used to mean "unlimited capacity". Nothing else in the
codebase agreed: `api_get_event` and `api_ticket_create` both treat
`amount_tickets < 1` as sold out. So a zero-capacity event advertised
"Unlimited tickets" on the card while the detail page and the purchase
both returned 410.

Observed on aio-demo — three approved, listed, free events in that
state. For `PKBVuusKikfJFU4PYGtBTW`:

  relay        tickets_available absent  -> webapp renders "Unlimited"
  GET  event   410 "Event is sold out."
  POST ticket  410 "Event is sold out."

The admin form was advertising it too (`min="0"`, `hint="0 = unlimited"`),
so organizers were being invited into the broken state.

Now the tag is always emitted and zero reads as sold out, which is what
every other part of the system already believed. Clients that must handle
an absent tag — a NIP-52 event from another publisher — are unaffected,
since we simply never omit it.

Scoped to removing the contradiction. Making capacity a *required* field
is the other half of #34 and is blocked on the webapp: its create dialog
uses a falsy check (`if (formValues.amount_tickets)`), so a 0 omits the
field entirely and a server-side `ge=1` would 422 it.

Refs #34
padreug deleted branch fix/zero-capacity-is-not-unlimited 2026-09-27 21:10:35 +00:00
padreug referenced this pull request from a commit 2026-09-27 21:12:16 +00:00
padreug referenced this pull request from a commit 2026-09-28 22:09:49 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/events!62
No description provided.