feat: issue free tickets without minting an invoice #31

Merged
padreug merged 2 commits from feat/free-tickets into main 2026-06-20 09:51:18 +00:00
Owner

Problem

Free events (price_per_ticket == 0) tried to mint a 0-amount Lightning invoice via create_payment_request. That invoice can't settle, and the invoice listener (on_invoice_paidset_ticket_paid) would never fire for it, so the ticket never became scannable. The webapp then hit its no-paymentRequest guard and threw a misleading "uses fiat checkout" error.

(Free ≠ donation — you can't pay 0 to a Lightning invoice; pay-what-you-want is tracked separately as #30.)

Change

api_ticket_create now short-circuits when the final charge is 0 — a free event or a 100%-off promo, computed after promo + quantity — before any invoice / fiat-provider logic. _issue_free_tickets creates the N rows and runs each through the existing set_ticket_paid: the exact path on_invoice_paid drives for a settled payment (flip paid, bump sold/amount_tickets under the per-event lock, republish the NIP-52 calendar event), plus the ticket notification. Only the payment is removed.

The response carries a new TicketPaymentRequest.paid = True with payment_request = None, so the client skips the QR / payment-poll and goes straight to the ticket QRs.

  • No invoice ⇒ sats_paid = 0, so free tickets are naturally skipped by refund_tickets (no accidental 0-sat LNURL refund).
  • All rows in a batch share one synthetic payment_hash — the join key the poll / WebSocket / My-Tickets lookups use — mirroring the paid multi-ticket path.
  • Capacity check (sold-out / remaining) already runs above the short-circuit, so free events respect amount_tickets.

Companion / follow-ups

  • Webapp success-branch PR (handle paid with no paymentRequest instead of throwing) — separate PR on aiolabs/webappdev.
  • Follow-ups filed: self-service forfeit #28, abuse/identity limits #29, donation tickets #30.

Testing note

No API/DB test harness is wired in this repo's tests/ (pure-unit only), and the free path delegates to the already-exercised set_ticket_paid. Needs the standard dev-LNbits smoke before tagging (FakeWallet): buy a free ticket → ticket returns paid, no invoice, scannable at the door; counters decrement + NIP-52 republishes; a 100%-off promo on a paid event takes the same path.

Includes the config.json bump to 1.6.1-aio.7 (separate commit).

🤖 Generated with Claude Code

## Problem Free events (`price_per_ticket == 0`) tried to mint a **0-amount Lightning invoice** via `create_payment_request`. That invoice can't settle, and the invoice listener (`on_invoice_paid` → `set_ticket_paid`) would never fire for it, so the ticket never became scannable. The webapp then hit its no-`paymentRequest` guard and threw a misleading "uses fiat checkout" error. (Free ≠ donation — you can't pay 0 to a Lightning invoice; pay-what-you-want is tracked separately as #30.) ## Change `api_ticket_create` now **short-circuits when the final charge is 0** — a free event *or* a 100%-off promo, computed after promo + quantity — **before** any invoice / fiat-provider logic. `_issue_free_tickets` creates the N rows and runs each through the **existing** `set_ticket_paid`: the exact path `on_invoice_paid` drives for a settled payment (flip `paid`, bump `sold`/`amount_tickets` under the per-event lock, republish the NIP-52 calendar event), plus the ticket notification. Only the payment is removed. The response carries a new `TicketPaymentRequest.paid = True` with `payment_request = None`, so the client skips the QR / payment-poll and goes straight to the ticket QRs. - No invoice ⇒ `sats_paid = 0`, so free tickets are **naturally skipped by `refund_tickets`** (no accidental 0-sat LNURL refund). - All rows in a batch share one synthetic `payment_hash` — the join key the poll / WebSocket / My-Tickets lookups use — mirroring the paid multi-ticket path. - Capacity check (sold-out / remaining) already runs above the short-circuit, so free events respect `amount_tickets`. ## Companion / follow-ups - Webapp success-branch PR (handle `paid` with no `paymentRequest` instead of throwing) — separate PR on `aiolabs/webapp` → `dev`. - Follow-ups filed: self-service forfeit #28, abuse/identity limits #29, donation tickets #30. ## Testing note No API/DB test harness is wired in this repo's `tests/` (pure-unit only), and the free path delegates to the already-exercised `set_ticket_paid`. Needs the standard **dev-LNbits smoke before tagging** (FakeWallet): buy a free ticket → ticket returns `paid`, no invoice, scannable at the door; counters decrement + NIP-52 republishes; a 100%-off promo on a paid event takes the same path. Includes the `config.json` bump to `1.6.1-aio.7` (separate commit). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Free events (price_per_ticket == 0) tried to mint a 0-amount Lightning
invoice via create_payment_request — an invoice that can't settle, and
which the invoice listener would never mark paid, so the ticket never
became scannable.

api_ticket_create now short-circuits when the final charge is 0 (a free
event or a 100%-off promo, computed after promo + quantity) before any
invoice / fiat-provider logic: _issue_free_tickets creates the N rows and
runs each through the existing set_ticket_paid — the same path
on_invoice_paid drives for a settled payment (flip paid, bump
sold/available under the per-event lock, republish the NIP-52 event) —
plus the ticket notification. The response carries a new
TicketPaymentRequest.paid=True with no payment_request so the client
skips the QR / payment-poll and goes straight to the ticket QRs.

No invoice means sats_paid=0, so free tickets are naturally skipped by
refund_tickets. All rows in a batch share one synthetic payment_hash —
the join key the poll / WebSocket / My-Tickets lookups use — mirroring
the paid multi-ticket path.

Self-service forfeit (#28), abuse/identity limits (#29) and
pay-what-you-want/donation tickets (#30) are tracked as follow-ups.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
chore: bump config.json version to 1.6.1-aio.7
Some checks failed
lint.yml / chore: bump config.json version to 1.6.1-aio.7 (pull_request) Failing after 0s
2093e63020
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
padreug deleted branch feat/free-tickets 2026-06-20 09:51:18 +00:00
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!31
No description provided.