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
|
|
@ -10,7 +10,7 @@ Modes C and D were added later in the same rebase, from `services.py`.
|
|||
|
||||
## The four failure modes
|
||||
|
||||
Git resolves *text*. Neither of these produces a conflict marker.
|
||||
Git resolves _text_. Neither of these produces a conflict marker.
|
||||
|
||||
### A. Missed application
|
||||
|
||||
|
|
@ -23,14 +23,14 @@ so the invariant silently does not hold there.
|
|||
> `get_event` and `get_events` in `crud.py`. Both merged cleanly. But the
|
||||
> fork has four getters upstream never had — `get_all_events`,
|
||||
> `get_public_events`, `get_pending_events`,
|
||||
> `get_events_pending_republish` — and two of them *publish*
|
||||
> `get_events_pending_republish` — and two of them _publish_
|
||||
> (`/republish-all` and the #55 sweep). Without the same call they emit
|
||||
> the stale roll-up, so waves would have been wrong on exactly the paths
|
||||
> that push to relays, and nowhere else.
|
||||
|
||||
### B. Changed meaning
|
||||
|
||||
Upstream redefines what an existing field *means*. Fork code that reads
|
||||
Upstream redefines what an existing field _means_. Fork code that reads
|
||||
it is untouched by the diff and keeps compiling, while now saying
|
||||
something false.
|
||||
|
||||
|
|
@ -44,7 +44,7 @@ something false.
|
|||
### C. Misplaced conflict boundary
|
||||
|
||||
Git anchors a conflict on whatever lines happen to match. When both sides
|
||||
rewrote the same region, an *incidental* shared line inside it can become
|
||||
rewrote the same region, an _incidental_ shared line inside it can become
|
||||
the anchor — and everything past that line lands **outside** the markers,
|
||||
where it reads as cleanly merged.
|
||||
|
||||
|
|
@ -57,7 +57,7 @@ is upstream's — the fork's version is shadowed without a single warning.
|
|||
> Ours (220 lines: multipart HTML mail, the QR-card attachment, the
|
||||
> Date/Message-ID headers that keep SpamAssassin quiet, npub DM support)
|
||||
> appeared between the markers; upstream's showed as 4 lines. But both
|
||||
> sides define the *same nine functions*, and git had anchored on a
|
||||
> sides define the _same nine functions_, and git had anchored on a
|
||||
> shared `_send_nostr_ticket_notification` line — so upstream's entire
|
||||
> parallel stack sat below `>>>>>>>`, looking merged. Taking "ours" and
|
||||
> moving on would have left upstream's definitions last in the file and
|
||||
|
|
@ -102,7 +102,7 @@ histories.
|
|||
|
||||
## The procedure
|
||||
|
||||
Run this *after* the merge resolves and *before* the release.
|
||||
Run this _after_ the merge resolves and _before_ the release.
|
||||
|
||||
### 1. Enumerate what upstream introduced
|
||||
|
||||
|
|
@ -117,7 +117,7 @@ git show <tag>:models.py | sed -n '/^def sync_event_ticket_waves/,/return event/
|
|||
Split the result into **new symbols** (mode A candidates) and
|
||||
**redefined fields** (mode B candidates).
|
||||
|
||||
### 2. For each new symbol upstream *calls*, find the fork-only siblings
|
||||
### 2. For each new symbol upstream _calls_, find the fork-only siblings
|
||||
|
||||
Ask what category of place the call belongs to — "every function that
|
||||
returns an `Event` from the DB", "every path that prices a ticket" — then
|
||||
|
|
@ -147,7 +147,7 @@ grep -n "price_per_ticket\|amount_tickets" $(comm -23 ...)
|
|||
Both examples above were confirmed by reading the code path end to end,
|
||||
not inferred from the diff. A wrong theory costs more than the check:
|
||||
during this rebase an inference that `amount_tickets` "goes stale on
|
||||
sale" was wrong — `sync_event_ticket_waves` also runs on *reads*, which
|
||||
sale" was wrong — `sync_event_ticket_waves` also runs on _reads_, which
|
||||
only the call-site list showed.
|
||||
|
||||
### 5. Write the reason at the site
|
||||
|
|
|
|||
|
|
@ -4,17 +4,18 @@ 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 |
|
||||
| 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 |
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue