From b55d6866d63cdde63f83db6f6183f1a2098b0018 Mon Sep 17 00:00:00 2001 From: Padreug Date: Wed, 30 Sep 2026 19:32:07 +0200 Subject: [PATCH 1/4] fix: honour per-wave fiat when the organiser set an explicit rail list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reported from aio-demo: an event with fiat enabled, Card offered at checkout, and the purchase refused with "Fiat payments are not enabled for this ticket wave." `effective_payment_methods` returned the organiser's explicit `extra.payment_methods` list before ever consulting the wave: explicit = list(...) if explicit: return explicit # <- the wave never got a look in So the `wave` argument I added for #61 did nothing in the common case. The webapp always sets `extra.payment_methods` from its payment-method checkboxes, which means the explicit path is the normal one, not the exception — three layers then disagreed: - the NIP-52 tag advertised `tickets_payment_methods: lightning,fiat` while omitting `tickets_allow_fiat`, contradicting itself - the checkout rendered a Card button - `api_ticket_create`, the only wave-aware check, refused the purchase Asking about a specific wave means asking what a buyer can actually use for it, so a rail that wave cannot honour is now dropped. The event-level question (no wave) still reports every rail the organiser enabled — that is what `/republish-all` and the admin views want. Fiat-only rails on a non-fiat wave now yield an empty list, which is honest: nothing is purchasable from that wave. 5 tests, including both publisher shapes with an explicit list — the case that actually bit, and which the #61 tests missed because their fixtures left `payment_methods` empty. 133 pass. Does not fix the data on events already created through the webapp: their waves were saved with `allow_fiat` unset, so those waves genuinely cannot take fiat until toggled. That is aiolabs/webapp's side. --- models.py | 8 ++++++ tests/test_publish_active_wave.py | 35 ++++++++++++++++++++++++ tests/test_ticket_models.py | 45 +++++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+) diff --git a/models.py b/models.py index ef6ad6f..c5a1131 100644 --- a/models.py +++ b/models.py @@ -224,6 +224,14 @@ def effective_payment_methods( """ explicit = list(getattr(event.extra, "payment_methods", []) or []) if explicit: + # The organiser's rail list is event-level, but fiat is a per-wave + # opt-in. Asking about a specific wave means asking what a buyer can + # actually use for it, so drop a rail that wave cannot honour — + # otherwise the NIP-52 tag advertises fiat and the checkout offers a + # card button that `api_ticket_create` then refuses with "Fiat + # payments are not enabled for this ticket wave." + if wave is not None and not wave.allow_fiat: + return [method for method in explicit if method != "fiat"] return explicit methods = ["lightning"] if wave.allow_fiat if wave is not None else event.allow_fiat: diff --git a/tests/test_publish_active_wave.py b/tests/test_publish_active_wave.py index 7c73781..964a0c4 100644 --- a/tests/test_publish_active_wave.py +++ b/tests/test_publish_active_wave.py @@ -158,3 +158,38 @@ def test_advertised_wave_key_tracks_the_published_wave(): # Published while nothing is on sale — a real state, distinct from the # NULL that means "never published". assert advertised_wave_key(_event([CLOSED_EARLY_BIRD])) == "" + + +def test_published_rails_drop_fiat_when_the_advertised_wave_cannot_take_it(): + """The webapp always sets `extra.payment_methods`, so the explicit-list + path is the normal one — and it used to ignore the wave entirely. + + That published `tickets_payment_methods: lightning,fiat` beside an + absent `tickets_allow_fiat`, and a card button the purchase endpoint + then refused ("Fiat payments are not enabled for this ticket wave"). + """ + event = _event( + [ + _wave("a", 10.0, 0, -10, -1, allow_fiat=True), + _wave("b", 25.0, 9, 0, 20, allow_fiat=False), + ] + ) + event.extra.payment_methods = ["lightning", "fiat"] + tags = _tags(event) + + assert "tickets_allow_fiat" not in tags + assert tags["tickets_payment_methods"] == "lightning" + + +def test_published_rails_keep_fiat_when_the_advertised_wave_takes_it(): + event = _event( + [ + _wave("a", 10.0, 0, -10, -1, allow_fiat=False), + _wave("b", 25.0, 9, 0, 20, allow_fiat=True), + ] + ) + event.extra.payment_methods = ["lightning", "fiat"] + tags = _tags(event) + + assert tags["tickets_allow_fiat"] == "true" + assert tags["tickets_payment_methods"] == "lightning,fiat" diff --git a/tests/test_ticket_models.py b/tests/test_ticket_models.py index 3a42e8f..6d782d2 100644 --- a/tests/test_ticket_models.py +++ b/tests/test_ticket_models.py @@ -172,3 +172,48 @@ def test_public_event_response_carries_waves(): assert [w["id"] for w in body["extra"]["ticket_waves"]] == ["regular"] assert "promo_codes" not in body["extra"] + + +# --- rails vs per-wave fiat -------------------------------------------------- + + +def _fiat_wave(allow_fiat: bool): + from ..models import TicketWave + + return TicketWave( + id="w", + title="w", + opening_date="2030-01-01", + closing_date="2030-02-01", + currency="EUR", + price_per_ticket=10, + amount_tickets=5, + allow_fiat=allow_fiat, + ) + + +def test_explicit_rails_drop_fiat_for_a_wave_that_cannot_take_it(): + """The organiser's rail list is event-level; fiat is per-wave. + + Without this the NIP-52 tag advertises fiat and the checkout renders a + card button that `api_ticket_create` refuses with "Fiat payments are + not enabled for this ticket wave" (reported on aio-demo). + """ + event = _event() + event.extra.payment_methods = ["lightning", "fiat"] + + assert effective_payment_methods(event, _fiat_wave(True)) == ["lightning", "fiat"] + assert effective_payment_methods(event, _fiat_wave(False)) == ["lightning"] + + +def test_event_level_question_still_reports_every_rail(): + """No wave means "what did the organiser enable at all" — unfiltered.""" + event = _event() + event.extra.payment_methods = ["lightning", "fiat"] + assert effective_payment_methods(event) == ["lightning", "fiat"] + + +def test_fiat_only_rails_on_a_non_fiat_wave_leave_nothing_purchasable(): + event = _event() + event.extra.payment_methods = ["fiat"] + assert effective_payment_methods(event, _fiat_wave(False)) == [] From 69c9a4f7570b522c08f6de02361a53b08200cb13 Mon Sep 17 00:00:00 2001 From: Padreug Date: Sat, 3 Oct 2026 07:51:46 +0200 Subject: [PATCH 2/4] chore(release): v1.6.8-aio.3 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #67 honour per-wave fiat when the organiser set an explicit rail list. `effective_payment_methods` returned `extra.payment_methods` before consulting the wave, so the `wave` argument added for #61 did nothing in the common case — the webapp always sets that list. The NIP-52 tag advertised `tickets_payment_methods: lightning,fiat` while omitting `tickets_allow_fiat`, and the checkout offered a card button that `api_ticket_create` then refused. Reported from aio-demo. Eight lines in `models.py`; no schema change, no migration, `migrations.py` still byte-identical to upstream v1.6.8. The organiser-facing symptom is already gone on demo — aiolabs/webapp#179 makes the Card checkbox govern every wave, and re-saving the event corrected all four. This is the backend half: it stops the published tag contradicting itself when a wave genuinely cannot take fiat, which the LNbits admin's per-wave toggles still allow. --- config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.json b/config.json index 0f62e6a..1a689ed 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,6 @@ { "id": "events", - "version": "1.6.8-aio.2", + "version": "1.6.8-aio.3", "name": "Events", "repo": "https://git.atitlan.io/aiolabs/events", "short_description": "Sell and register event tickets", From b4ca9fe65dc8a95082cb9399e633eb902e1a8266 Mon Sep 17 00:00:00 2001 From: Padreug Date: Sat, 3 Oct 2026 20:53:27 +0200 Subject: [PATCH 3/4] fix(ui): ticket waves never rendered in the organiser table MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- static/js/index.js | 20 ++++++++++++++++++++ static/js/index.vue | 18 +----------------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/static/js/index.js b/static/js/index.js index edddaba..fc6e807 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -414,6 +414,26 @@ window.PageEvents = { this.ticketImageUploadTarget = null } }, + waveSummary(eventId, wave) { + // Built here, not in the template. Vue resolves template expressions + // against the component instance, where the `LNbits` global is NOT + // in scope — upstream's v1.6.8 chip called `LNbits.utils` inline and + // threw "Cannot read properties of undefined (reading 'utils')", + // which killed the whole v-for and left the wave list looking empty. + // No other template in this extension touches `LNbits` directly. + const price = this.isFiatCurrency(wave.currency) + ? LNbits.utils.formatCurrency( + Number(wave.price_per_ticket || 0).toFixed(2), + wave.currency + ) + : `${wave.price_per_ticket} sats` + // Wave dates can carry a time (closing_date defaults from + // event_end_date); show the day only. + const opens = String(wave.opening_date || '').slice(0, 10) + const closes = String(wave.closing_date || '').slice(0, 10) + const sold = this.soldTicketsForWave(eventId, wave.id) + return `${wave.title} - ${opens} to ${closes} - ${price} - ${wave.amount_tickets} tickets - ${sold} sold` + }, soldTicketsForWave(eventId, waveId) { return this.allPaidTickets.filter( ticket => diff --git a/static/js/index.vue b/static/js/index.vue index 3ffe9bb..4795a3c 100644 --- a/static/js/index.vue +++ b/static/js/index.vue @@ -271,23 +271,7 @@ > From cadf30b70449471729d16cde0dc6841e8595ec11 Mon Sep 17 00:00:00 2001 From: Padreug Date: Sat, 3 Oct 2026 21:06:55 +0200 Subject: [PATCH 4/4] chore(release): v1.6.8-aio.4 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - #68 ticket waves never rendered in the organiser table. The chip built its label inline with `LNbits.utils.formatCurrency(...)`, but Vue resolves template expressions against the component instance, where the `LNbits` global is not in scope. It threw "Cannot read properties of undefined (reading 'utils')" and the whole v-for rendered nothing, so every event showed an empty wave list however many waves it had. Organisers could not see or edit a wave from the LNbits UI at all. The line came in with the v1.6.8 merge: it is upstream's, and the only `LNbits.` reference in any template here — display.vue and ticket.vue have none. `waveSummary` now builds the label in index.js, and trims the time off wave dates for display. Frontend only; no Python, no schema, no migration. Verified in a browser against a two-wave event before tagging: both chips render with formatted currency and no TypeError. --- config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.json b/config.json index 1a689ed..f7e0ec1 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,6 @@ { "id": "events", - "version": "1.6.8-aio.3", + "version": "1.6.8-aio.4", "name": "Events", "repo": "https://git.atitlan.io/aiolabs/events", "short_description": "Sell and register event tickets",