amount_tickets is remaining in one place and capacity in another — event goes "sold out" at half capacity #53
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Two parts of the extension disagree about what
amount_ticketsmeans.set_ticket_paid(services.py:72-73) treats it as remaining:api_ticket_create(views_api.py:673-682) treats it as original capacity:The decrement wins —
views_api.py:287(if event.amount_tickets < 1: sold out) and the NIP-52tickets_availabletag both read it as remaining, and the admin table labels it "No tickets" next to a separate "Sold" column.So on a 50-ticket event, after n sales
amount_tickets = 50 - nandsold = n, and the purchase endpoint computesremaining = 50 - 2n:nand will refuse orders it could fillsold >= amount_ticketsbecomes true atn = 25, so the event declares itself sold out with 25 tickets still availableLive on cfaun right now:
Q6mhQQzQPSXqkQuFndzXnfhasamount_tickets=45, sold=5, so an order larger than 40 is already refused, and it will go sold-out at 25.Fix
Pick one meaning and make everything agree.
amount_tickets-as-remaining is what the schema, the published tag and the admin UI already assume, so the smaller change is inapi_ticket_create:Worth a test that walks an event to full capacity and asserts the last ticket still sells. Check the other
sold/amount_ticketscomparisons at the same time —extra.min_ticketsin the conditional-event path readssoldand is fine, but the pair should be audited together.Upstream review before touching this
Checked
lnbits/events(upstream/main@891eaf1, tags through v1.6.8; our merge-base is v1.6.1). Upstream is completely consistent:amount_ticketsis remaining, everywhere, with no exceptions.services.py:95-105:and every capacity check is the same one line —
views_api.py:188and:415:event.soldis never subtracted fromamount_ticketsanywhere upstream. It's used only forextra.min_ticketson conditional events and for display.There's no
remaining = amount_tickets - soldupstream because there's nothing to compute it for: upstream'sCreateTickethas noquantity— one ticket per request. The bug is entirely ours, introduced with the multi-quantity purchase feature. So the fix is to delete the aio-introduced arithmetic, not to reconcile two competing conventions:That's upstream's guard plus the one line
quantityactually needs, which should rebase cleanly.Relevant for the rebase
Upstream v1.6.x added ticket waves (
TicketWave, per-waveamount_tickets+price_per_ticket,get_active_ticket_waves), and the event-level field became a derived roll-up —models.py:240:Still remaining-semantics, just aggregated. Our capacity logic will need to move to the selected wave when we rebase onto v1.6.8, so keeping this fix minimal and upstream-shaped matters more than usual. Worth a line in the "Rebase onto v1.6.8" issue.
Second, separate collision found while checking
Our fork has a third reading of the field that upstream doesn't share —
0 = unlimited:models.py:96—amount_tickets: int = 0 # 0 = unlimited / not ticketed(upstream has it required:Query(..., ge=0))nostr_publisher.py:99-103— omits thetickets_availabletag whenamount_tickets == 0, with the comment "Omitting the tag is how clients distinguish unlimited from '0 left' (sold out)"event.ts:100derivestotalonly whenavailable !== undefined, andEventDetailPage.vue:416renders "Unlimited tickets" foravailable === undefinedThat collides head-on with
views_api.py:287inapi_get_event(the public event detail endpoint), which is upstream'sif event.amount_tickets < 1: sold out. So an event created withamount_tickets = 0advertises "Unlimited tickets" on oyez while the public detail endpoint returns 410 GONE and every purchase is refused as sold out.Needs deciding before the fix above lands, since both touch the same predicate: either drop the unlimited concept and follow upstream (
0= sold out, capacity always required), or keep it and make the sold-out checksamount_tickets == 0 → unlimitedaware — which is a deliberate deviation to record indocs/upstream-candidates.mdand the rebase issue. Upstream's waves make the first option the cheaper one long-term.Decision: follow upstream, capacity always required
Dropping the
0 = unlimitedreading.amount_ticketsmeans remaining, full stop, matching upstream — which also means we don't fight the wave roll-up at rebase time.Free tickets are unaffected. They're gated on the computed charge, not on capacity (
views_api.py:743):A free event stays
price_per_ticket = 0with a realamount_tickets. The only thing lost is an uncapped event; large number if you want effectively-unlimited.Scope
views_api.py:673-682— delete the aio-introducedremaining = amount_tickets - soldarithmetic; upstream'sif event.amount_tickets < 1guard plusif quantity > event.amount_tickets.models.py:96—CreateEvent.amount_ticketsrequired instead of defaulting to 0, and drop the0 = unlimited / not ticketedcomment. Upstream has itQuery(..., ge=0);ge=1on create is a small deliberate deviation that stops an event being born dead — worth a line indocs/upstream-candidates.mdeither way.nostr_publisher.py:99-103— always emittickets_available; remove the omit-when-zero branch and its comment about clients distinguishing unlimited from sold out.Not changing the webapp.
EventDetailPage.vue:416/event.ts:100render "Unlimited tickets" whentickets_availableis absent, which becomes unreachable for our own events but stays correct for a NIP-52 event published by any third-party client — keep it as the foreign-event fallback, consistent with the client-agnostic target.Pre-deploy check
Any existing event rows with
amount_tickets = 0need a decision — backfill to a real capacity, or leave them reading as sold out. They're already broken today (api_get_event410s on them), but the migration shouldn't silently pick for the organizer. Check each castle host before this ships.Duplicate of #34, which diagnosed the same conflation in September with better evidence — it also found the part I missed, that the
10 of 17inflation comes from the webapp reconstructing a total that was never stored, and that the organizer form's prefill is what drives an organizer into setting capacity wrong. Filed this before checking the tracker — my mistake.The upstream review and the
0 = unlimitedcollision are reproduced as a comment on #34, along with the argument that upstream's wave roll-up makes thecapacitycolumn proposed there the more expensive option.Closing in favour of #34.