A tester's ticket email landed in spam. A mail-tester run against demo
scored 8.3/10 with SPF, DKIM and DMARC all passing through the VPS relay,
so the deductions were all in the message: MISSING_DATE (1.4),
HTML_IMAGE_ONLY_04 (0.3), MISSING_MID (0.1) — and Gmail/Outlook weigh a
missing Date/Message-ID as "machine-generated" far more than that.
- `build_ticket_email` sets Date, a Message-ID under the sender domain,
and a From display name from `lnbits_site_title`.
- The body now carries the event name, dates, location, name on ticket,
ticket id and the door instruction, so the HTML part is no longer a
QR with a handful of words.
Upstream candidate: lnbits core `send_email` has the same omissions.
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
`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
NIP-52 calendar events (31922/31923) are replaceable and republished
whenever inventory changes (a ticket sells). build_nip52_event stamped
created_at=int(time.time()); relays only push a replacement to OPEN
subscriptions when created_at is strictly newer, so two republishes in
the same wall-clock second tie and the second is silently dropped for
live subscribers — clients' "tickets remaining" badge stalls until a
reload. Same root cause as the webapp fix (aiolabs/webapp#122).
- Add monotonic_created_at() in nostr_timestamp.py = max(now, last+1),
mirroring the webapp helper + docs/nostr-patterns/replaceable-events.md.
- Anchor it on the already-persisted Event.nostr_event_created_at
(set after each publish in nostr_hooks.py). The kind-5 delete event is
not replaceable, so it keeps plain int(time.time()).
- Unit tests mirror the webapp's timestamp suite.
Concurrent same-second sales reading the same stored anchor can still
collide; full hardening (row-level lock) is noted as follow-up in #26.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>