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
This commit is contained in:
parent
4c3b1bca31
commit
8602bd71e3
7 changed files with 618 additions and 19 deletions
17
services.py
17
services.py
|
|
@ -28,6 +28,7 @@ from .crud import (
|
|||
)
|
||||
from .models import Event, NotificationDeliveryResult, Ticket, TicketResendResult
|
||||
from .nostr_hooks import publish_or_delete_nostr_event
|
||||
from .promo import promo_usage
|
||||
from .qr import (
|
||||
format_event_when,
|
||||
image_png_bytes,
|
||||
|
|
@ -82,6 +83,22 @@ async def set_ticket_paid(ticket: Ticket) -> Ticket:
|
|||
return ticket
|
||||
|
||||
|
||||
async def event_promo_usage(event_id: str) -> dict[str, int]:
|
||||
"""Paid redemptions per promo code for one event (see promo.promo_usage)."""
|
||||
return promo_usage(await get_event_tickets(event_id))
|
||||
|
||||
|
||||
async def hydrate_promo_usage(event: Event) -> Event:
|
||||
"""Fill `used_count` on each of the event's promo codes. No query when
|
||||
the event has no codes, so listing stays cheap."""
|
||||
if not event.extra.promo_codes:
|
||||
return event
|
||||
usage = await event_promo_usage(event.id)
|
||||
for promo in event.extra.promo_codes:
|
||||
promo.used_count = usage.get(promo.code, 0)
|
||||
return event
|
||||
|
||||
|
||||
def send_ticket_notification_in_background(ticket: Ticket) -> None:
|
||||
create_task(_send_ticket_notification(ticket))
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue