diff --git a/models.py b/models.py index 1768b44..ef6ad6f 100644 --- a/models.py +++ b/models.py @@ -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 diff --git a/tests/test_ticket_models.py b/tests/test_ticket_models.py index 7d1ab3f..3a42e8f 100644 --- a/tests/test_ticket_models.py +++ b/tests/test_ticket_models.py @@ -5,6 +5,10 @@ from ..models import ( CreateEvent, CreateTicket, EventExtra, + PromoCode, + PublicEvent, + PublicEventExtra, + TicketWave, effective_payment_methods, ) @@ -108,3 +112,63 @@ def test_payment_methods_are_normalised_and_deduplicated(): def test_unknown_payment_method_is_rejected(): with pytest.raises(ValidationError): EventExtra(payment_methods=["cash"]) + + +# --- public projection ------------------------------------------------------ + + +def test_public_extra_exposes_waves_but_never_promo_codes(): + """A buyer needs a wave id to choose a tier, so waves are public; promo + codes stay organizer-only (aiolabs/events#61, v1.6.1-aio.12).""" + organizer = EventExtra( + promo_codes=[PromoCode(code="SECRET", discount_percent=50)], + ticket_waves=[ + TicketWave( + id="early", + title="Early", + opening_date="2030-01-01", + closing_date="2030-02-01", + currency="sat", + price_per_ticket=10, + amount_tickets=5, + ) + ], + ) + + public = PublicEventExtra(**organizer.dict()) + + assert [wave.id for wave in public.ticket_waves] == ["early"] + assert public.ticket_waves[0].price_per_ticket == 10 + assert not hasattr(public, "promo_codes") + assert "promo_codes" not in public.dict() + + +def test_public_event_response_carries_waves(): + """End of the chain: what `GET /events/{id}` actually serialises.""" + wave = TicketWave( + id="regular", + title="Regular", + opening_date="2030-01-01", + closing_date="2030-02-01", + currency="sat", + price_per_ticket=25, + amount_tickets=40, + ) + organizer_extra = EventExtra( + ticket_waves=[wave], promo_codes=[PromoCode(code="SECRET")] + ) + public = PublicEvent( + id="evt", + name="Test", + info="", + canceled=False, + event_start_date="2030-01-01", + currency="sat", + price_per_ticket=25, + banner=None, + extra=PublicEventExtra(**organizer_extra.dict()), + ) + body = public.dict() + + assert [w["id"] for w in body["extra"]["ticket_waves"]] == ["regular"] + assert "promo_codes" not in body["extra"]