Rebase onto upstream v1.6.8 #63

Merged
padreug merged 10 commits from rebase/upstream-v1.6.8 into main 2026-09-28 22:02:55 +00:00
Owner

Merges upstream v1.6.8 into the fork, then the two pieces of work that merge made necessary. Three commits, meant to be read in order.

Closes #33. Refs #41, #61.

1. ae5affa — the merge

Brings in ticket waves (per-wave price/currency/stock/fiat), the paginated ticket endpoint, the organiser ticket-image template, and upstream's SatsPay on-chain surface.

Resolutions that were not mechanical:

  • set_ticket_paid debits the wave named on the ticket, keeping upstream's > 0 guards — ours decremented unconditionally and could go negative. The purchase and free-ticket paths now stamp ticket_wave_id/ticket_wave_title; without them every sale would have come off the primary wave.
  • Pricing moved onto the selected wave. basket_totals takes it as a required argument and the promo-validate endpoint resolves the same wave through a shared _resolve_ticket_wave, so the quote and the charge cannot disagree. event.price_per_ticket is a roll-up of the primary wave since sync_event_ticket_waves, so pricing off the event charged the first wave's price to buyers who picked a later one.
  • npub support kept in the purchase endpoint and the notification dispatcher. Upstream replaced both with a hard "Only NIP-05 Nostr identifiers are supported", which is false here — _send_nostr_ticket_notification sends bare pubkeys via send_nostr_dm. The purchase-side rejection had merged in outside any conflict marker.
  • _ticket_image_url existed on both sides as two unrelated features. Ours (always-attached rendered QR card) became _ticket_card_url; upstream's (organiser template, opt-in per wave) kept the name. /qr/{ticket_id} was likewise defined on both sides on the same route — merged into one handler rather than registered twice, where the second copy would have been unreachable.
  • models._parse_date now accepts a full ISO datetime. Upstream's date-only strptime raised ValueError on any event whose closing_date carries a time, which create_event produces by defaulting it from event_end_date. That would have 500'd the purchase path, the public event gate and the promo preview. Reproduced before fixing.
  • Event create/update stayed ours — upstream's combined endpoint would have replaced the approval workflow and the allowlist that keeps status out of the request body.

2. d90a0f8 — drop the SatsPay/watchonly surface

The parity note on #41, as its own commit so the merge above stays a faithful diff of what upstream shipped. Removes the five SatsPay/watchonly HTTP clients, GET /events/onchain/status, the SatsPay charge branch, POST /tickets/{id}/satspay-webhook, PUT /tickets/{hash}/onchain-confirm, the organiser's on-chain panel, and the onchain_{enabled,wallet_id,zeroconf,fasttrack} / satspay_charge_* fields.

On-chain goes through the fork's native LndRestWallet once core can mint purpose-bound receiving addresses (aiolabs/lnbits#53), so onchain is no longer an accepted payment_method — there is no rail to fulfil it and accepting it could only fail later. TicketExtra.onchain/onchain_address and TicketPaymentRequest.onchain_amount_sat stay as vocabulary for #41, in upstream's names so it needs no migration.

3. 15c2276 — publish the active wave

The publisher read the event-level roll-up, so once an organiser created a second wave the public card advertised the early-bird price after early bird closed and counted stock in waves that had not opened.

  • Advertises the cheapest open wave — several can be open at once and a publisher has no one to ask. Deviation recorded in docs/upstream-candidates.md; rationale and rejected alternatives in #61.
  • tickets_available is 0 and never omitted when nothing is open (omission meant "unlimited", which #34/#62 removed).
  • tickets_payment_methods is scoped to the advertised wave too — it was derived from event.allow_fiat and could offer a fiat rail the purchase endpoint would refuse while tickets_allow_fiat was absent.
  • Wave boundaries are time-driven and every republish we have is sale-driven, so a publish now records nostr_published_wave_id (m004) and the reconciliation sweep flags rows whose advertised wave has moved — reusing the existing retry path instead of adding a scheduler. NULL means "never published" and is left alone, so upgrading does not republish the whole table on first boot.

Notes for review

docs/rebase-playbook.md is new, and gained two failure modes found during this merge: C, misplaced conflict boundary (git anchors on an incidental shared line, so an entire parallel implementation lands outside the markers and reads as merged — ruff does not flag duplicate defs, and mypy, which does, cannot run while any file in the package still has markers) and D, collided names. The uniq -d check it prescribes is what caught the duplicate /qr handler and a duplicate paymentMethodOptions in display.js.

Still open and deliberately not in scope: #61 stays open to hold the richer tier-event representation; #41 stays blocked on aiolabs/lnbits#53; #34's remaining half — "capacity always required" needs restating as a per-wave rule now that amount_tickets is a roll-up.

Verification

114 tests pass (17 new). ruff, black and prettier clean. 35 routes register with no duplicates and literal routes still precede parameterised ones. mypy's error set is byte-identical to main's baseline, diffed against a clean checkout rather than counted.

Not yet exercised against a running instance — worth a pass on aio-demo before v1.6.8-aio.1 is tagged, particularly a purchase on a second wave and a wave boundary crossing.

Merges upstream v1.6.8 into the fork, then the two pieces of work that merge made necessary. Three commits, meant to be read in order. Closes #33. Refs #41, #61. ## 1. `ae5affa` — the merge Brings in ticket waves (per-wave price/currency/stock/fiat), the paginated ticket endpoint, the organiser ticket-image template, and upstream's SatsPay on-chain surface. Resolutions that were not mechanical: - **`set_ticket_paid` debits the wave named on the ticket**, keeping upstream's `> 0` guards — ours decremented unconditionally and could go negative. The purchase and free-ticket paths now stamp `ticket_wave_id`/`ticket_wave_title`; without them every sale would have come off the primary wave. - **Pricing moved onto the selected wave.** `basket_totals` takes it as a required argument and the promo-validate endpoint resolves the same wave through a shared `_resolve_ticket_wave`, so the quote and the charge cannot disagree. `event.price_per_ticket` is a roll-up of the *primary* wave since `sync_event_ticket_waves`, so pricing off the event charged the first wave's price to buyers who picked a later one. - **npub support kept** in the purchase endpoint and the notification dispatcher. Upstream replaced both with a hard "Only NIP-05 Nostr identifiers are supported", which is false here — `_send_nostr_ticket_notification` sends bare pubkeys via `send_nostr_dm`. The purchase-side rejection had merged in *outside any conflict marker*. - **`_ticket_image_url` existed on both sides as two unrelated features.** Ours (always-attached rendered QR card) became `_ticket_card_url`; upstream's (organiser template, opt-in per wave) kept the name. `/qr/{ticket_id}` was likewise defined on both sides on the same route — merged into one handler rather than registered twice, where the second copy would have been unreachable. - **`models._parse_date` now accepts a full ISO datetime.** Upstream's date-only `strptime` raised `ValueError` on any event whose `closing_date` carries a time, which `create_event` produces by defaulting it from `event_end_date`. That would have 500'd the purchase path, the public event gate and the promo preview. Reproduced before fixing. - Event create/update stayed ours — upstream's combined endpoint would have replaced the approval workflow and the allowlist that keeps `status` out of the request body. ## 2. `d90a0f8` — drop the SatsPay/watchonly surface The parity note on #41, as its own commit so the merge above stays a faithful diff of what upstream shipped. Removes the five SatsPay/watchonly HTTP clients, `GET /events/onchain/status`, the SatsPay charge branch, `POST /tickets/{id}/satspay-webhook`, `PUT /tickets/{hash}/onchain-confirm`, the organiser's on-chain panel, and the `onchain_{enabled,wallet_id,zeroconf,fasttrack}` / `satspay_charge_*` fields. On-chain goes through the fork's native `LndRestWallet` once core can mint purpose-bound receiving addresses (aiolabs/lnbits#53), so `onchain` is no longer an accepted `payment_method` — there is no rail to fulfil it and accepting it could only fail later. `TicketExtra.onchain`/`onchain_address` and `TicketPaymentRequest.onchain_amount_sat` stay as vocabulary for #41, in upstream's names so it needs no migration. ## 3. `15c2276` — publish the active wave The publisher read the event-level roll-up, so once an organiser created a second wave the public card advertised the early-bird price after early bird closed and counted stock in waves that had not opened. - Advertises the **cheapest open wave** — several can be open at once and a publisher has no one to ask. Deviation recorded in `docs/upstream-candidates.md`; rationale and rejected alternatives in #61. - `tickets_available` is `0` and never omitted when nothing is open (omission meant "unlimited", which #34/#62 removed). - `tickets_payment_methods` is scoped to the advertised wave too — it was derived from `event.allow_fiat` and could offer a fiat rail the purchase endpoint would refuse while `tickets_allow_fiat` was absent. - Wave boundaries are time-driven and every republish we have is sale-driven, so a publish now records `nostr_published_wave_id` (m004) and the reconciliation sweep flags rows whose advertised wave has moved — reusing the existing retry path instead of adding a scheduler. NULL means "never published" and is left alone, so upgrading does not republish the whole table on first boot. ## Notes for review `docs/rebase-playbook.md` is new, and gained two failure modes found during this merge: **C, misplaced conflict boundary** (git anchors on an incidental shared line, so an entire parallel implementation lands outside the markers and reads as merged — `ruff` does not flag duplicate `def`s, and `mypy`, which does, cannot run while any file in the package still has markers) and **D, collided names**. The `uniq -d` check it prescribes is what caught the duplicate `/qr` handler and a duplicate `paymentMethodOptions` in `display.js`. Still open and deliberately not in scope: #61 stays open to hold the richer tier-event representation; #41 stays blocked on aiolabs/lnbits#53; #34's remaining half — "capacity always required" needs restating as a per-wave rule now that `amount_tickets` is a roll-up. ## Verification 114 tests pass (17 new). ruff, black and prettier clean. 35 routes register with no duplicates and literal routes still precede parameterised ones. mypy's error set is byte-identical to `main`'s baseline, diffed against a clean checkout rather than counted. Not yet exercised against a running instance — worth a pass on aio-demo before `v1.6.8-aio.1` is tagged, particularly a purchase on a second wave and a wave boundary crossing.
* email input fixes

* promo code ui fix

* ticket waves

* works, but horrible slop

* slightly cleaner

* filter most recent

* make

* pyright
* feat: onchain payments
* fix: onchain listeners
* use satspay
* safeguard satspay
* add fasttrack and 0conf
- query from localhost and port, important if behind a proxy without
loopback
Brings in ticket waves (per-wave price/currency/stock/fiat), the paginated
ticket endpoint, the organiser ticket-image template, and the SatsPay
on-chain surface. Refs #33.

Resolutions that were not mechanical, and why:

- set_ticket_paid debits the wave named on the ticket, keeping upstream's
  `> 0` guards; ours decremented unconditionally and could go negative.
  The purchase and free-ticket paths now stamp ticket_wave_id /
  ticket_wave_title, without which every sale would debit the primary wave.

- Pricing moved onto the selected wave (basket_totals takes it as a required
  argument, the promo-validate endpoint resolves the same wave through the
  shared _resolve_ticket_wave). event.price_per_ticket is a roll-up of the
  PRIMARY wave since sync_event_ticket_waves, so pricing off the event
  quoted and charged the first wave's price to buyers who picked a later
  one. Regression test added.

- Kept npub support in two places upstream removed it: the purchase
  endpoint's normalize_public_key path and the notification dispatcher.
  Upstream's replacement rejects with "Only NIP-05 Nostr identifiers are
  supported", which is false for this fork. The purchase-side rejection had
  merged in outside any conflict marker.

- _ticket_image_url existed on both sides as two unrelated features. Ours
  (always-attached rendered QR card) is now _ticket_card_url; upstream's
  (organiser template, opt-in per wave) keeps the name. Both are wired into
  the mail, and the /qr/{ticket_id} endpoint — also duplicated on both
  sides, on the same route — is merged into one handler rather than
  registered twice, where the second copy would have been unreachable.

- models._parse_date now accepts a full ISO datetime. Upstream's date-only
  strptime raised ValueError on any event whose closing_date carries a time,
  which create_event produces by defaulting it from event_end_date — it
  would have 500'd the purchase path, the public event gate and the promo
  preview. Reproduced before fixing.

- Dropped upstream's inline make_qr_png (we import a superset from .qr) and
  its duplicate paymentMethodOptions in display.js, which re-derived payment
  options from per-method booleans and offered an on-chain option the
  backend rejects; the submit gate now matches the template's condition.

- Restored imports the merge silently dropped with upstream's npub removal
  (normalize_public_key, normalize_private_key, DEFAULT_NOSTR_RELAYS).

Event create/update stays ours: upstream's combined endpoint would have
replaced the approval workflow and the explicit field allowlist that keeps
`status` out of the request body.

mypy error set is unchanged from HEAD; ruff, black and the 97 tests pass.
v1.6.8 sells tickets on-chain through SatsPay charges — a per-purchase
watchonly address, a hosted charge page, and a webhook back into the
extension. We are not adopting that: on-chain goes through the fork's
native LndRestWallet support once core can mint purpose-bound receiving
addresses (aiolabs/lnbits#53). This is the parity note on #41, applied
as its own commit so the merge it follows stays a faithful diff of what
upstream shipped.

Removed:
- services: the five SatsPay/watchonly HTTP clients (and with them the
  only uses of httpx and typing.Any in that module)
- views_api: _get_watchonly_status, GET /events/onchain/status, the
  SatsPay charge branch of api_ticket_create, POST
  /tickets/{id}/satspay-webhook, PUT /tickets/{hash}/onchain-confirm
- models: EventExtra.onchain_{enabled,wallet_id,zeroconf,fasttrack},
  TicketExtra.satspay_charge_id, TicketPaymentRequest.satspay_charge_url
- frontend: the organiser's "Onchain payments" panel and its watchonly
  wallet picker, the per-ticket confirm-onchain button, the SatsPay
  charge-page redirect, and the wallet-status fetch behind them

`onchain` is no longer an accepted payment_method — there is no rail to
fulfil it until #41 lands, so accepting it could only fail later.

Kept as vocabulary for #41, in upstream's field names so it needs no
migration: TicketExtra.onchain / onchain_address and
TicketPaymentRequest.onchain_amount_sat.

35 routes register with no duplicates; ruff, black, prettier clean; 97
tests pass; mypy error set still identical to HEAD's baseline.
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
15c2276e57
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.
padreug deleted branch rebase/upstream-v1.6.8 2026-09-28 22:02:55 +00:00
padreug referenced this pull request from a commit 2026-09-28 22:09:49 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
aiolabs/events!63
No description provided.