Commit graph

1 commit

Author SHA1 Message Date
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