fix(tickets): amount_tickets is the remaining count, stop subtracting sold #59

Merged
padreug merged 1 commit from fix/amount-tickets-remaining into main 2026-09-27 20:27:10 +00:00
Owner

Refs #34 — the live half of it. Deliberately not Closes; see scope below.

set_ticket_paid decrements amount_tickets on every sale and increments sold, so the two describe the same tickets. api_ticket_create then did:

remaining = event.amount_tickets - event.sold

which removes each sale twice.

This is refusing sales right now

Because remaining + sold is the original capacity, sold >= amount_tickets first becomes true at exactly the halfway point — so every event locks itself as sold out once half its seats have gone. Measured on aio-demo before the fix:

24 live events; 16 affected

Tech Meetup            9 remaining,  11 sold  ->  offers -2   FALSE SOLD-OUT
pups leash training    2 remaining,   3 sold  ->  offers -1   FALSE SOLD-OUT
Art therapy            5 remaining,   5 sold  ->  offers  0   FALSE SOLD-OUT
Transhumance           9 remaining,   8 sold  ->  offers  1
Unicorns Revival      50 remaining,   5 sold  ->  offers 45

Three events turning away buyers with stock on the shelf, thirteen more under-reporting availability. Demo is test data, but the same code runs on cfaun, four84 and atio.

The fix removes divergence rather than adding any

The arithmetic was ours, introduced with the multi-quantity purchase feature. Upstream has no equivalent — it sells one ticket per request and reads amount_tickets as remaining everywhere (services.py:95-105, views_api.py:188 and :415). So this restores upstream's own guard plus the single line quantity actually needs:

if event.amount_tickets < 1:
    raise HTTPException(status_code=HTTPStatus.GONE, detail="Event is sold out.")
if quantity > event.amount_tickets:
    raise HTTPException(..., detail=f"Only {event.amount_tickets} ticket(s) remaining...")

Net effect on #33: slightly easier, not harder. Checked the rest of the codebase — this arithmetic existed in exactly one place, including the Quasar frontend.

Scope

Only the double-subtraction. The rest of #34 waits for #33, as agreed on the issue:

  • making capacity a required field and dropping the 0 = unlimited reading
  • the organizer form that prefills remaining into a field labelled "Tickets", which is what produced the 10 of 17 inflation
  • whether "X of Y" can be displayed honestly at all under remaining-only semantics

Those need ticket waves in view, since v1.6.8 makes amount_tickets a per-wave roll-up. This part doesn't.

Testing

84 passed (74 + 10 new). The tests walk a 50-seat event to capacity one sale at a time, and pin the boundary empirically rather than by assertion: sold=49 passes even without the fix, sold=50 is where it used to lock. Six of the ten fail on the unfixed code. ruff and black clean.

A detail worth knowing that the tests surfaced: CreateTicket.quantity is capped at le=10 by the model, so the under-reported buyer cap only ever bit events with fewer than ~10 apparent tickets left. The false sold-out is the serious half.

Deploy note

Existing rows need no migration — amount_tickets already holds the correct remaining count. Affected events simply start selling again once this ships. Worth re-running the exposure query on each host afterwards to confirm nothing is still stuck.

No config.json bump — version gets cut on main per the release procedure.

Refs #34 — the live half of it. Deliberately **not** `Closes`; see scope below. `set_ticket_paid` decrements `amount_tickets` on every sale *and* increments `sold`, so the two describe the same tickets. `api_ticket_create` then did: ```python remaining = event.amount_tickets - event.sold ``` which removes each sale twice. ## This is refusing sales right now Because `remaining + sold` is the original capacity, `sold >= amount_tickets` first becomes true at exactly the **halfway point** — so every event locks itself as sold out once half its seats have gone. Measured on aio-demo before the fix: ``` 24 live events; 16 affected Tech Meetup 9 remaining, 11 sold -> offers -2 FALSE SOLD-OUT pups leash training 2 remaining, 3 sold -> offers -1 FALSE SOLD-OUT Art therapy 5 remaining, 5 sold -> offers 0 FALSE SOLD-OUT Transhumance 9 remaining, 8 sold -> offers 1 Unicorns Revival 50 remaining, 5 sold -> offers 45 ``` Three events turning away buyers with stock on the shelf, thirteen more under-reporting availability. Demo is test data, but the same code runs on cfaun, four84 and atio. ## The fix removes divergence rather than adding any The arithmetic was ours, introduced with the multi-quantity purchase feature. Upstream has no equivalent — it sells one ticket per request and reads `amount_tickets` as remaining everywhere (`services.py:95-105`, `views_api.py:188` and `:415`). So this restores upstream's own guard plus the single line `quantity` actually needs: ```python if event.amount_tickets < 1: raise HTTPException(status_code=HTTPStatus.GONE, detail="Event is sold out.") if quantity > event.amount_tickets: raise HTTPException(..., detail=f"Only {event.amount_tickets} ticket(s) remaining...") ``` Net effect on #33: slightly easier, not harder. Checked the rest of the codebase — this arithmetic existed in exactly one place, including the Quasar frontend. ## Scope Only the double-subtraction. The rest of #34 waits for #33, as agreed on the issue: - making capacity a required field and dropping the `0 = unlimited` reading - the organizer form that prefills *remaining* into a field labelled "Tickets", which is what produced the `10 of 17` inflation - whether "X of Y" can be displayed honestly at all under remaining-only semantics Those need ticket waves in view, since v1.6.8 makes `amount_tickets` a per-wave roll-up. This part doesn't. ## Testing `84 passed` (74 + 10 new). The tests walk a 50-seat event to capacity one sale at a time, and pin the boundary empirically rather than by assertion: `sold=49` passes even **without** the fix, `sold=50` is where it used to lock. Six of the ten fail on the unfixed code. ruff and black clean. A detail worth knowing that the tests surfaced: `CreateTicket.quantity` is capped at `le=10` by the model, so the under-reported buyer cap only ever bit events with fewer than ~10 apparent tickets left. The false sold-out is the serious half. ## Deploy note Existing rows need no migration — `amount_tickets` already holds the correct remaining count. Affected events simply start selling again once this ships. Worth re-running the exposure query on each host afterwards to confirm nothing is still stuck. No `config.json` bump — version gets cut on main per the release procedure.
fix(tickets): amount_tickets is the remaining count, stop subtracting sold
Some checks failed
lint.yml / fix(tickets): amount_tickets is the remaining count, stop subtracting sold (pull_request) Failing after 0s
ad8aa2cf00
`set_ticket_paid` decrements `amount_tickets` on every sale and
increments `sold`, so the two describe the same tickets. Subtracting
one from the other in `api_ticket_create` removed each sale twice:

  remaining = amount_tickets - sold

That under-reported availability to buyers, and because
`remaining + sold` is the original capacity, `sold >= amount_tickets`
first becomes true at the halfway point — so every event locked itself
as sold out once half its seats had gone, with the rest still unsold.

Measured on aio-demo before the fix: 16 of 24 live events affected,
3 of them already refusing sales while stock remained. "Tech Meetup"
had 9 tickets left and offered -2.

The arithmetic was ours, introduced with the multi-quantity purchase
feature; upstream has no equivalent because it sells one ticket per
request and reads `amount_tickets` as remaining everywhere. So this
deletes the divergence and restores upstream's own guard plus the one
line `quantity` needs, which should also make the v1.6.8 rebase (#33)
a little easier rather than harder.

Tests walk a 50-seat event to capacity one sale at a time and pin the
boundary: sold=49 passes even unfixed, sold=50 is where it used to
lock. Six of the ten fail without the change.

Scoped deliberately to the double-subtraction. The rest of #34 — making
capacity a required field, dropping the `0 = unlimited` reading, and the
organizer form that prefills remaining as though it were capacity —
waits for #33, since ticket waves change the shape of that fix.

Refs #34
padreug deleted branch fix/amount-tickets-remaining 2026-09-27 20:27:10 +00:00
padreug referenced this pull request from a commit 2026-09-27 20:27:44 +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!59
No description provided.