diff --git a/nostr_publisher.py b/nostr_publisher.py index 677d96f..0e5b4c4 100644 --- a/nostr_publisher.py +++ b/nostr_publisher.py @@ -46,7 +46,7 @@ def build_nip52_event(event: Event, pubkey: str) -> NostrEvent: start - unix timestamp (31923) or YYYY-MM-DD (31922) end - same encoding (optional) image, location, t (categories) - optional - tickets_available - current remaining capacity (omitted when unlimited) + tickets_available - current remaining capacity (always emitted; 0 = sold out) tickets_sold - running paid-count (always emitted; clients can derive original_capacity = available + sold) tickets_price - price_per_ticket (always emitted; 0 means free) @@ -96,11 +96,14 @@ def build_nip52_event(event: Event, pubkey: str) -> NostrEvent: for cat in event.categories or []: tags.append(["t", cat]) - # `amount_tickets == 0` means unlimited capacity in this extension's - # schema. Omitting the tag is how clients distinguish unlimited from - # "0 left" (sold out). - if event.amount_tickets > 0: - tags.append(["tickets_available", str(event.amount_tickets)]) + # Always emitted, including zero. Omitting it used to mean "unlimited", + # which contradicted every other reader: `api_get_event` and + # `api_ticket_create` both treat `amount_tickets < 1` as sold out, so a + # zero-capacity event advertised "Unlimited tickets" on the card while + # the detail page and the purchase both returned 410 (aiolabs/events#34). + # Clients that must still handle an absent tag — a NIP-52 event from + # some other publisher — are unaffected; we simply never omit it. + tags.append(["tickets_available", str(event.amount_tickets)]) tags.append(["tickets_sold", str(event.sold)]) tags.append(["tickets_price", str(event.price_per_ticket)]) tags.append(["tickets_currency", event.currency]) diff --git a/static/js/index.vue b/static/js/index.vue index d53f4c8..fc7db77 100644 --- a/static/js/index.vue +++ b/static/js/index.vue @@ -614,9 +614,9 @@ dense v-model.number="formDialog.data.amount_tickets" type="number" - min="0" + min="1" label="Amount of tickets" - hint="0 = unlimited" + hint="Total tickets on sale" >
diff --git a/tests/test_publish_availability.py b/tests/test_publish_availability.py new file mode 100644 index 0000000..0ef88a5 --- /dev/null +++ b/tests/test_publish_availability.py @@ -0,0 +1,56 @@ +"""`tickets_available` is always published, including zero (#34). + +Omitting the tag used to mean "unlimited", which contradicted every other +reader: `api_get_event` and `api_ticket_create` both treat +`amount_tickets < 1` as sold out. On aio-demo three zero-capacity events +advertised "Unlimited tickets" on the card while the detail endpoint and +the purchase both returned 410. +""" + +from datetime import datetime, timezone + +import pytest + +from ..models import Event +from ..nostr_publisher import build_nip52_event + +PUBKEY = "a" * 64 + + +def _event(amount_tickets: int, sold: int = 0) -> Event: + return Event( + id="evt", + wallet="w", + name="Availability", + info="", + closing_date="2030-01-01", + event_start_date="2030-01-01T18:00", + event_end_date="2030-01-01T22:00", + currency="sat", + price_per_ticket=0, + amount_tickets=amount_tickets, + sold=sold, + time=datetime.now(timezone.utc), + status="approved", + ) + + +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} + + +@pytest.mark.parametrize("amount", [0, 1, 50]) +def test_tickets_available_is_always_present(amount): + assert _tags(_event(amount))["tickets_available"] == str(amount) + + +def test_zero_capacity_reads_as_sold_out_not_unlimited(): + """The regression: an absent tag is what clients render as unlimited.""" + tags = _tags(_event(0, sold=0)) + assert "tickets_available" in tags + assert tags["tickets_available"] == "0" + + +def test_sold_out_after_selling_through_still_publishes_zero(): + assert _tags(_event(0, sold=25))["tickets_available"] == "0" + assert _tags(_event(0, sold=25))["tickets_sold"] == "25"