events/tests/test_ticket_models.py
Padreug b55d6866d6
Some checks failed
lint.yml / fix: honour per-wave fiat when the organiser set an explicit rail list (pull_request) Failing after 0s
fix: honour per-wave fiat when the organiser set an explicit rail list
Reported from aio-demo: an event with fiat enabled, Card offered at
checkout, and the purchase refused with "Fiat payments are not enabled
for this ticket wave."

`effective_payment_methods` returned the organiser's explicit
`extra.payment_methods` list before ever consulting the wave:

    explicit = list(...)
    if explicit:
        return explicit          # <- the wave never got a look in

So the `wave` argument I added for #61 did nothing in the common case.
The webapp always sets `extra.payment_methods` from its payment-method
checkboxes, which means the explicit path is the normal one, not the
exception — three layers then disagreed:

- the NIP-52 tag advertised `tickets_payment_methods: lightning,fiat`
  while omitting `tickets_allow_fiat`, contradicting itself
- the checkout rendered a Card button
- `api_ticket_create`, the only wave-aware check, refused the purchase

Asking about a specific wave means asking what a buyer can actually use
for it, so a rail that wave cannot honour is now dropped. The
event-level question (no wave) still reports every rail the organiser
enabled — that is what `/republish-all` and the admin views want.

Fiat-only rails on a non-fiat wave now yield an empty list, which is
honest: nothing is purchasable from that wave.

5 tests, including both publisher shapes with an explicit list — the
case that actually bit, and which the #61 tests missed because their
fixtures left `payment_methods` empty. 133 pass.

Does not fix the data on events already created through the webapp:
their waves were saved with `allow_fiat` unset, so those waves genuinely
cannot take fiat until toggled. That is aiolabs/webapp's side.
2026-09-30 19:32:07 +02:00

219 lines
6.3 KiB
Python

import pytest
from pydantic import ValidationError
from ..models import (
CreateEvent,
CreateTicket,
EventExtra,
PromoCode,
PublicEvent,
PublicEventExtra,
TicketWave,
effective_payment_methods,
)
def _ticket(**kwargs) -> CreateTicket:
return CreateTicket(**kwargs)
def test_user_id_only_is_a_valid_identity():
assert _ticket(user_id="u1").user_id == "u1"
def test_name_and_email_is_a_valid_guest_identity():
ticket = _ticket(name="Guest", email="guest@example.com")
assert ticket.user_id is None
assert ticket.email == "guest@example.com"
def test_user_id_may_carry_an_email_for_delivery():
ticket = _ticket(user_id="u1", email="me@example.com")
assert ticket.user_id == "u1"
assert ticket.email == "me@example.com"
@pytest.mark.parametrize(
"kwargs",
[
{},
{"name": "Guest"},
{"email": "guest@example.com"},
],
)
def test_missing_identity_is_rejected(kwargs):
with pytest.raises(ValidationError):
_ticket(**kwargs)
@pytest.mark.parametrize(
"url,expected",
[
("https://app.example/events", "https://app.example/events"),
("https://app.example/events/", "https://app.example/events"),
("http://localhost:5173/", "http://localhost:5173"),
(" ", None),
],
)
def test_frontend_url_is_normalised(url, expected):
assert _ticket(user_id="u1", frontend_url=url).frontend_url == expected
@pytest.mark.parametrize(
"url",
[
"/events", # relative
"ftp://app.example/events",
"https://app.example/events?x=1",
"https://app.example/events#top",
"https://app.example/../events",
"https://" + "a" * 520,
],
)
def test_frontend_url_rejects_unsafe_values(url):
with pytest.raises(ValidationError):
_ticket(user_id="u1", frontend_url=url)
def _event(**overrides) -> CreateEvent:
data = {
"wallet": "w",
"name": "Test",
"info": "",
"closing_date": "2030-01-01",
"event_start_date": "2030-01-01",
"event_end_date": "2030-01-02",
"amount_tickets": 10,
"price_per_ticket": 5,
}
data.update(overrides)
return CreateEvent(**data)
def test_effective_payment_methods_legacy_rule():
assert effective_payment_methods(_event()) == ["lightning"]
assert effective_payment_methods(_event(allow_fiat=True)) == ["lightning", "fiat"]
def test_effective_payment_methods_explicit_list_wins():
event = _event(allow_fiat=True, extra=EventExtra(payment_methods=["fiat"]))
assert effective_payment_methods(event) == ["fiat"]
def test_payment_methods_are_normalised_and_deduplicated():
extra = EventExtra(payment_methods=["Fiat", " lightning ", "fiat"])
assert extra.payment_methods == ["fiat", "lightning"]
assert EventExtra(payment_methods="lightning,fiat").payment_methods == [
"lightning",
"fiat",
]
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"]
# --- rails vs per-wave fiat --------------------------------------------------
def _fiat_wave(allow_fiat: bool):
from ..models import TicketWave
return TicketWave(
id="w",
title="w",
opening_date="2030-01-01",
closing_date="2030-02-01",
currency="EUR",
price_per_ticket=10,
amount_tickets=5,
allow_fiat=allow_fiat,
)
def test_explicit_rails_drop_fiat_for_a_wave_that_cannot_take_it():
"""The organiser's rail list is event-level; fiat is per-wave.
Without this the NIP-52 tag advertises fiat and the checkout renders a
card button that `api_ticket_create` refuses with "Fiat payments are
not enabled for this ticket wave" (reported on aio-demo).
"""
event = _event()
event.extra.payment_methods = ["lightning", "fiat"]
assert effective_payment_methods(event, _fiat_wave(True)) == ["lightning", "fiat"]
assert effective_payment_methods(event, _fiat_wave(False)) == ["lightning"]
def test_event_level_question_still_reports_every_rail():
"""No wave means "what did the organiser enable at all" — unfiltered."""
event = _event()
event.extra.payment_methods = ["lightning", "fiat"]
assert effective_payment_methods(event) == ["lightning", "fiat"]
def test_fiat_only_rails_on_a_non_fiat_wave_leave_nothing_purchasable():
event = _event()
event.extra.payment_methods = ["fiat"]
assert effective_payment_methods(event, _fiat_wave(False)) == []