Some checks failed
lint.yml / fix: honour per-wave fiat when the organiser set an explicit rail list (pull_request) Failing after 0s
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.
219 lines
6.3 KiB
Python
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)) == []
|