feat(nostr): publish the active ticket wave, not the roll-up
Some checks failed
lint.yml / feat(nostr): publish the active ticket wave, not the roll-up (pull_request) Failing after 0s
Some checks failed
lint.yml / feat(nostr): publish the active ticket wave, not the roll-up (pull_request) Failing after 0s
Since upstream v1.6.8 price, currency and inventory belong to time-boxed ticket waves, and the event-level fields `sync_event_ticket_waves` derives are the PRIMARY wave's price/currency and the SUM of every wave's stock. The NIP-52 publisher read those, so as soon as an organiser created a second wave the public card would advertise the early-bird price after early bird closed and count stock in waves that had not opened. Refs #61. `build_nip52_event` now describes the wave a buyer can actually buy from: - several waves can be open at once, and a publisher has no one to ask which one the buyer wants (the purchase endpoint errors with "Please select a ticket wave"), so it advertises the CHEAPEST open wave — the price a buyer is able to obtain. Deviation recorded in docs/upstream-candidates.md. - with no open wave, `tickets_available` is 0 and never omitted: omission used to mean "unlimited", which #34/#62 removed as a concept. - `tickets_payment_methods` is scoped to the advertised wave too. It was derived from `event.allow_fiat` — the primary wave's — so it could offer a fiat rail while `tickets_allow_fiat` was absent and the purchase endpoint would refuse it. They are the same fact and now come from the same place. Wave boundaries are time-driven, and every republish we have is sale-driven, so nothing fires when early bird ends at midnight. Rather than add a scheduler, a publish records which wave it advertised (`nostr_published_wave_id`, m004) and the reconciliation sweep compares that against the wave that would be advertised now, setting `nostr_publish_pending` on a mismatch — reusing the existing retry path. NULL means "never published", which the sweep leaves alone so an upgrade does not republish the whole table on first boot. The selection rule lives in `models.advertised_ticket_wave` so the publisher and the drift detector cannot disagree about what is on the relay. 17 new tests; 114 pass. ruff, black, prettier clean; mypy error set still identical to HEAD's baseline.
This commit is contained in:
parent
d90a0f8322
commit
15c2276e57
10 changed files with 525 additions and 39 deletions
45
models.py
45
models.py
|
|
@ -151,6 +151,12 @@ class Event(BaseModel):
|
|||
# True means the relay's copy may be behind this row — see
|
||||
# migrations_fork.m003 and the sweep in __init__.events_start.
|
||||
nostr_publish_pending: bool = False
|
||||
# Which wave the last successful publish advertised (see
|
||||
# `advertised_wave_key`). NULL = never published; "" = published while
|
||||
# nothing was on sale. The sweep compares this against the current key
|
||||
# to catch wave boundaries, which are time-driven and so fire no
|
||||
# sale-triggered republish (aiolabs/events#61).
|
||||
nostr_published_wave_id: str | None = None
|
||||
|
||||
@validator("categories", pre=True)
|
||||
def parse_categories(cls, v):
|
||||
|
|
@ -186,18 +192,28 @@ class PublicEvent(BaseModel):
|
|||
return v or []
|
||||
|
||||
|
||||
def effective_payment_methods(event: "Event | PublicEvent | CreateEvent") -> list[str]:
|
||||
def effective_payment_methods(
|
||||
event: "Event | PublicEvent | CreateEvent",
|
||||
wave: "TicketWave | None" = None,
|
||||
) -> list[str]:
|
||||
"""Rails a buyer may pick for `event`.
|
||||
|
||||
Explicit `extra.payment_methods` wins; an empty list falls back to the
|
||||
pre-#payment-methods rule so events created before the field existed
|
||||
keep behaving the same (Lightning always, fiat iff `allow_fiat`).
|
||||
|
||||
Pass `wave` when the answer is about one specific ticket wave. Fiat is a
|
||||
per-wave opt-in since v1.6.8 and `event.allow_fiat` is only the PRIMARY
|
||||
wave's, so without it a publisher can advertise a fiat rail for an
|
||||
advertised wave that does not accept fiat — which the purchase endpoint
|
||||
then rejects (aiolabs/events#61). Callers asking the event-level
|
||||
question ("which rails did the organiser enable at all") leave it unset.
|
||||
"""
|
||||
explicit = list(getattr(event.extra, "payment_methods", []) or [])
|
||||
if explicit:
|
||||
return explicit
|
||||
methods = ["lightning"]
|
||||
if event.allow_fiat:
|
||||
if wave.allow_fiat if wave is not None else event.allow_fiat:
|
||||
methods.append("fiat")
|
||||
return methods
|
||||
|
||||
|
|
@ -430,6 +446,31 @@ def ensure_ticket_waves(event: Event | PublicEvent | CreateEvent) -> list[Ticket
|
|||
]
|
||||
|
||||
|
||||
def advertised_ticket_wave(event: "Event | PublicEvent") -> "TicketWave | None":
|
||||
"""The wave a public listing should describe, or None when nothing is
|
||||
on sale (sold out, between waves, or not yet open).
|
||||
|
||||
Several waves can be open at once. The purchase endpoint refuses to
|
||||
guess; a publisher has no one to ask, so it advertises the CHEAPEST
|
||||
open wave — the price a buyer is actually able to obtain.
|
||||
|
||||
Shared by the NIP-52 publisher and the wave-transition detector so the
|
||||
two cannot disagree about which wave is currently being advertised.
|
||||
"""
|
||||
active = get_active_ticket_waves(event)
|
||||
if not active:
|
||||
return None
|
||||
return min(active, key=lambda wave: wave.price_per_ticket)
|
||||
|
||||
|
||||
def advertised_wave_key(event: "Event | PublicEvent") -> str:
|
||||
"""Stable key for what a publish advertised. Empty string means "nothing
|
||||
on sale", which is a real published state and distinct from NULL in
|
||||
`nostr_published_wave_id` (never published)."""
|
||||
wave = advertised_ticket_wave(event)
|
||||
return wave.id if wave else ""
|
||||
|
||||
|
||||
def sync_event_ticket_waves(event: Event | CreateEvent) -> Event | CreateEvent:
|
||||
ticket_waves = ensure_ticket_waves(event)
|
||||
event.extra.ticket_waves = ticket_waves
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue