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.
195 lines
6.1 KiB
Python
195 lines
6.1 KiB
Python
"""The NIP-52 tags describe the ACTIVE ticket wave, not the roll-up (#61).
|
|
|
|
Upstream v1.6.8 moved price, currency and inventory onto time-boxed waves
|
|
and made the event-level fields derived: `price_per_ticket` and `currency`
|
|
become the PRIMARY (first) wave's, `amount_tickets` the SUM across every
|
|
wave. Publishing those would advertise the early-bird price after early
|
|
bird closed and count stock in waves that have not opened yet.
|
|
"""
|
|
|
|
from datetime import datetime, timedelta, timezone
|
|
from typing import cast
|
|
|
|
import pytest
|
|
|
|
from ..models import (
|
|
Event,
|
|
EventExtra,
|
|
TicketWave,
|
|
advertised_wave_key,
|
|
sync_event_ticket_waves,
|
|
)
|
|
from ..nostr_publisher import build_nip52_event
|
|
|
|
PUBKEY = "a" * 64
|
|
TODAY = datetime.now(timezone.utc).date()
|
|
|
|
|
|
def _day(offset: int) -> str:
|
|
return (TODAY + timedelta(days=offset)).isoformat()
|
|
|
|
|
|
def _wave(
|
|
wave_id: str,
|
|
price: float,
|
|
stock: int,
|
|
opens: int,
|
|
closes: int,
|
|
*,
|
|
currency: str = "EUR",
|
|
allow_fiat: bool = False,
|
|
) -> TicketWave:
|
|
return TicketWave(
|
|
id=wave_id,
|
|
title=wave_id,
|
|
opening_date=_day(opens),
|
|
closing_date=_day(closes),
|
|
currency=currency,
|
|
price_per_ticket=price,
|
|
amount_tickets=stock,
|
|
allow_fiat=allow_fiat,
|
|
fiat_currency="GBP",
|
|
)
|
|
|
|
|
|
def _event(waves: list[TicketWave], sold: int = 0) -> Event:
|
|
event = Event(
|
|
id="evt",
|
|
wallet="w",
|
|
name="Waves",
|
|
info="",
|
|
closing_date=_day(30),
|
|
event_start_date=_day(30),
|
|
event_end_date=_day(30),
|
|
currency="sat",
|
|
price_per_ticket=0,
|
|
amount_tickets=0,
|
|
sold=sold,
|
|
time=datetime.now(timezone.utc),
|
|
status="approved",
|
|
extra=EventExtra(ticket_waves=waves),
|
|
)
|
|
# Mirrors the CRUD layer, which syncs on every read and write.
|
|
return cast(Event, sync_event_ticket_waves(event))
|
|
|
|
|
|
def _tags(event: Event) -> dict[str, str]:
|
|
return {t[0]: t[1] for t in build_nip52_event(event, PUBKEY).tags if len(t) > 1}
|
|
|
|
|
|
CLOSED_EARLY_BIRD = _wave("early", 10.0, 5, -10, -1)
|
|
OPEN_REGULAR = _wave("regular", 25.0, 40, 0, 20)
|
|
UNOPENED_VIP = _wave("vip", 50.0, 10, 5, 25)
|
|
|
|
|
|
def test_closed_wave_price_is_not_advertised():
|
|
"""The defect this fixes: early bird closed yesterday."""
|
|
event = _event([CLOSED_EARLY_BIRD, OPEN_REGULAR, UNOPENED_VIP])
|
|
|
|
# What the event-level roll-up would have published.
|
|
assert event.price_per_ticket == 10.0
|
|
assert event.amount_tickets == 55
|
|
|
|
tags = _tags(event)
|
|
assert tags["tickets_price"] == "25.0"
|
|
assert tags["tickets_available"] == "40"
|
|
|
|
|
|
def test_unopened_wave_stock_is_not_counted():
|
|
event = _event([OPEN_REGULAR, UNOPENED_VIP])
|
|
assert _tags(event)["tickets_available"] == "40"
|
|
|
|
|
|
def test_cheapest_open_wave_wins_when_several_are_open():
|
|
"""A publisher cannot ask which wave the buyer wants, so it advertises
|
|
the price a buyer is actually able to obtain."""
|
|
cheaper = _wave("cheap", 15.0, 3, 0, 10)
|
|
tags = _tags(_event([OPEN_REGULAR, cheaper]))
|
|
assert tags["tickets_price"] == "15.0"
|
|
assert tags["tickets_available"] == "3"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"waves",
|
|
[
|
|
pytest.param([CLOSED_EARLY_BIRD], id="all-closed"),
|
|
pytest.param([UNOPENED_VIP], id="not-yet-open"),
|
|
pytest.param([_wave("only", 25.0, 0, 0, 20)], id="sold-out"),
|
|
],
|
|
)
|
|
def test_no_open_wave_publishes_zero_availability(waves):
|
|
"""Never omit the tag: omission meant "unlimited" (#34, #62)."""
|
|
tags = _tags(_event(waves))
|
|
assert tags["tickets_available"] == "0"
|
|
|
|
|
|
def test_currency_and_fiat_follow_the_advertised_wave():
|
|
fiat_primary = _wave("a", 10.0, 0, -10, -1, currency="GBP", allow_fiat=True)
|
|
sats_open = _wave("b", 2500.0, 9, 0, 20, currency="sat", allow_fiat=False)
|
|
tags = _tags(_event([fiat_primary, sats_open]))
|
|
|
|
assert tags["tickets_currency"] == "sat"
|
|
assert "tickets_allow_fiat" not in tags
|
|
# Must agree with tickets_allow_fiat — they are the same fact, and a
|
|
# client rendering a fiat button here would hit a purchase-time refusal.
|
|
assert tags["tickets_payment_methods"] == "lightning"
|
|
|
|
|
|
def test_payment_methods_offer_fiat_when_the_advertised_wave_accepts_it():
|
|
tags = _tags(
|
|
_event(
|
|
[
|
|
_wave("a", 10.0, 0, -10, -1, allow_fiat=False),
|
|
_wave("b", 25.0, 9, 0, 20, allow_fiat=True),
|
|
]
|
|
)
|
|
)
|
|
assert tags["tickets_allow_fiat"] == "true"
|
|
assert tags["tickets_payment_methods"] == "lightning,fiat"
|
|
|
|
|
|
def test_sold_stays_event_level():
|
|
"""`tickets_sold` is the total paid across all waves."""
|
|
assert _tags(_event([OPEN_REGULAR], sold=7))["tickets_sold"] == "7"
|
|
|
|
|
|
def test_advertised_wave_key_tracks_the_published_wave():
|
|
assert advertised_wave_key(_event([CLOSED_EARLY_BIRD, OPEN_REGULAR])) == "regular"
|
|
# Published while nothing is on sale — a real state, distinct from the
|
|
# NULL that means "never published".
|
|
assert advertised_wave_key(_event([CLOSED_EARLY_BIRD])) == ""
|
|
|
|
|
|
def test_published_rails_drop_fiat_when_the_advertised_wave_cannot_take_it():
|
|
"""The webapp always sets `extra.payment_methods`, so the explicit-list
|
|
path is the normal one — and it used to ignore the wave entirely.
|
|
|
|
That published `tickets_payment_methods: lightning,fiat` beside an
|
|
absent `tickets_allow_fiat`, and a card button the purchase endpoint
|
|
then refused ("Fiat payments are not enabled for this ticket wave").
|
|
"""
|
|
event = _event(
|
|
[
|
|
_wave("a", 10.0, 0, -10, -1, allow_fiat=True),
|
|
_wave("b", 25.0, 9, 0, 20, allow_fiat=False),
|
|
]
|
|
)
|
|
event.extra.payment_methods = ["lightning", "fiat"]
|
|
tags = _tags(event)
|
|
|
|
assert "tickets_allow_fiat" not in tags
|
|
assert tags["tickets_payment_methods"] == "lightning"
|
|
|
|
|
|
def test_published_rails_keep_fiat_when_the_advertised_wave_takes_it():
|
|
event = _event(
|
|
[
|
|
_wave("a", 10.0, 0, -10, -1, allow_fiat=False),
|
|
_wave("b", 25.0, 9, 0, 20, allow_fiat=True),
|
|
]
|
|
)
|
|
event.extra.payment_methods = ["lightning", "fiat"]
|
|
tags = _tags(event)
|
|
|
|
assert tags["tickets_allow_fiat"] == "true"
|
|
assert tags["tickets_payment_methods"] == "lightning,fiat"
|