feat: expose ticket waves on public event responses
Some checks failed
lint.yml / feat: expose ticket waves on public event responses (pull_request) Failing after 0s

A buyer cannot choose a ticket wave without its id, and nothing public
carried one: `PublicEventExtra` is `EventExtraBase`, which did not
include `ticket_waves`, and the NIP-52 tags deliberately publish only
the active wave with no identifier (#61). So `GET /events/{id}` and
`/events/public` gave a client everything except the one field it needs
to say which tier it wants — and the purchase endpoint refuses when
several waves are open.

Moves `ticket_waves` from `EventExtra` to `EventExtraBase`, leaving
`promo_codes` as the only organizer-private field in `extra`.

A wave holds an id, title, date window, currency, price, remaining stock
and the fiat flags — the sales information a buyer needs in order to
choose. The only thing publishing it reveals is the upcoming price
schedule, which is precisely what #61 recorded as the cost of settling
on flat Nostr tags; over REST it is a gain rather than a leak.

Prerequisite for wave support in the webapp, which otherwise cannot
offer a wave picker to anyone but the organizer.

Two tests pin the projection: the public extra carries waves and never
promo codes, and a serialised `PublicEvent` shows the same.
This commit is contained in:
Padreug 2026-09-29 08:14:49 +02:00
commit 209a895415
2 changed files with 79 additions and 7 deletions

View file

@ -58,7 +58,8 @@ class TicketWave(BaseModel):
class EventExtraBase(BaseModel):
"""Everything in `extra` that is safe to show anyone. `EventExtra` adds
"""Everything in `extra` that is safe to show anyone — ticket waves
included, since a buyer needs a wave id to choose one. `EventExtra` adds
the organizer-only promo codes on top; `PublicEventExtra` is this base,
so anonymous responses can never carry them."""
@ -73,6 +74,19 @@ class EventExtraBase(BaseModel):
# `effective_payment_methods`. Same field name/shape as upstream v2 so the
# eventual rebase (#33) merges cleanly.
payment_methods: list[str] = Field(default_factory=list)
# Upstream v1.6.8 ticket waves — time-boxed pricing tiers. The
# event-level `currency` / `allow_fiat` / `amount_tickets` /
# `price_per_ticket` fields become derived values (see
# `sync_event_ticket_waves`), which is why fork code that reads them
# needs auditing — aiolabs/events#61.
#
# Public, not organizer-only: a buyer cannot choose a wave without its
# id, and neither the public event response nor the NIP-52 tags carried
# one before. A wave holds price, dates and remaining stock — the sales
# information a buyer needs — so the only thing exposing it reveals is
# the upcoming price schedule, which is what #61 regretted giving up
# when it settled on flat Nostr tags.
ticket_waves: list[TicketWave] = Field(default_factory=list)
@validator("payment_methods", pre=True)
def normalize_payment_methods(cls, v):
@ -92,12 +106,6 @@ class EventExtraBase(BaseModel):
class EventExtra(EventExtraBase):
promo_codes: list[PromoCode] = Field(default_factory=list)
# Upstream v1.6.8 ticket waves — time-boxed pricing tiers. The
# event-level `currency` / `allow_fiat` / `amount_tickets` /
# `price_per_ticket` fields become derived values (see
# `sync_event_ticket_waves`), which is why fork code that reads them
# needs auditing — aiolabs/events#61.
ticket_waves: list[TicketWave] = Field(default_factory=list)
PublicEventExtra = EventExtraBase