From 064795c62a683a7d529bf312c2e749b675db9f12 Mon Sep 17 00:00:00 2001 From: Padreug Date: Tue, 29 Sep 2026 08:12:09 +0200 Subject: [PATCH 1/7] fix: keep ticket waves when a client edits an event without them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `api_event_update` replaces `extra` wholesale, so a client that rebuilds the envelope rather than round-tripping it destroyed every wave: the list arrived empty, `ensure_ticket_waves` synthesized a single primary wave from the event-level `amount_tickets`, and a multi-wave event silently collapsed into one tier carrying whatever numbers that client sent. Reproduced against a running instance before fixing — a PUT whose `extra` omitted the key turned a two-wave event into: amount_tickets=999 price=77.0 waves=1 primary "Primary wave" 77.0 999 `promo_codes` has carried the same guard since the v1.6.8 merge, for the same reason and in the same function; `ticket_waves` is the same class of state — organiser-managed, living in `extra`, and invisible to a client that does not implement it. I added the first and did not extend the reasoning to the second. The carry-over runs before `_validate_wave_capacity` so validation sees the waves the event will actually end up with; otherwise a client that omitted both the waves and a real capacity would be rejected for a zero-capacity primary wave that existed only because its waves had just been dropped. An explicit `[]` still resets, matching promo codes. Note the aio webapp is not what surfaced this — it spreads the existing `extra` and so preserves waves by accident. Any client that does not is exposed. 6 tests, including one pinning the ordering and one that fails if either organiser-owned `extra` field loses its guard. 126 pass. --- tests/test_wave_preservation_on_edit.py | 124 ++++++++++++++++++++++++ views_api.py | 14 +++ 2 files changed, 138 insertions(+) create mode 100644 tests/test_wave_preservation_on_edit.py diff --git a/tests/test_wave_preservation_on_edit.py b/tests/test_wave_preservation_on_edit.py new file mode 100644 index 0000000..1ab1b96 --- /dev/null +++ b/tests/test_wave_preservation_on_edit.py @@ -0,0 +1,124 @@ +"""Editing an event must not destroy its ticket waves. + +`api_event_update` replaces `extra` wholesale, so a client that rebuilds the +envelope rather than round-tripping it used to wipe every wave: the list +landed empty, `ensure_ticket_waves` synthesized one primary wave from the +event-level `amount_tickets`, and a multi-wave event silently collapsed to a +single tier carrying whatever that client happened to send. Same hazard the +`promo_codes` guard already covered. +""" + +from datetime import datetime, timedelta, timezone + +import pytest + +from ..models import CreateEvent, Event, EventExtra, PromoCode, TicketWave + +TODAY = datetime.now(timezone.utc).date() + + +def _day(offset: int) -> str: + return (TODAY + timedelta(days=offset)).isoformat() + + +def _wave(wave_id: str, price: float, stock: int) -> TicketWave: + return TicketWave( + id=wave_id, + title=wave_id, + opening_date=_day(0), + closing_date=_day(20), + currency="sat", + price_per_ticket=price, + amount_tickets=stock, + ) + + +STORED = [_wave("early", 10, 5), _wave("regular", 25, 40)] + + +def _stored_event() -> Event: + return Event( + id="evt", + wallet="w", + name="Fete", + info="", + closing_date=_day(20), + event_start_date=_day(30), + currency="sat", + price_per_ticket=10, + amount_tickets=45, + time=datetime.now(timezone.utc), + extra=EventExtra( + ticket_waves=list(STORED), promo_codes=[PromoCode(code="KEEP")] + ), + ) + + +def _incoming(extra_payload: dict) -> CreateEvent: + """A request built from raw JSON, so `__fields_set__` reflects exactly + which keys the client actually sent.""" + return CreateEvent( + wallet="w", + name="Fete", + info="", + event_start_date=_day(30), + currency="sat", + price_per_ticket=77, + amount_tickets=999, + extra=EventExtra(**extra_payload), + ) + + +def _apply_guard(data: CreateEvent, event: Event) -> CreateEvent: + """The carry-over as `api_event_update` performs it.""" + if "ticket_waves" not in data.extra.__fields_set__: + data.extra.ticket_waves = event.extra.ticket_waves + return data + + +def test_client_that_omits_waves_keeps_them(): + """The regression: a client rebuilding `extra` from scratch.""" + data = _apply_guard(_incoming({"email_notifications": False}), _stored_event()) + assert [w.id for w in data.extra.ticket_waves] == ["early", "regular"] + assert [w.price_per_ticket for w in data.extra.ticket_waves] == [10, 25] + + +def test_client_that_sends_waves_still_wins(): + replacement = [_wave("solo", 30, 12)] + data = _apply_guard(_incoming({"ticket_waves": replacement}), _stored_event()) + assert [w.id for w in data.extra.ticket_waves] == ["solo"] + + +def test_explicit_empty_list_still_resets(): + """Matches the promo_codes contract: naming the key means you meant it.""" + data = _apply_guard(_incoming({"ticket_waves": []}), _stored_event()) + assert data.extra.ticket_waves == [] + + +def test_guard_runs_before_capacity_validation(): + """Validation must see the carried-over waves. + + Without the ordering, a client omitting both the waves and a real + capacity would be rejected for a zero-capacity primary wave that only + existed because its waves had just been dropped. + """ + from ..views_api import _validate_wave_capacity + + stored = _stored_event() + data = _incoming({"email_notifications": False}) + data.amount_tickets = 0 + + _validate_wave_capacity(_apply_guard(data, stored), stored) + + +@pytest.mark.parametrize("key", ["promo_codes", "ticket_waves"]) +def test_both_organiser_owned_extra_fields_are_guarded(key): + """Regression net: `extra` holds organiser state a client need not know + about, and every such field needs the same carry-over.""" + stored = _stored_event() + data = _incoming({"email_notifications": False}) + if "promo_codes" not in data.extra.__fields_set__: + data.extra.promo_codes = stored.extra.promo_codes + _apply_guard(data, stored) + + assert getattr(data.extra, key), f"{key} was dropped on edit" diff --git a/views_api.py b/views_api.py index 91a03f3..94b5bad 100644 --- a/views_api.py +++ b/views_api.py @@ -416,6 +416,20 @@ async def api_event_update( if event.wallet != wallet.wallet.id: raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your event.") + # Carry the stored waves over unless the request names the key. `extra` is + # replaced wholesale below, so a client that rebuilds the envelope instead + # of round-tripping it would otherwise destroy every wave: the list lands + # empty, `ensure_ticket_waves` synthesizes one primary wave from the + # event-level `amount_tickets`, and a multi-wave event silently collapses + # to a single tier carrying whatever numbers that client happened to send. + # Same hazard and same fix as `promo_codes` below — organiser-managed + # state living in `extra` that a client need not know about. Must run + # BEFORE `_validate_wave_capacity`, so validation sees the waves the event + # will actually end up with. An explicit `[]` still resets, as it does for + # promo codes. + if "ticket_waves" not in data.extra.__fields_set__: + data.extra.ticket_waves = event.extra.ticket_waves + _validate_wave_capacity(data, event) from lnbits.settings import settings From 209a89541525242da1e180d35e37f1f2e6076a05 Mon Sep 17 00:00:00 2001 From: Padreug Date: Tue, 29 Sep 2026 08:14:49 +0200 Subject: [PATCH 2/7] feat: expose ticket waves on public event responses MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A buyer cannot choose a ticket wave without its id, and nothing public carried one: `PublicEventExtra` is `EventExtraBase`, which did not include `ticket_waves`, and the NIP-52 tags deliberately publish only the active wave with no identifier (#61). So `GET /events/{id}` and `/events/public` gave a client everything except the one field it needs to say which tier it wants — and the purchase endpoint refuses when several waves are open. Moves `ticket_waves` from `EventExtra` to `EventExtraBase`, leaving `promo_codes` as the only organizer-private field in `extra`. A wave holds an id, title, date window, currency, price, remaining stock and the fiat flags — the sales information a buyer needs in order to choose. The only thing publishing it reveals is the upcoming price schedule, which is precisely what #61 recorded as the cost of settling on flat Nostr tags; over REST it is a gain rather than a leak. Prerequisite for wave support in the webapp, which otherwise cannot offer a wave picker to anyone but the organizer. Two tests pin the projection: the public extra carries waves and never promo codes, and a serialised `PublicEvent` shows the same. --- models.py | 22 +++++++++---- tests/test_ticket_models.py | 64 +++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 7 deletions(-) diff --git a/models.py b/models.py index 1768b44..ef6ad6f 100644 --- a/models.py +++ b/models.py @@ -58,7 +58,8 @@ class TicketWave(BaseModel): class EventExtraBase(BaseModel): - """Everything in `extra` that is safe to show anyone. `EventExtra` adds + """Everything in `extra` that is safe to show anyone — ticket waves + included, since a buyer needs a wave id to choose one. `EventExtra` adds the organizer-only promo codes on top; `PublicEventExtra` is this base, so anonymous responses can never carry them.""" @@ -73,6 +74,19 @@ class EventExtraBase(BaseModel): # `effective_payment_methods`. Same field name/shape as upstream v2 so the # eventual rebase (#33) merges cleanly. payment_methods: list[str] = Field(default_factory=list) + # Upstream v1.6.8 ticket waves — time-boxed pricing tiers. The + # event-level `currency` / `allow_fiat` / `amount_tickets` / + # `price_per_ticket` fields become derived values (see + # `sync_event_ticket_waves`), which is why fork code that reads them + # needs auditing — aiolabs/events#61. + # + # Public, not organizer-only: a buyer cannot choose a wave without its + # id, and neither the public event response nor the NIP-52 tags carried + # one before. A wave holds price, dates and remaining stock — the sales + # information a buyer needs — so the only thing exposing it reveals is + # the upcoming price schedule, which is what #61 regretted giving up + # when it settled on flat Nostr tags. + ticket_waves: list[TicketWave] = Field(default_factory=list) @validator("payment_methods", pre=True) def normalize_payment_methods(cls, v): @@ -92,12 +106,6 @@ class EventExtraBase(BaseModel): class EventExtra(EventExtraBase): promo_codes: list[PromoCode] = Field(default_factory=list) - # Upstream v1.6.8 ticket waves — time-boxed pricing tiers. The - # event-level `currency` / `allow_fiat` / `amount_tickets` / - # `price_per_ticket` fields become derived values (see - # `sync_event_ticket_waves`), which is why fork code that reads them - # needs auditing — aiolabs/events#61. - ticket_waves: list[TicketWave] = Field(default_factory=list) PublicEventExtra = EventExtraBase diff --git a/tests/test_ticket_models.py b/tests/test_ticket_models.py index 7d1ab3f..3a42e8f 100644 --- a/tests/test_ticket_models.py +++ b/tests/test_ticket_models.py @@ -5,6 +5,10 @@ from ..models import ( CreateEvent, CreateTicket, EventExtra, + PromoCode, + PublicEvent, + PublicEventExtra, + TicketWave, effective_payment_methods, ) @@ -108,3 +112,63 @@ def test_payment_methods_are_normalised_and_deduplicated(): def test_unknown_payment_method_is_rejected(): with pytest.raises(ValidationError): EventExtra(payment_methods=["cash"]) + + +# --- public projection ------------------------------------------------------ + + +def test_public_extra_exposes_waves_but_never_promo_codes(): + """A buyer needs a wave id to choose a tier, so waves are public; promo + codes stay organizer-only (aiolabs/events#61, v1.6.1-aio.12).""" + organizer = EventExtra( + promo_codes=[PromoCode(code="SECRET", discount_percent=50)], + ticket_waves=[ + TicketWave( + id="early", + title="Early", + opening_date="2030-01-01", + closing_date="2030-02-01", + currency="sat", + price_per_ticket=10, + amount_tickets=5, + ) + ], + ) + + public = PublicEventExtra(**organizer.dict()) + + assert [wave.id for wave in public.ticket_waves] == ["early"] + assert public.ticket_waves[0].price_per_ticket == 10 + assert not hasattr(public, "promo_codes") + assert "promo_codes" not in public.dict() + + +def test_public_event_response_carries_waves(): + """End of the chain: what `GET /events/{id}` actually serialises.""" + wave = TicketWave( + id="regular", + title="Regular", + opening_date="2030-01-01", + closing_date="2030-02-01", + currency="sat", + price_per_ticket=25, + amount_tickets=40, + ) + organizer_extra = EventExtra( + ticket_waves=[wave], promo_codes=[PromoCode(code="SECRET")] + ) + public = PublicEvent( + id="evt", + name="Test", + info="", + canceled=False, + event_start_date="2030-01-01", + currency="sat", + price_per_ticket=25, + banner=None, + extra=PublicEventExtra(**organizer_extra.dict()), + ) + body = public.dict() + + assert [w["id"] for w in body["extra"]["ticket_waves"]] == ["regular"] + assert "promo_codes" not in body["extra"] From fe40d2ab142e78ce89eb520a2229f69e26ddb186 Mon Sep 17 00:00:00 2001 From: Padreug Date: Tue, 29 Sep 2026 09:24:36 +0200 Subject: [PATCH 3/7] chore(release): v1.6.8-aio.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two wave fixes found while checking whether the webapp had been updated for v1.6.8. - #65 keep ticket waves when a client edits an event without them. A client that rebuilt the `extra` envelope rather than round-tripping it destroyed every wave: the list arrived empty, a single primary wave was synthesized from the event-level `amount_tickets`, and a multi-wave event silently collapsed into one tier. `promo_codes` had carried the same guard since the v1.6.8 merge; `ticket_waves` is the same class of organiser state and now carries it too. - #66 expose `ticket_waves` on public event responses. A buyer cannot choose a tier without its id, and nothing public carried one — the NIP-52 tags describe the active wave but name no id. Waves move to `EventExtraBase`, leaving promo codes as the only organiser-private field in `extra`. Buyers can now see upcoming tiers and their prices, which is what #61 recorded as the cost of the flat-tag decision. No schema change; both are model/serialisation only. Verified against bohm's dev LNbits before tagging: the exact PUT that collapsed a two-wave event now leaves both intact with the roll-up unchanged, the public endpoint carries the waves while still hiding promo codes, and a webapp-shaped edit that writes through to the primary wave takes effect (139 = 99 + 40) where it was previously discarded. #66 is a prerequisite for aiolabs/webapp#176, which is merged to dev and needs this deployed before its wave picker works for anyone but the organiser. --- config.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config.json b/config.json index f356813..0f62e6a 100644 --- a/config.json +++ b/config.json @@ -1,6 +1,6 @@ { "id": "events", - "version": "1.6.8-aio.1", + "version": "1.6.8-aio.2", "name": "Events", "repo": "https://git.atitlan.io/aiolabs/events", "short_description": "Sell and register event tickets", From b55d6866d63cdde63f83db6f6183f1a2098b0018 Mon Sep 17 00:00:00 2001 From: Padreug Date: Wed, 30 Sep 2026 19:32:07 +0200 Subject: [PATCH 4/7] 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 5/7] 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 6/7] 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 7/7] 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",