Commit graph

33 commits

Author SHA1 Message Date
b4ca9fe65d fix(ui): ticket waves never rendered in the organiser table
Some checks failed
lint.yml / fix(ui): ticket waves never rendered in the organiser table (pull_request) Failing after 0s
The expanded event row showed "Ticket waves" with a + button and an
empty list, however many waves the event had. Reported from aio-demo and
reproduced in a fresh incognito window, so not a caching artifact.

The chip built its label inline with `LNbits.utils.formatCurrency(...)`.
Vue resolves template expressions against the component instance, where
the `LNbits` global is not in scope, so the expression threw

    TypeError: Cannot read properties of undefined (reading 'utils')

and the whole v-for rendered nothing. The promo-code chips beside it
were unaffected, which is what made the list look like a data problem —
the waves were always present in the response.

That line arrived with the v1.6.8 merge: it is upstream's, and upstream
is the only `LNbits.` reference in any template in this extension
(display.vue and ticket.vue have none). So the convention here was
already "format in a method", and the merge quietly broke it.

`waveSummary` now builds the label in index.js, where the global is in
scope. It also trims the day off wave dates for display — closing_date
defaults from event_end_date and can carry a time, which was rendering
as "2026-12-09T16:00:00+01:00" in the chip.

Verified in a browser against a two-wave event: both chips render as
"Primary wave - 2026-10-03 to 2026-11-02 - €30.00 - 50 tickets - 0 sold"
with no TypeError.
2026-10-03 20:53:27 +02:00
ecd05b4168 fix: require a capacity on every ticket wave
Some checks failed
lint.yml / fix: require a capacity on every ticket wave (pull_request) Failing after 0s
"Capacity is always required, there is no unlimited" was settled on #34
and enforced in #62 — but as a single number on the event. Since v1.6.8
capacity is per-wave and `event.amount_tickets` is a derived roll-up
(`sync_event_ticket_waves`), so the rule had nothing holding it up at
the level organisers actually set: the wave form's capacity input had no
minimum, and nothing on the backend checked one at all.

A zero-capacity wave can never be active — `get_active_ticket_waves`
requires `amount_tickets > 0` — so it is the wave-level form of exactly
what #62 removed: an event that looks on sale but refuses every
purchase, on the card and at checkout both.

`_validate_wave_capacity` now runs on create and update. Two details
that matter:

- it checks `ensure_ticket_waves(data)` rather than the raw list, so an
  event submitted with no waves is checked through the primary wave it
  is about to be given, not vacuously passed
- on an edit it only checks waves NEW to the event. Selling out is the
  legitimate route to zero, and rejecting it would make a sold-out event
  uneditable — including the legacy zero-capacity rows this rule exists
  to let organisers fix

Frontend: the wave dialog's capacity input gains the `min="1"` and hint
the event-level field already had, and a new wave opens at 1 rather than
0. That default uses `??`, not `||` — a sold-out wave holds 0 and must
keep showing it instead of silently regaining stock when saved.

Also drops the stale `0 = unlimited / not ticketed` contract still
documented on `CreateEvent.amount_tickets`, which has not been true
since #62.

6 new tests; 120 pass. ruff, black, prettier clean; mypy error set
unchanged from baseline.
2026-09-28 23:11:30 +02:00
d90a0f8322 refactor: drop the SatsPay/watchonly on-chain surface
v1.6.8 sells tickets on-chain through SatsPay charges — a per-purchase
watchonly address, a hosted charge page, and a webhook back into the
extension. We are not adopting that: on-chain goes through the fork's
native LndRestWallet support once core can mint purpose-bound receiving
addresses (aiolabs/lnbits#53). This is the parity note on #41, applied
as its own commit so the merge it follows stays a faithful diff of what
upstream shipped.

Removed:
- services: the five SatsPay/watchonly HTTP clients (and with them the
  only uses of httpx and typing.Any in that module)
- views_api: _get_watchonly_status, GET /events/onchain/status, the
  SatsPay charge branch of api_ticket_create, POST
  /tickets/{id}/satspay-webhook, PUT /tickets/{hash}/onchain-confirm
- models: EventExtra.onchain_{enabled,wallet_id,zeroconf,fasttrack},
  TicketExtra.satspay_charge_id, TicketPaymentRequest.satspay_charge_url
- frontend: the organiser's "Onchain payments" panel and its watchonly
  wallet picker, the per-ticket confirm-onchain button, the SatsPay
  charge-page redirect, and the wallet-status fetch behind them

`onchain` is no longer an accepted payment_method — there is no rail to
fulfil it until #41 lands, so accepting it could only fail later.

Kept as vocabulary for #41, in upstream's field names so it needs no
migration: TicketExtra.onchain / onchain_address and
TicketPaymentRequest.onchain_amount_sat.

35 routes register with no duplicates; ruff, black, prettier clean; 97
tests pass; mypy error set still identical to HEAD's baseline.
2026-09-28 22:21:55 +02:00
ae5affa44f Merge upstream v1.6.8 into the aio fork
Brings in ticket waves (per-wave price/currency/stock/fiat), the paginated
ticket endpoint, the organiser ticket-image template, and the SatsPay
on-chain surface. Refs #33.

Resolutions that were not mechanical, and why:

- set_ticket_paid debits the wave named on the ticket, keeping upstream's
  `> 0` guards; ours decremented unconditionally and could go negative.
  The purchase and free-ticket paths now stamp ticket_wave_id /
  ticket_wave_title, without which every sale would debit the primary wave.

- Pricing moved onto the selected wave (basket_totals takes it as a required
  argument, the promo-validate endpoint resolves the same wave through the
  shared _resolve_ticket_wave). event.price_per_ticket is a roll-up of the
  PRIMARY wave since sync_event_ticket_waves, so pricing off the event
  quoted and charged the first wave's price to buyers who picked a later
  one. Regression test added.

- Kept npub support in two places upstream removed it: the purchase
  endpoint's normalize_public_key path and the notification dispatcher.
  Upstream's replacement rejects with "Only NIP-05 Nostr identifiers are
  supported", which is false for this fork. The purchase-side rejection had
  merged in outside any conflict marker.

- _ticket_image_url existed on both sides as two unrelated features. Ours
  (always-attached rendered QR card) is now _ticket_card_url; upstream's
  (organiser template, opt-in per wave) keeps the name. Both are wired into
  the mail, and the /qr/{ticket_id} endpoint — also duplicated on both
  sides, on the same route — is merged into one handler rather than
  registered twice, where the second copy would have been unreachable.

- models._parse_date now accepts a full ISO datetime. Upstream's date-only
  strptime raised ValueError on any event whose closing_date carries a time,
  which create_event produces by defaulting it from event_end_date — it
  would have 500'd the purchase path, the public event gate and the promo
  preview. Reproduced before fixing.

- Dropped upstream's inline make_qr_png (we import a superset from .qr) and
  its duplicate paymentMethodOptions in display.js, which re-derived payment
  options from per-method booleans and offered an on-chain option the
  backend rejects; the submit gate now matches the template's condition.

- Restored imports the merge silently dropped with upstream's npub removal
  (normalize_public_key, normalize_private_key, DEFAULT_NOSTR_RELAYS).

Event create/update stays ours: upstream's combined endpoint would have
replaced the approval workflow and the explicit field allowlist that keeps
`status` out of the request body.

mypy error set is unchanged from HEAD; ruff, black and the 97 tests pass.
2026-09-28 22:09:18 +02:00
adfd4529f5 fix(nostr): always publish tickets_available, zero is not unlimited
Some checks failed
lint.yml / fix(nostr): always publish tickets_available, zero is not unlimited (pull_request) Failing after 0s
Omitting the tag used to mean "unlimited capacity". Nothing else in the
codebase agreed: `api_get_event` and `api_ticket_create` both treat
`amount_tickets < 1` as sold out. So a zero-capacity event advertised
"Unlimited tickets" on the card while the detail page and the purchase
both returned 410.

Observed on aio-demo — three approved, listed, free events in that
state. For `PKBVuusKikfJFU4PYGtBTW`:

  relay        tickets_available absent  -> webapp renders "Unlimited"
  GET  event   410 "Event is sold out."
  POST ticket  410 "Event is sold out."

The admin form was advertising it too (`min="0"`, `hint="0 = unlimited"`),
so organizers were being invited into the broken state.

Now the tag is always emitted and zero reads as sold out, which is what
every other part of the system already believed. Clients that must handle
an absent tag — a NIP-52 event from another publisher — are unaffected,
since we simply never omit it.

Scoped to removing the contradiction. Making capacity a *required* field
is the other half of #34 and is blocked on the webapp: its create dialog
uses a falsy check (`if (formValues.amount_tickets)`), so a 0 omits the
field entirely and a server-side `ge=1` would 422 it.

Refs #34
2026-09-27 23:03:54 +02:00
07519bec1b feat(admin): max uses column + used count in the promo editor
The Quasar editor gains a "Max uses" input per code (blank = unlimited,
normalised to null on save) and shows "Used n / max" from the derived
count. The public buy page's Clear button now also clears the promo field.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ByAwHU4pRnyE58YocQvAas
2026-09-13 18:43:34 +02:00
87c74ce13f feat(admin): "Email sent" column in the tickets table
Some checks failed
lint.yml / feat(admin): "Email sent" column in the tickets table (pull_request) Failing after 0s
Organizers had no way to see whether a ticket email went out short of
reading the Postfix journal. Derive a column from the flag the mailer
already sets on each ticket (`extra.email_notification_sent`):
"✓ sent" / "not sent" (paid, no delivery yet) / "unpaid" / "no email".

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
2026-09-13 18:39:52 +02:00
f835766935 feat(admin): event form parity with the webapp (1.6.1-aio.13)
Some checks failed
lint.yml / feat(admin): event form parity with the webapp (1.6.1-aio.13) (pull_request) Failing after 0s
lint.yml / feat(admin): event form parity with the webapp (1.6.1-aio.13) (push) Failing after 0s
The LNbits admin form lagged the webapp's CreateEventDialog:

- Payment methods never rendered. c2d9a96 wired the template to
  `paymentMethodOptions` / `acceptsFiat` but never defined them, so the
  q-option-group got `options=undefined` and the fiat-currency select
  was gated on `undefined`. Rails are now two q-checkboxes; Card is
  disabled with an explanatory tooltip when `g.user.fiat_providers` is
  empty (same rule as the webapp) and names the providers otherwise.
- Location (NIP-52 `location` tag) and Categories (NIP-52 `t` tags,
  same 25-item list as the webapp's category.ts) were missing from the
  form even though the model, CRUD and publisher already carry them.
- Datetimes are stamped with the browser's UTC offset on submit, as the
  webapp does; `_to_unix` treats naive values as UTC, so 18:00 CEST
  entered here went out on Nostr as 18:00 UTC. Table columns render
  "YYYY-MM-DD HH:MM" instead of the raw ISO string.
- Validation: title + start date required, end >= start on the folded
  date+time, fiat currency required when a sat-priced event accepts
  card. Create is enabled once wallet + title + start are set; info,
  closing date, tickets and price were all effectively required before
  because the disable check compared undefined fields to null.
- Labels follow the payment-rails vocabulary: "Unit" -> "Price
  currency", "Fiat checkout currency" -> "Fiat currency"; ticket
  closing date and end date explain their defaults.
- A fiat-priced event mirrors `fiat_currency = currency` on save so the
  payload and the `tickets_fiat_currency` tag stay coherent.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018b1bDExMX7W3a47wcgFUjb
2026-09-10 13:43:53 +02:00
defe6d5b00 chore: bundle DejaVu Sans for server-rendered ticket cards
Pillow's built-in default font has no accented glyphs, so any French
event name ("Château") renders as tofu. DejaVu Sans (Bitstream Vera
licence, included) covers Latin fully and is what the ticket card uses.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYwoAkZZmXMMmaBp4WGUBo
2026-09-08 16:40:23 +02:00
c2d9a96239 feat: per-event payment methods (extra.payment_methods)
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
2026-09-06 19:53:05 +02:00
dni ⚡
99b43ff851
fix: use satspay for onchain payments (#56)
* use satspay
* safeguard satspay
* add fasttrack and 0conf
2026-06-05 12:12:08 +02:00
dni ⚡
e0ea0e30e7
feat: onchain payments (#53)
* feat: onchain payments
2026-06-03 10:51:15 +02:00
3606fd9a0a feat(admin): Owner column on All Users' Events card
Some checks failed
lint.yml / feat(admin): Owner column on All Users' Events card (pull_request) Failing after 0s
Adds the event's wallet owner (user_id) as the first column of the
admin-only All Users' Events table so cross-tenant rows are
attributable at a glance. Server-side join: GET /events/all now
resolves each event.wallet -> wallet.user and stamps the result on
the response as wallet_user_id. Frontend gets a dedicated
allUsersEventsTable.columns definition so the user's own-events
table stays unchanged.

Follow-up #22 covers letting the admin actually edit those events
once attributed.
2026-05-24 18:51:51 +02:00
66d263ef14 ui(admin): Tickets card above All Users' Events on the admin index
Some checks failed
lint.yml / ui(admin): Tickets card above All Users' Events on the admin index (pull_request) Failing after 0s
The Tickets table is what an organiser actually scans during day-of
operations — it deserves the top slot. All Users' Events stays one
section down for the cross-tenant audit view (admin-only anyway).
2026-05-24 18:46:18 +02:00
ced6ca2b2b feat: organizer-side "Republish mine" button + scoped endpoint
The admin /republish-all hits every approved event regardless of
owner — useful for the catalog migration, but heavy. Organizers
who want to re-emit just THEIR own events (e.g. after the AIO
publisher gained the tickets_* tags and an organizer's events
should pick them up) need a lighter knob.

Backend: new POST /republish-mine wallet-scoped via require_admin_key,
mirrors api_tickets's `all_wallets=true` shape so the page can
re-emit across every wallet the user owns. Filters to approved +
non-canceled rows.

UI: "Republish mine" button alongside "New Event" so every
logged-in user sees it (no isAdmin gate). Loading state +
confirm dialog + success count notification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:02:36 +02:00
fa2a6e40f0 feat(ui): "Republish all" button on the admin Settings card
Surfaces the POST /republish-all endpoint added in the previous
commit. Lives in the existing admin-gated Settings card on the
events extension landing page, so the LNbits operator can trigger
the migration without curl + access tokens.

Confirm dialog before firing (the endpoint emits one Nostr event
per approved row, fine to retry but worth a click of friction).
Notification shows the republished/total count on success.

Self-closing tags expanded per the LNbits UMD rule
(webapp CLAUDE.md > LNbits + Quasar UMD gotchas) — q-separator
and q-btn would silently nest wrong otherwise.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-23 21:00:30 +02:00
Arc
35c20eda72 fix: another hot one 2026-05-22 20:36:30 +01:00
Arc
8c1538f5fc fix: no submit bug 2026-05-22 20:30:24 +01:00
Arc
777c107c63
feat: UI fixes, ticket waves, ticket image (#52)
* email input fixes

* promo code ui fix

* ticket waves

* works, but horrible slop

* slightly cleaner

* filter most recent

* make

* pyright
2026-05-22 20:18:39 +01:00
df4775126f feat: support optional start/end time on events
event_start_date / event_end_date now accept either YYYY-MM-DD (date-only)
or YYYY-MM-DDTHH:MM (ISO datetime). The NIP-52 publisher switches kind
on the "T" delimiter: kind 31922 (date-based, YYYY-MM-DD start/end) when
absent, kind 31923 (time-based, unix-timestamp start/end + day-granularity
D tags) when present. Delete events match the original publish kind.

Closing-date parsing accepts both formats. The LNbits admin form gains
optional HH:MM inputs alongside each date picker; they fold into the
wire-format string on submit and split back on edit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:21:30 +02:00
c7e95c5452 feat: add event approval workflow with admin UI
Non-admin event submissions now land in a "proposed" queue that LNbits
admins review before the event becomes ticketable and publicly listed.

- m008 adds events.events.status (proposed/approved/rejected); m010 seeds
  an events.settings singleton row with the auto_approve toggle.
- Models: Event/CreateEvent.status, EventsSettings, optional date fields
  with sensible defaults (closing_date defaults to event_end_date which
  defaults to event_start_date), PublicEvent.status surfaces the workflow
  state on the public endpoint.
- crud: get_all/public/pending_events for the admin views; get/update_settings
  for the auto_approve toggle; create_event auto-fills missing date defaults.
- views_api:
  * POST /api/v1/events accepts wallet invoice keys so anyone can submit;
    handler stamps status="proposed" for non-admins when auto_approve is off
  * /public, /all, /pending, /settings (GET+PUT), /{id}/{approve,reject},
    /{id}/tickets endpoints; literal-prefix routes declared before /{event_id}
    so FastAPI matches them correctly
  * Public GET /{event_id} bypasses sold-out / closing-window gates for
    proposed/rejected events and returns the trimmed PublicEvent so the SFC
    can render a "pending approval" banner
  * POST /tickets/{event_id} rejects when event.status != "approved"
- Frontend: index.vue gains an admin Settings card, Pending Approvals list,
  status badge column and approve/reject row actions, plus an All Users'
  Events admin table; index.js gains the data + methods + an isAdmin probe
  via GET /events/all; display.vue shows pending/rejected banners and
  hides the Buy Ticket form unless status === "approved".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-22 09:19:21 +02:00
dni ⚡
4bf867eef0
feat: add resend email button to ticket list (#51)
- resending only possible when ticket is paid.
2026-05-13 11:30:14 +02:00
Arc
6768b78c6f Custom subject and body 2026-05-08 19:14:07 +01:00
dni ⚡
0824b1120b
feat: add paid/registered badge to ticket page (#49)
some visual verification on the ticket page that it is paid / checked
in.
2026-05-07 17:06:38 +02:00
Arc
32c230957e fix: if sats and fiat checkout conversion currency 2026-05-07 14:34:22 +01:00
Arc
680b035ec9
feat: add fiat checkout and nostr + email notification (#50)
* feat: fiat and email/nostr notifications

* make n bake
2026-05-07 12:31:32 +01:00
dni ⚡
4afc78d44d
feat: register public page saves to localstorage (#48)
* feat: register public page saves to localstorage

previsously it fetched all tickets without much information. now it
saves the full scanned ticket after it was scanned, so it can be checked
by some1 without a login

* add last scan

* short id

* prettier
2026-05-05 10:45:14 +02:00
Tiago Vasconcelos
9e477ac959
feat: make events dynamic (#43)
---------

Co-authored-by: dni <office@dnilabs.com>
2026-05-04 17:01:53 +02:00
Tiago Vasconcelos
42de6d4791
feat: add promo codes and conditional events (#40)
Some checks failed
/ release (push) Has been cancelled
/ pullrequest (push) Has been cancelled
* add extra column
* add conditional events
* refunds
* conditional events working
* adding promo codes
* promo codes logic

---------

Co-authored-by: dni ⚡ <office@dnilabs.com>
2025-12-09 10:48:00 +00:00
dni ⚡
6714dcddc7
feat: update to lnbits 1.0.0 (#36) 2024-10-11 13:52:39 +02:00
benarc
ea3a60ecd4 Added video 2024-05-17 17:39:11 +01:00
benarc
1b1cf72e17 Added extended description 2024-04-30 15:36:05 +01:00
Arc
bcde392f41
Add files via upload 2023-02-11 08:06:45 +00:00