feat: republish endpoints + polling + multi-ticket via N-rows model #16
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "tickets-nostr-sync"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Follow-up to PR #15 (which already merged the GET /tickets/user
endpoint + NIP-52 ticket tags + republish-on-sale). This PR carries
the seven commits that landed on
tickets-nostr-syncafter thatmerge, rebased to drop a superseded multi-ticket intermediate
(
5afc888— extra.quantity approach) so the history reads cleanly.Commits (oldest → newest)
05593c9POST /events/republish-alladmin endpoint. Loops approved events throughpublish_or_delete_nostr_event— useful for catalog-bump migrations when the publisher tag set changes.fa2a6e4ced6ca2POST /republish-minescoped viarequire_admin_key+ an organizer-visible "Republish mine" button on the New Event card. Lets an organizer trigger the migration for just their own events without instance-admin rights.902bafePOST /tickets/{event_id}/{payment_hash}polling endpoint. The webapp polls this after presenting the invoice; without it the post-payment poll loop 404'd indefinitely and the buyer never saw the success state, even thoughset_ticket_paidran.36568d3CreateTicket.user_idto the persisted ticket row. The endpoint accepteduser_idon the request body (its root_validator even requires it XOR name+email) but dropped it on the way tocrud.create_ticket, leaving every webapp-bought ticket withuser_id = NULLand breaking the GET /tickets/user endpoint.59068feid(short-hash uuid); the LNbits invoice payment_hash is shared via a newticket.payment_hashcolumn. Migrationm002_ticket_payment_hashadds the column + backfills existing rows fromid(legacyid == payment_hashinvariant). The door scanner (existing/register/{ticket_id}PUT) works unchanged — each attendee scans their own QR independently. CreateTicket growsquantity: Field(ge=1, le=10); the API loops to create N rows, the polling endpoint returns everyticket_idsso clients render N QRs;on_invoice_paiditerates all rows for a payment_hash andset_ticket_paidincrements counters by 1 per row, all under the existing per-event asyncio lock.7b761a1urlsafe_short_hashid — no payment-hash reuse for the first row, so single- and multi-ticket purchases produce uniform short ids on the wire (rather than one 64-hex id + N-1 short-hash siblings).Smoke
Locally:
m002runs cleanly on the regtest stack; existing rows getpayment_hash = idbackfilled. New 3-ticket purchase creates 3 rows with sharedpayment_hash, distinct short-hash ids,extra.quantitynot used (gone). Counters increment by 3.POST /tickets/{event_id}/{payment_hash}returns{paid, ticket_id, ticket_ids: [...]}once the invoice settles.POST /events/republish-allre-emits every approved event;POST /events/republish-minescopes to the caller's wallets.Test plan
idis a short hash,payment_hashequals the invoice hash, polling endpoint returnsticket_ids=[id].payment_hash, three distinct short-hash ids. Each PUT/register/{id}registers exactly that attendee.event.amount_tickets - event.soldreturns 400 with the remaining-count detail string (not 410 sold-out)._alter_add_column_safehelper swallows duplicate-column errors).🤖 Generated with Claude Code
The webapp's useTicketPurchase polls this every 2s after firing Pay with Wallet (or after presenting the QR) to confirm payment before advancing to the ticket-QR success state. Without this endpoint the post-payment poll loop returns 404 indefinitely and the buyer never sees their ticket land — even though set_ticket_paid fired on the invoice listener and the row is correctly marked paid in the DB. Returns {paid: bool, ticket_id?: str}. A missing or cross-event ticket returns paid: false rather than 404 so the poll loop doesn't need to special-case the not-yet-created race. The WebSocket at /tickets/ws/{payment_hash} is more efficient for push notifications — this POST is the fallback for clients that can't open a relay-side socket. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>api_ticket_create accepted user_id in the CreateTicket request body (its root_validator even requires user_id XOR name+email), but dropped it on the way to crud.create_ticket — tickets ended up with user_id = NULL and the new GET /tickets/user/{id} endpoint returned an empty list for every webapp buyer. Pull data.user_id alongside name/email and forward it to create_ticket. Backfilling existing rows is left to the operator (deployment-specific data fix); fresh purchases starting from this commit are correctly attributed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Replaces the previous "one row, N seats via extra.quantity" model with proper one-row-per-attendee semantics. Each attendee gets a unique scannable id; the door PUT /register/{ticket_id} marks them registered independently — so a buyer can purchase 3 tickets, hand 2 QRs to friends arriving separately, and each attendee can enter on their own schedule. Schema (migrations_fork.py m002): - ticket.payment_hash: new TEXT column shared across all rows of a multi-ticket purchase. Backfilled `payment_hash = id` for pre-migration rows (id WAS the payment_hash by invariant). Wire: - TicketPaymentRequest grows `ticket_ids: list[str]` so the webapp gets every scannable id back in the create response. - POST /tickets/{event_id}/{payment_hash} polling endpoint now reports `ticket_ids` (every row) + keeps `ticket_id` for back-compat. - api_ticket_create loops quantity times; the first row reuses payment_hash as id (preserves legacy `id == payment_hash` invariant for single-ticket purchases), the rest get urlsafe_short_hash() uuids. Payment flow: - on_invoice_paid fetches all rows by payment_hash and marks each paid via set_ticket_paid, which now increments event.sold by 1 per row (was N per row via extra.quantity — simpler now). The per-event asyncio lock still serializes counter + republish so concurrent multi-ticket purchases for the same event don't reorder the published Nostr state. - Each paid row triggers its own send_ticket_notification_in_ background call — no-op for buyers without nostr_identifier / email, useful when the buyer set those on the row. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>