Ticket waves: manage them, buy from them, stop lying about totals #176
No reviewers
Labels
No labels
app:activities
app:chat
app:chatelet
app:events
app:forum
app:libra
app:market
app:restaurant
app:tasks
app:wallet
app:webapp
bug
enhancement
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
aiolabs/webapp!176
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feat/ticket-waves"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Full wave support. The events extension moved price, currency and stock onto time-boxed waves in v1.6.8 and made the event-level fields derived — the webapp still read them, so it was wrong in three separate ways.
Closes #143. Needs
aiolabs/events#66 deployed (see Dependency below).What was broken
Organizer edits were silently discarded. The backend derives
amount_ticketsandprice_per_ticketfrom the waves on every write, so submitting changed values beside an unchanged wave list left the wave's old numbers winning. Verified against a running instance: 999 / 77 went in, 45 / 10.0 came back, HTTP 200, no error anywhere.Multi-wave events couldn't be bought, and the price shown wasn't the price charged. Nothing sent
ticket_wave_id, so an event with two open waves was refused outright ("Please select a ticket wave"). Worse when one wave was open: the dialog displayedevent.price_per_ticket— the primary wave's — while the backend charged the active one. After early bird closed, the webapp showed the old price and charged the new.The ticket total was fiction. "{available} of {total}" derived total as
available + sold. Already wrong after any edit re-baselined remaining (#143); now meaningless, sincetickets_availableis the active wave's stock whiletickets_soldcounts every wave. 5 left in the open wave with 7 sold earlier displayed "5 of 12".The four commits
8490cdftypes + arithmetic.lib/ticketWaves.tsmirrors the extension'sensure_ticket_waves,get_active_ticket_waves,advertised_ticket_wave,sync_event_ticket_wavesand the_resolve_ticket_waveselection rule.afa34c9checkout. Resolves a wave the way the backend does — one open wave implied, several means the buyer picks, none means nothing on sale. Price, currency andallow_fiatall read from that wave.ticket_wave_idrides on all three purchase paths and the promo preview, so quote and charge price the same tier.d55115dorganizer form. The price / capacity / currency fields are the primary wave, so they're written into it on submit. A new "Ticket waves" section manages later tiers, mirroringPromoCodesEditor.072ca88display. Total removed rather than recomputed.Two things worth a reviewer's attention
The mirror was cross-checked, not assumed. Every function in
ticketWaves.tswas run against the live Python with shared fixtures. That caught a divergence I'd otherwise have shipped: the backend keeps the time component on a synthesized primary wave'sclosing_date, andsync_event_ticket_wavestakes a string max over those. Truncating to a date — the obvious-looking thing, and what I wrote first — makes the client derive a different event closing date than the server. Truncation belongs at comparison time, inwaveDay, mirroring_parse_date.A crash caught while wiring the write-through. On create there's no stored event, so the implied primary wave was synthesized with empty date strings — which the backend throws on for every subsequent read (
ValueError: Invalid isoformat string: ''). The seed now supplies the same inputscreate_eventuses. Pinned by a test.Wave validation follows
_validate_wave_capacityincluding its subtlety: a wave already stored may sit at zero capacity, because that's what sold out looks like and rejecting it would make a sold-out event uneditable. Only newly added waves must state a capacity; stored zeroes render as "Sold out".Dependency
Needs aiolabs/events#66 (
ticket_waveson public event responses). Without it only the organizer can see waves, because the NIP-52 tags describe the active wave but carry no wave id — so the picker would never render for a buyer. The detail page fetches the LNbits record for the pickable tiers and treats failure as non-fatal (the backend answers 410 for sold-out events, where there's no tier to pick anyway).Verification
83 tests pass (24 new),
vue-tscclean, prettier clean on every touched file, production build succeeds.Not yet exercised against a running stack — that needs events#66 deployed to the dev LNbits plus a
npm run devsession. Worth doing ondev/aio-demo before this reaches main: create a two-wave event, check the picker appears, buy from the dearer wave and confirm the invoice matches it.Checkout was priced and requested off the event, which since events ext v1.6.8 means the PRIMARY wave. Two things were wrong at once: once early bird closed the dialog displayed the closed tier's price while the backend charged the open one, and an event with two waves open could not be bought at all — the purchase endpoint refuses to guess ("Please select a ticket wave") and nothing sent `ticket_wave_id`. The dialog now resolves a wave the way the backend does: a single open wave is implied, several means the buyer picks, none means nothing is on sale. Price, currency and fiat availability all read from that wave — `allow_fiat` too, which is a per-wave opt-in and could otherwise offer a rail the purchase would refuse. The picker only renders when there is an actual choice, so the common single-wave case gains no extra click, and the CTA is disabled rather than firing a request the backend will reject. `ticket_wave_id` now rides on all three purchase paths (discounted-free, Lightning, fiat) and on the promo preview, so the quote and the charge are priced against the same tier. Plumbing worth noting: the tags a card renders from describe the active wave but carry no wave id, by the flat-tag decision on aiolabs/events#61. So the detail page fetches the LNbits record for the pickable tiers — possible only because `ticket_waves` is public from v1.6.8-aio.2 (aiolabs/events#66). Non-fatal on failure: the backend answers 410 for sold-out or closed events, where there is no tier to pick anyway. 3 service tests pin the wire format, including that no empty wave id is sent when the buyer had no choice. 62 pass; vue-tsc and prettier clean.The card and detail page rendered "{available} of {total} tickets left", where total was derived as `available + sold`. That was already wrong after any organizer edit re-baselined remaining (#143), and waves make it meaningless: since events ext v1.6.8 `tickets_available` is the ACTIVE WAVE's stock while `tickets_sold` counts every wave, so the two no longer share a denominator. An event with 5 left in the open wave and 7 sold across earlier ones displayed "5 of 12". Nothing publishes a capacity — no tag, no field — so nothing can honestly show one. Both sites now use the existing `ticketsAvailable` string, which says only what the data knows. `EventTicketInfo.total` and the `ticketsRemainingOfTotal` string are removed rather than left unused, so neither can be picked up again. Closes #143. Whether to publish a real capacity remains open on aiolabs/events#34, which narrowed it to either dropping the total (this) or storing capacity and publishing an explicit `tickets_total` tag.