events/docs/upstream-candidates.md
Padreug 15c2276e57
Some checks failed
lint.yml / feat(nostr): publish the active ticket wave, not the roll-up (pull_request) Failing after 0s
feat(nostr): publish the active ticket wave, not the roll-up
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.
2026-09-28 22:28:09 +02:00

12 KiB

Upstream PR candidates

Running log of fork features that are shaped so they could be offered to lnbits/events (or lnbits/lnbits). Add a row whenever a change lands here in an upstream-compatible form; strike it when the PR merges upstream.

Feature Where Upstream target Readiness
frontend_url + origin allow-list + ?checkout= return contract views_api.py _resolve_frontend_root, api_ticket_create lnbits/events after the #33 rebase, as a small PR
Ticket ids minted before the invoice so success_url can carry them api_ticket_create lnbits/events ships with the above
extra.checkout (success/cancel URL, customer_email, line item, metadata) on fiat purchases api_ticket_create lnbits/events (needs lnbits StripeCheckoutOptions.cancel_url/customer_email, PR'd from aiolabs/lnbits) with the lnbits patch
extra.payment_methods per event + tickets_payment_methods NIP-52 tag models.py, nostr_publisher.py lnbits/events (v2 PR #64 introduces the same field) offer as review input on #64
asyncio.to_thread around the smtplib send services.py _send_ticket_email_notification lnbits/events trivial, standalone
QR logo overlay in make_qr_png (instance lnbits_qr_logo) views_api.py lnbits/events standalone
Multi-ticket purchase as N rows on one payment_hash api_ticket_create, crud.py lnbits/events overlaps v2 baskets; review input on #64
Free tickets without minting an invoice _issue_free_tickets lnbits/events small, standalone
NIP-52 publishing + approval workflow nostr_*.py, views_api.py lnbits/events #46 open; rebase onto v1.6.8
Date + Message-ID + From display name on the ticket email (build_ticket_email); event details in the body services.py lnbits/events (mailer) and lnbits/lnbits send_email (same omissions, hits password-reset/admin mails) trivial, standalone — measured: SpamAssassin MISSING_DATE 1.4 + MISSING_MID 0.14
Ticket card PNG (event/when/where/QR/name/id) attached to the ticket email instead of a remote <img> (qr.py, GET /api/v1/ticket-card/{id}) qr.py, services.py lnbits/events (their "ticket image" compositing could reuse the renderer) standalone; mail-tester: removes HTML_IMAGE_ONLY (1.8)
Promo max_uses + derived used_count (per ticket; v2 counts per basket), POST /promo/validate/{event_id} with quantity instead of items, PublicEventExtra projection (v2 still exposes extra fully) promo.py, models.py, views_api.py lnbits/events (v2 PR #64) review input on #64
NIP-52 tags describe the active ticket wave (cheapest open wave; tickets_available: 0 when none is open), plus nostr_published_wave_id so the reconciliation sweep republishes at wave boundaries nostr_publisher.py, crud.py flag_wave_transitions, migrations_fork.py m004 lnbits/events #46 (rides with the NIP-52 publishing PR) deviation, deliberate — upstream has waves but publishes no calendar event, so there is nothing upstream to match. Several waves can be open at once and a publisher cannot ask which one the buyer wants (upstream's purchase path errors with "Please select a ticket wave"), so it advertises the cheapest — the price a buyer can actually obtain. Rationale and the rejected alternatives: aiolabs/events#61