Keep ticket waves when a client edits an event without them #65

Merged
padreug merged 1 commit from fix/preserve-ticket-waves-on-edit into main 2026-09-29 06:58:25 +00:00
Owner

Data loss, found while checking whether the webapp had been updated for waves. Refs #33.

The bug

api_event_update replaces extra wholesale. A client that rebuilds the envelope instead of round-tripping it destroyed every wave: the list arrived empty, ensure_ticket_waves synthesized a single primary wave from the event-level amount_tickets, and a multi-wave event silently collapsed into one tier carrying whatever numbers that client happened to send.

Reproduced against a running instance before fixing — a PUT whose extra omitted the key turned a two-wave event into:

waves before: 2
result: amount_tickets=999 price=77.0 waves=1
        primary  "Primary wave"  77.0  999

No error, 200 response, both tiers gone.

The fix

Carry the stored waves over unless the request names the key.

promo_codes has had exactly this guard since the v1.6.8 merge, for exactly this reason, ten lines further down the same function. ticket_waves is the same class of state — organiser-managed, living in extra, invisible to a client that doesn't implement it. I wrote the first guard and didn't extend the reasoning to the second; this closes that.

The carry-over runs before _validate_wave_capacity, which matters: otherwise a client that omitted both the waves and a real capacity would be rejected for a zero-capacity primary wave that only existed because its waves had just been dropped. An explicit [] still resets, matching the promo-codes contract.

Scope of exposure

The aio webapp is not what surfaced this and is not affected — CreateEventDialog does ...(props.event?.extra ?? {}), so it preserves waves as an undeclared runtime key, by accident rather than design. Any client that builds extra from scratch is exposed, which now includes anything written against the documented EventExtra shape, since ticket_waves isn't in the webapp's type either.

Tests

6 new, including one that pins the ordering against _validate_wave_capacity and one parameterised over both organiser-owned extra fields, so dropping either guard fails. 126 pass; ruff, black clean; mypy error set unchanged from baseline.

Not yet re-run against the live instance — it needs a restart to pick up the change, and I'd batch that with the webapp work rather than ask for two.

Release

main is already tagged v1.6.8-aio.1, so this wants either a v1.6.8-aio.2 or a ride-along with the next events release — your call. Nothing is breaking in production today, since the webapp is the only client editing events and it's incidentally safe.

Data loss, found while checking whether the webapp had been updated for waves. Refs #33. ## The bug `api_event_update` replaces `extra` wholesale. A client that rebuilds the envelope instead of round-tripping it destroyed every wave: the list arrived empty, `ensure_ticket_waves` synthesized a single primary wave from the event-level `amount_tickets`, and a multi-wave event silently collapsed into one tier carrying whatever numbers that client happened to send. Reproduced against a running instance before fixing — a PUT whose `extra` omitted the key turned a two-wave event into: ``` waves before: 2 result: amount_tickets=999 price=77.0 waves=1 primary "Primary wave" 77.0 999 ``` No error, 200 response, both tiers gone. ## The fix Carry the stored waves over unless the request names the key. `promo_codes` has had exactly this guard since the v1.6.8 merge, for exactly this reason, ten lines further down the same function. `ticket_waves` is the same class of state — organiser-managed, living in `extra`, invisible to a client that doesn't implement it. I wrote the first guard and didn't extend the reasoning to the second; this closes that. The carry-over runs **before** `_validate_wave_capacity`, which matters: otherwise a client that omitted both the waves and a real capacity would be rejected for a zero-capacity primary wave that only existed because its waves had just been dropped. An explicit `[]` still resets, matching the promo-codes contract. ## Scope of exposure The aio webapp is **not** what surfaced this and is not affected — `CreateEventDialog` does `...(props.event?.extra ?? {})`, so it preserves waves as an undeclared runtime key, by accident rather than design. Any client that builds `extra` from scratch is exposed, which now includes anything written against the documented `EventExtra` shape, since `ticket_waves` isn't in the webapp's type either. ## Tests 6 new, including one that pins the ordering against `_validate_wave_capacity` and one parameterised over both organiser-owned `extra` fields, so dropping either guard fails. 126 pass; ruff, black clean; mypy error set unchanged from baseline. Not yet re-run against the live instance — it needs a restart to pick up the change, and I'd batch that with the webapp work rather than ask for two. ## Release `main` is already tagged `v1.6.8-aio.1`, so this wants either a `v1.6.8-aio.2` or a ride-along with the next events release — your call. Nothing is breaking in production today, since the webapp is the only client editing events and it's incidentally safe.
fix: keep ticket waves when a client edits an event without them
Some checks failed
lint.yml / fix: keep ticket waves when a client edits an event without them (pull_request) Failing after 0s
064795c62a
`api_event_update` replaces `extra` wholesale, so a client that rebuilds
the envelope rather than round-tripping it destroyed every wave: the list
arrived empty, `ensure_ticket_waves` synthesized a single primary wave
from the event-level `amount_tickets`, and a multi-wave event silently
collapsed into one tier carrying whatever numbers that client sent.

Reproduced against a running instance before fixing — a PUT whose `extra`
omitted the key turned a two-wave event into:

    amount_tickets=999 price=77.0 waves=1
    primary "Primary wave" 77.0 999

`promo_codes` has carried the same guard since the v1.6.8 merge, for the
same reason and in the same function; `ticket_waves` is the same class of
state — organiser-managed, living in `extra`, and invisible to a client
that does not implement it. I added the first and did not extend the
reasoning to the second.

The carry-over runs before `_validate_wave_capacity` so validation sees
the waves the event will actually end up with; otherwise a client that
omitted both the waves and a real capacity would be rejected for a
zero-capacity primary wave that existed only because its waves had just
been dropped. An explicit `[]` still resets, matching promo codes.

Note the aio webapp is not what surfaced this — it spreads the existing
`extra` and so preserves waves by accident. Any client that does not is
exposed.

6 tests, including one pinning the ordering and one that fails if either
organiser-owned `extra` field loses its guard. 126 pass.
padreug deleted branch fix/preserve-ticket-waves-on-edit 2026-09-29 06:58:26 +00:00
padreug referenced this pull request from a commit 2026-09-29 07:24:41 +00:00
padreug referenced this pull request from a commit 2026-09-29 07:25:21 +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!65
No description provided.