fix(events): the Card checkbox governs fiat on every wave

Follows the inheritance commit: seeding only NEW waves still left the
ones already on an event refusing card payments, and gave the organiser
no single place to change that.

The extension treats fiat as a per-wave opt-in, but the webapp offers
exactly one control for it — the event-level Card checkbox. So that
checkbox now governs every wave on save: ticking it enables fiat on all
of them, unticking disables it on all of them, and each wave settles in
the event's fiat currency rather than whatever it happened to hold.
Waves created before this carry the backend's "GBP" default even on a
EUR event, which is what demo showed.

Removes the per-wave switch added a commit ago. With the event-level
setting applied on save it would be overwritten silently, and a control
that disagrees with what gets stored is worse than no control.

The trade-off, stated plainly: a per-wave fiat setting made in the
LNbits admin is overwritten the next time the event is saved from the
webapp. That is acceptable while the webapp has no way to represent one
— and it is deliberate rather than accidental, which the previous
behaviour was not.

4 tests: fiat on and off across every wave, the event currency winning
over a stale per-wave one, and everything else about each wave left
alone. 83 pass; vue-tsc, prettier and the production build clean.
This commit is contained in:
Padreug 2026-09-30 21:43:57 +02:00
commit 773d87d35b
3 changed files with 71 additions and 20 deletions

View file

@ -598,7 +598,28 @@ const onSubmit = form.handleSubmit(
notification_subject: formValues.notification_subject,
notification_body: formValues.notification_body,
promo_codes: toWirePromoCodes(promoRows.value),
ticket_waves: [primaryWave, ...extraWaves.value],
// Fiat is a per-wave opt-in in the extension, but the webapp
// offers ONE event-level control for it (the Card checkbox), so
// that checkbox governs every wave: ticking Card enables fiat on
// all of them, unticking disables it on all of them. Without this a
// wave added here refuses card payments while the event itself
// reads as fiat-enabled — what organisers hit on demo.
//
// Deliberately simple for now: it does mean a per-wave setting made
// in the LNbits admin is overwritten next time the event is saved
// from here. Acceptable while the webapp has no per-wave control to
// represent one, and better than a control that silently disagrees
// with what gets stored.
ticket_waves: [primaryWave, ...extraWaves.value].map((wave) => ({
...wave,
allow_fiat: acceptsFiat,
// The event's fiat currency, unconditionally — not a fallback to
// whatever the wave happened to hold. Waves created before this
// carry the backend's "GBP" default, and a wave settling in a
// different currency than the event advertises is not a state the
// webapp can represent or an organiser can have asked for.
fiat_currency: formValues.fiat_currency,
})),
};
if (isEditMode.value) {

View file

@ -1,7 +1,6 @@
<script setup lang="ts">
import { computed } from "vue";
import { Button } from "@/components/ui/button";
import { Switch } from "@/components/ui/switch";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Plus, Trash2 } from "lucide-vue-next";
@ -28,9 +27,10 @@ const props = defineProps<{
knownIds?: string[];
/** Currency of the primary wave; new rows inherit it. */
currency?: string;
/** Whether the event accepts fiat, and in what. New rows inherit both:
* fiat is a per-wave opt-in, so a wave created without it refuses card
* payments no matter what the event-level toggle says. */
/** Whether the event accepts fiat, and in what. New rows inherit both so
* the in-memory row is right before saving; on save the dialog applies
* the event-level Card checkbox to every wave anyway, which is why
* there is no per-wave switch here — it would be overwritten. */
allowFiat?: boolean;
fiatCurrency?: string;
disabled?: boolean;
@ -186,21 +186,6 @@ function isSoldOut(row: TicketWave): boolean {
</div>
</div>
<div
v-if="allowFiat"
class="flex items-center justify-between rounded-md bg-muted/40 px-2 py-1.5"
>
<span class="text-xs">
Accept card for this wave
<span class="text-muted-foreground">({{ row.fiat_currency }})</span>
</span>
<Switch
:model-value="row.allow_fiat ?? false"
:disabled="disabled"
@update:model-value="update(index, { allow_fiat: Boolean($event) })"
/>
</div>
<p v-if="errors[index]" class="text-xs text-destructive">
{{ errors[index] }}
</p>

View file

@ -345,3 +345,48 @@ describe("newWaveRow — fiat inheritance", () => {
expect(row.fiat_currency).toBe("EUR");
});
});
describe("the Card checkbox governs every wave", () => {
// The webapp has one event-level fiat control, so on save it is applied
// to all waves. Pinned because getting it wrong is silent: the event
// reads as fiat-enabled while a wave refuses card payments, and the
// buyer only finds out at checkout.
const applyEventFiat = (
waves: TicketWave[],
acceptsFiat: boolean,
cur: string,
) =>
waves.map((w) => ({ ...w, allow_fiat: acceptsFiat, fiat_currency: cur }));
const waves = [
wave({ id: "primary", allow_fiat: true, fiat_currency: "EUR" }),
wave({ id: "early", allow_fiat: false, fiat_currency: "GBP" }),
wave({ id: "late", allow_fiat: false, fiat_currency: "GBP" }),
];
it("ticking Card enables fiat on every wave, not just the primary", () => {
const out = applyEventFiat(waves, true, "EUR");
expect(out.map((w) => w.allow_fiat)).toEqual([true, true, true]);
expect(out.map((w) => w.fiat_currency)).toEqual(["EUR", "EUR", "EUR"]);
});
it("unticking Card disables it on every wave", () => {
expect(
applyEventFiat(waves, false, "EUR").map((w) => w.allow_fiat),
).toEqual([false, false, false]);
});
it("overwrites a stale wave fiat currency with the event's", () => {
// Waves created before this carry the backend's "GBP" default even on
// a EUR event, which is what demo showed.
const out = applyEventFiat(waves, true, "EUR");
expect(out.every((w) => w.fiat_currency === "EUR")).toBe(true);
});
it("leaves everything else about each wave alone", () => {
const [, early] = applyEventFiat(waves, true, "EUR");
expect(early.id).toBe("early");
expect(early.price_per_ticket).toBe(waves[1].price_per_ticket);
expect(early.amount_tickets).toBe(waves[1].amount_tickets);
});
});