Commit graph

2 commits

Author SHA1 Message Date
ae5affa44f Merge upstream v1.6.8 into the aio fork
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.
2026-09-28 22:09:18 +02:00
8602bd71e3 feat(promo): enforce active + max_uses, validate endpoint, codes hidden from public
Promo handling was inherited from upstream unchanged and had four gaps
the webapp was about to put in front of buyers:

- `active` was decorative: purchase never read it, so a deactivated code
  kept discounting. Now rejected with "Promo code is not active."
- No redemption cap (#32). `PromoCode.max_uses` (None/0 = unlimited) with
  `used_count` DERIVED from paid tickets carrying the code in
  `extra.applied_promo_code` — each ticket of a multi-ticket purchase
  consumes one use (upstream v2 counts one per basket; documented).
  Paid-only counting so an abandoned Stripe session can't lock out the
  last uses for the 24 h unpaid-row lifetime; bounded overshoot under
  concurrency accepted.
- Every code was readable by anyone: `PublicEvent.extra` was the full
  `EventExtra` and `/events/public` returned the untrimmed `Event`
  (wallet id included). `EventExtraBase` / `PublicEventExtra` project
  them out; `/public` now goes through `PublicEvent`. Organizer and admin
  listings keep the full model, now hydrated with `used_count`.
- No preview: `POST /events/api/v1/promo/validate/{event_id}` (same URL as
  upstream v2; `quantity` replaces v2's `items` since this fork has no
  ticket types) returns v2-shaped `BasketTotals` + `currency`. Advisory:
  bad codes are simply absent from `discounts_applied`; purchase still
  hard-fails them with distinct details.

All pricing (validate, invoice, Stripe amount) goes through one pure
`basket_totals` with a single rounding rule (whole sats / 2 dp fiat), so
the preview equals the charge. Stripe metadata carries `promo_code`; the
organizer stats rows carry `applied_promo_code`.

`api_event_update` keeps stored codes when the request omits
`extra.promo_codes` (explicit `[]` still clears): now that public
records don't carry them, a client round-tripping one would otherwise
wipe the organizer's codes on every edit.

Closes #32

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ByAwHU4pRnyE58YocQvAas
2026-09-13 18:43:34 +02:00