feat: tickets-by-user endpoint + Nostr-driven inventory sync #15

Merged
padreug merged 3 commits from tickets-nostr-sync into main 2026-05-23 18:50:53 +00:00
Owner

Summary

Two pieces the webapp needs to restore the ticket purchase + "tickets
visible in the events area" loop after the activities module was
extracted into a standalone Nostr-driven PWA.

1. GET /api/v1/tickets/user/{user_id} (commit 814581f)

The webapp's MyTickets view + the owned-ticket badges across the
activities feed both rely on this endpoint to enumerate a buyer's
tickets across events. The CRUD function get_tickets_by_user_id
already existed; just expose it.

  • Auth: Bearer access token (the same shape the webapp already sends
    to other LNbits endpoints).
  • The path user_id must match the token-bound user.id — users
    can only enumerate their own tickets, not anyone else's by ID
    guessing.
  • Returns full Ticket rows (not PublicTicket) because the owner
    needs the payment_hash (for the QR) + the extra envelope (for
    refund / promo / notification state) in My Tickets.

2. Ticket counts in NIP-52 tags + republish on sale (commit edf1493)

Inventory sync over Nostr, mirroring how nostrmarket republishes
kind 30018 product events when stock changes. Connected webapp /
other-client subscriptions pick up the new state via their existing
relay subscription — no REST polling needed.

build_nip52_event grows four AIO custom tags on every published
kind 31922/31923 event:

Tag Value When present
tickets_available string integer When amount_tickets > 0. Omitted when capacity is unlimited (the schema's 0 sentinel) so clients can tell unlimited from sold-out.
tickets_sold string integer Always. Zero is published explicitly so clients can derive original_capacity = available + sold and render progress bars.
tickets_price string number The price_per_ticket. 0 means free.
tickets_currency string The currency field.

Tags are AIO additions outside the NIP-52 spec; spec-compliant
clients MUST ignore unknown tags so this stays backwards-compatible.

set_ticket_paid now calls publish_or_delete_nostr_event(event)
after the counter update so the new state lands on relays. The
whole sequence (counter update + republish) is wrapped in a per-
event-id asyncio.Lock to address the existing # todo: lock and
to ensure two paid invoices for the same event can't reorder the
published state.

Failures inside the Nostr publish are logged + swallowed by the
existing wrapper, so a relay outage can never break the payment
flow.

Smoke

Static smoke against the local regtest stack — tag construction
verified for the three cases:

Case amount_tickets Result
Limited 10 All four tags emitted (tickets_available=10, tickets_sold=3, tickets_price=100.0, tickets_currency=sats).
Unlimited 0 tickets_available correctly omitted; the other three present.
Fiat (time-based) 50 All four tags emitted on kind 31923.

Endpoint smoke: GET /events/api/v1/tickets/user/{user_id} rejects
unauthenticated requests with 401 (Missing user ID or access token.).

The companion webapp PR (forthcoming) consumes these tags. Before
merging:

  1. Run the local regtest stack with this branch checked out.
  2. Create a ticketed event via the LNbits admin UI.
  3. Inspect the published Nostr event in the nostrclient relay tab
    — confirm the four new tags are present.
  4. Purchase one ticket via the FakeWallet auto-pay path. Confirm
    tickets_sold increments and the event is republished.
  5. Two-browser concurrency: open the same event in two tabs, buy
    simultaneously, confirm sold increments by 2 (lock prevents
    the double-publish race) and the final published event reflects
    the latest counters.

Test plan

  • Lightning purchase → tickets_sold/tickets_available update
    in the republished event.
  • Fiat purchase (Stripe stub) → same.
  • Unlimited-capacity event → tickets_available tag stays
    absent across multiple sales.
  • My Tickets endpoint returns the buyer's tickets via Bearer
    auth and refuses cross-user enumeration with 403.

🤖 Generated with Claude Code

## Summary Two pieces the webapp needs to restore the ticket purchase + "tickets visible in the events area" loop after the activities module was extracted into a standalone Nostr-driven PWA. ### 1. GET /api/v1/tickets/user/{user_id} (commit `814581f`) The webapp's MyTickets view + the owned-ticket badges across the activities feed both rely on this endpoint to enumerate a buyer's tickets across events. The CRUD function `get_tickets_by_user_id` already existed; just expose it. - Auth: Bearer access token (the same shape the webapp already sends to other LNbits endpoints). - The path `user_id` must match the token-bound `user.id` — users can only enumerate their own tickets, not anyone else's by ID guessing. - Returns full `Ticket` rows (not `PublicTicket`) because the owner needs the payment_hash (for the QR) + the `extra` envelope (for refund / promo / notification state) in My Tickets. ### 2. Ticket counts in NIP-52 tags + republish on sale (commit `edf1493`) Inventory sync over Nostr, mirroring how `nostrmarket` republishes kind 30018 product events when stock changes. Connected webapp / other-client subscriptions pick up the new state via their existing relay subscription — no REST polling needed. `build_nip52_event` grows four AIO custom tags on every published kind 31922/31923 event: | Tag | Value | When present | |---|---|---| | `tickets_available` | string integer | When `amount_tickets > 0`. Omitted when capacity is unlimited (the schema's `0` sentinel) so clients can tell unlimited from sold-out. | | `tickets_sold` | string integer | Always. Zero is published explicitly so clients can derive `original_capacity = available + sold` and render progress bars. | | `tickets_price` | string number | The `price_per_ticket`. `0` means free. | | `tickets_currency` | string | The `currency` field. | Tags are AIO additions outside the NIP-52 spec; spec-compliant clients MUST ignore unknown tags so this stays backwards-compatible. `set_ticket_paid` now calls `publish_or_delete_nostr_event(event)` after the counter update so the new state lands on relays. The whole sequence (counter update + republish) is wrapped in a per- event-id `asyncio.Lock` to address the existing `# todo: lock` and to ensure two paid invoices for the same event can't reorder the published state. Failures inside the Nostr publish are logged + swallowed by the existing wrapper, so a relay outage can never break the payment flow. ## Smoke Static smoke against the local regtest stack — tag construction verified for the three cases: | Case | `amount_tickets` | Result | |---|---|---| | Limited | 10 | All four tags emitted (`tickets_available=10`, `tickets_sold=3`, `tickets_price=100.0`, `tickets_currency=sats`). | | Unlimited | 0 | `tickets_available` correctly omitted; the other three present. | | Fiat (time-based) | 50 | All four tags emitted on kind 31923. | Endpoint smoke: `GET /events/api/v1/tickets/user/{user_id}` rejects unauthenticated requests with 401 (`Missing user ID or access token.`). ## End-to-end smoke (recommended before merge) The companion webapp PR (forthcoming) consumes these tags. Before merging: 1. Run the local regtest stack with this branch checked out. 2. Create a ticketed event via the LNbits admin UI. 3. Inspect the published Nostr event in the nostrclient relay tab — confirm the four new tags are present. 4. Purchase one ticket via the FakeWallet auto-pay path. Confirm `tickets_sold` increments and the event is republished. 5. Two-browser concurrency: open the same event in two tabs, buy simultaneously, confirm `sold` increments by 2 (lock prevents the double-publish race) and the final published event reflects the latest counters. ## Test plan - [ ] Lightning purchase → `tickets_sold`/`tickets_available` update in the republished event. - [ ] Fiat purchase (Stripe stub) → same. - [ ] Unlimited-capacity event → `tickets_available` tag stays absent across multiple sales. - [ ] My Tickets endpoint returns the buyer's tickets via Bearer auth and refuses cross-user enumeration with 403. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The webapp My Tickets view + the owned-ticket badges in the
activities feed both rely on this endpoint to enumerate a buyer's
tickets across all events. The CRUD function already existed
(`get_tickets_by_user_id`); just expose it.

Auth: Bearer access token (the same shape the webapp already sends
to other LNbits endpoints). The path param must match the token-
bound user.id — users can only enumerate their own tickets, not
anyone else's by ID-guessing.

Returns full `Ticket` rows rather than `PublicTicket` because the
owner needs the payment_hash (for the QR) + the `extra` envelope
(for refund / promo / notification state) in My Tickets.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat: publish ticket counts in NIP-52 tags + republish on sale
Some checks failed
lint.yml / feat: publish ticket counts in NIP-52 tags + republish on sale (pull_request) Failing after 0s
edf1493e0c
Inventory sync over Nostr, mirroring how nostrmarket republishes
kind 30018 product events when stock changes. Connected webapp /
other-client subscriptions pick up the new state via their existing
relay subscription — no REST polling needed.

build_nip52_event grows four AIO custom tags on every published
kind 31922/31923 event:
- tickets_available — current remaining (omitted when amount_tickets
  is 0, the schema's "unlimited" sentinel, so clients can tell the
  difference between unlimited and sold-out)
- tickets_sold — running count, always emitted (clients derive
  original_capacity = available + sold for progress bars)
- tickets_price — price_per_ticket (0 means free)
- tickets_currency — the currency string

Tags are AIO additions outside the NIP-52 spec; spec-compliant
clients MUST ignore unknown tags so this stays backwards-compatible.

set_ticket_paid calls publish_or_delete_nostr_event after the
counter update so the new state lands on relays. The whole sequence
(counter update + republish) is wrapped in a per-event-id asyncio
lock to address the existing # todo: lock and to ensure two paid
invoices for the same event can't reorder the published state.

Failures inside the Nostr publish are logged + swallowed by the
existing wrapper, so a relay outage can never break the payment
flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
feat: also publish allow_fiat + fiat_currency in NIP-52 tags
Some checks failed
lint.yml / feat: also publish allow_fiat + fiat_currency in NIP-52 tags (pull_request) Failing after 0s
lint.yml / feat: also publish allow_fiat + fiat_currency in NIP-52 tags (push) Failing after 0s
b0d089d3c9
The buyer-side webapp Purchase button needs allow_fiat to know
whether to surface the fiat method, and fiat_currency for the
conversion-preview label. Without these in the published Nostr
event, the buyer would either have to REST-fetch the LNbits event
again (defeats the inventory-sync goal) or guess.

Same backwards-compat reasoning as the four counter tags — tags
are AIO additions outside the NIP-52 spec; unknown tags are
ignored by spec-compliant clients.

- tickets_allow_fiat: "true" when the organizer enabled the fiat
  toggle. Omitted otherwise so the on-the-wire payload stays
  small for the common Lightning-only case.
- tickets_fiat_currency: only emitted when allow_fiat is on
  (otherwise it'd be ambiguous what the value represents).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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!15
No description provided.