feat: guest checkout, Stripe return flow, emailed QR tickets, per-event payment methods (v1.6.1-aio.8) #36

Merged
padreug merged 7 commits from feat/guest-checkout-email into main 2026-09-07 09:27:51 +00:00
Owner

Backend half of the guest-checkout work for the end-of-month production run (webapp half: aiolabs/webapp feat/events-guest-checkout; lnbits half: aiolabs/lnbits#66).

What changes

  1. Identity — CreateTicket accepts user_id together with email/name (the exclusion was our own dispatch convenience from dfabcb8, nothing needed it); crud.create_ticket stops blanking name/email for user_id rows. Until now no webapp buyer could ever receive a ticket email, logged in or not.
  2. Return to the calling app — new optional CreateTicket.frontend_url (app root). Its origin must be one of LNBITS_CORS_ALLOWED_ORIGINS, the LNbits base URL or LNBITS_CUSTOM_FRONTEND_URL, else 400 (fail loud; a silent fallback sends the buyer to the wrong app). Under that root the fiat path sets extra["checkout"] for lnbits' StripeCheckoutOptions: success_url=/events/{id}?checkout=success&tickets=<ids>, cancel_url=/events/{id}?checkout=cancelled, customer_email, an event-named line item and event_id/quantity/ticket_ids metadata. Ticket ids are minted before the invoice so the success URL can carry them. ticket_base_url uses the same root, so the emailed link opens the webapp when the webapp was the client; absent frontend_url = today's behaviour (LNbits host / Quasar pages).
  3. Emailed QR ticket — port of upstream v1.6.8's delivery layer, wave-free: multipart text + HTML with the QR embedded from the new anonymous GET /events/api/v1/qr/{ticket_id} (PNG at error-correction H, instance lnbits_qr_logo pasted in the centre). resend-email returns a TicketResendResult (per-channel attempted/sent/error). SMTP runs in a worker thread. Our nsec-DM Nostr path is kept.
  4. Per-event payment methods — extra.payment_methods (lightning, fiat; empty = legacy rule) with the field name upstream v2 (#64) uses; enforced at purchase, published as NIP-52 tag tickets_payment_methods, and exposed as checkboxes in the admin dialog. This is how Lightning is switched off for the card-only run without code.

No schema change (ticket.email exists; payment_methods lives in extra). config.json → 1.6.1-aio.8. README section + docs/upstream-candidates.md (running log of upstream-shaped features).

Verification

  • pytest (34 passed, incl. new test_ticket_models, test_frontend_root, test_ticket_qr, test_crud_ticket_email) run with the fork's lnbits on PYTHONPATH — the uv env resolves upstream lnbits, which lacks lnbits.core.signers.
  • black / ruff / prettier clean. mypy reports 15 pre-existing var-annotated errors in untouched crud.py/nostr_sync.py lines, none in this diff.
  • Local API smoke against bohm's native lnbits still pending (needs a restart to load the checkout) — will report on this PR.

Deploy

Merge → tag v1.6.1-aio.8 → sha256 → add a catalog entry in aiolabs/lnbits-extensions → upgrade on aio-demo. Needs aiolabs/lnbits#66 deployed for customer_email/cancel_url to take effect (without it those two keys are ignored by lnbits; everything else works).

🤖 Generated with Claude Code

https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo

Backend half of the guest-checkout work for the end-of-month production run (webapp half: aiolabs/webapp `feat/events-guest-checkout`; lnbits half: aiolabs/lnbits#66). ## What changes 1. **Identity** — `CreateTicket` accepts `user_id` together with `email`/`name` (the exclusion was our own dispatch convenience from dfabcb8, nothing needed it); `crud.create_ticket` stops blanking name/email for `user_id` rows. Until now **no webapp buyer could ever receive a ticket email**, logged in or not. 2. **Return to the calling app** — new optional `CreateTicket.frontend_url` (app root). Its origin must be one of `LNBITS_CORS_ALLOWED_ORIGINS`, the LNbits base URL or `LNBITS_CUSTOM_FRONTEND_URL`, else 400 (fail loud; a silent fallback sends the buyer to the wrong app). Under that root the fiat path sets `extra["checkout"]` for lnbits' `StripeCheckoutOptions`: `success_url=/events/{id}?checkout=success&tickets=<ids>`, `cancel_url=/events/{id}?checkout=cancelled`, `customer_email`, an event-named line item and `event_id/quantity/ticket_ids` metadata. Ticket ids are minted before the invoice so the success URL can carry them. `ticket_base_url` uses the same root, so the emailed link opens the webapp when the webapp was the client; absent `frontend_url` = today's behaviour (LNbits host / Quasar pages). 3. **Emailed QR ticket** — port of upstream v1.6.8's delivery layer, wave-free: multipart text + HTML with the QR embedded from the new anonymous `GET /events/api/v1/qr/{ticket_id}` (PNG at error-correction H, instance `lnbits_qr_logo` pasted in the centre). `resend-email` returns a `TicketResendResult` (per-channel attempted/sent/error). SMTP runs in a worker thread. Our nsec-DM Nostr path is kept. 4. **Per-event payment methods** — `extra.payment_methods` (`lightning`, `fiat`; empty = legacy rule) with the field name upstream v2 (#64) uses; enforced at purchase, published as NIP-52 tag `tickets_payment_methods`, and exposed as checkboxes in the admin dialog. This is how Lightning is switched off for the card-only run without code. No schema change (`ticket.email` exists; `payment_methods` lives in `extra`). `config.json` → `1.6.1-aio.8`. README section + `docs/upstream-candidates.md` (running log of upstream-shaped features). ## Verification - `pytest` (34 passed, incl. new `test_ticket_models`, `test_frontend_root`, `test_ticket_qr`, `test_crud_ticket_email`) run with the fork's lnbits on `PYTHONPATH` — the uv env resolves upstream lnbits, which lacks `lnbits.core.signers`. - black / ruff / prettier clean. mypy reports 15 pre-existing `var-annotated` errors in untouched `crud.py`/`nostr_sync.py` lines, none in this diff. - Local API smoke against bohm's native lnbits still pending (needs a restart to load the checkout) — will report on this PR. ## Deploy Merge → tag `v1.6.1-aio.8` → sha256 → **add** a catalog entry in aiolabs/lnbits-extensions → upgrade on aio-demo. Needs aiolabs/lnbits#66 deployed for `customer_email`/`cancel_url` to take effect (without it those two keys are ignored by lnbits; everything else works). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
`CreateTicket` no longer rejects `user_id` together with `name`/`email`
(the exclusion was a fork-only dispatch convenience from dfabcb8; nothing
needed it). `crud.create_ticket` stops blanking name/email when a user_id
is present, so logged-in webapp buyers can have their ticket emailed —
until now `_send_ticket_notification` short-circuited on the empty
address for every app purchase.

New optional `frontend_url` (absolute http(s) root, no query/fragment/..,
trailing slash stripped) lets a buyer-side client name the app the buyer
should be returned to and linked into from the ticket email; the origin
allow-list lives in views_api.

Also folds in the pending black reflow of migrations_fork.py.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
`_resolve_frontend_root` honours `CreateTicket.frontend_url` when its
origin is one of LNBITS_CORS_ALLOWED_ORIGINS, the LNbits base URL or
LNBITS_CUSTOM_FRONTEND_URL (400 otherwise — a silent fallback would send
the buyer to the wrong app), and falls back to request.base_url as before.
Under that root the fiat path now parameterises the hosted checkout via
`extra["checkout"]` (lnbits StripeCheckoutOptions): success_url
`/events/{id}?checkout=success&tickets=<ids>`, cancel_url
`/events/{id}?checkout=cancelled`, customer_email, an event-named line
item and event_id/quantity/ticket_ids metadata. Ticket ids are minted
before the invoice so the success URL can carry them (the payment_hash
only exists afterwards). ticket_base_url on the rows uses the same root,
so the emailed link lands in the webapp when the webapp was the client.

Purchases are gated on `effective_payment_methods(event)` (checked after
the free-ticket short-circuit, which charges nothing on any rail).

New anonymous `GET /events/api/v1/qr/{ticket_id}` returns a PNG of
`ticket://<id>` — port of upstream v1.6.8's endpoint without ticket-image
compositing — built at error-correction H with the instance QR logo
(`lnbits_qr_logo`) pasted in the centre, matching the client-side QRs.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
Port of upstream v1.6.8's delivery layer, wave-free: `_deliver_ticket_
notifications` sends text + HTML (the HTML embeds the ticket QR PNG from
this extension on the LNbits host — built from lnbits_baseurl on purpose,
since ticket_base_url may point at a separate web app) and returns a
`TicketResendResult` with per-channel attempted/sent/error. The SMTP
session runs via asyncio.to_thread so a slow relay cannot stall the event
loop while a batch of tickets settles. Resend keeps bypassing the
per-event email opt-in (organizer asked explicitly) and is email-only.
Our nsec-DM Nostr path is kept (upstream went NIP-05-only).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
Organizers pick which rails an event accepts — Lightning, card (fiat) or
both — instead of a bare "allow fiat" toggle. `extra.payment_methods`
uses the field name upstream v2 (lnbits/events#64) introduces so the
eventual rebase merges cleanly; an empty list keeps the legacy rule
(Lightning always, fiat when allow_fiat), and allow_fiat stays the
fiat-currency carrier, kept in lockstep on save. The effective list is
published as the NIP-52 tag `tickets_payment_methods` so clients render
exactly the buttons the purchase endpoint will accept, and the buyer page
defaults to the first accepted rail (a card-only event never submits
"lightning").

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
docs: guest checkout contract, upstream-candidates log; bump 1.6.1-aio.8
Some checks failed
lint.yml / docs: guest checkout contract, upstream-candidates log; bump 1.6.1-aio.8 (pull_request) Failing after 0s
a67c6faff3
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
chore: run tests against the aio lnbits fork (PYTHONPATH), not PyPI lnbits
Some checks failed
lint.yml / chore: run tests against the aio lnbits fork (PYTHONPATH), not PyPI lnbits (pull_request) Failing after 0s
bebf7becc8
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
chore: ignore the data/ dir pytest creates
Some checks failed
lint.yml / chore: ignore the data/ dir pytest creates (pull_request) Failing after 0s
07124b36ae
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
padreug deleted branch feat/guest-checkout-email 2026-09-07 09:27:51 +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!36
No description provided.