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
This commit is contained in:
parent
92642a1f24
commit
c2d9a96239
5 changed files with 94 additions and 38 deletions
|
|
@ -17,7 +17,7 @@ from datetime import datetime, timezone
|
|||
from lnbits.core.signers import NostrSigner
|
||||
from loguru import logger
|
||||
|
||||
from .models import Event
|
||||
from .models import Event, effective_payment_methods
|
||||
from .nostr.event import NostrEvent
|
||||
from .nostr_timestamp import monotonic_created_at
|
||||
|
||||
|
|
@ -110,6 +110,11 @@ def build_nip52_event(event: Event, pubkey: str) -> NostrEvent:
|
|||
tags.append(["tickets_allow_fiat", "true"])
|
||||
if event.fiat_currency:
|
||||
tags.append(["tickets_fiat_currency", event.fiat_currency])
|
||||
# Rails the organizer accepts, resolved through the same helper the
|
||||
# ticket endpoint enforces with, so a client can render exactly the
|
||||
# buttons that will be accepted (e.g. a card-only event) without a REST
|
||||
# round-trip. Comma-separated, lowercase.
|
||||
tags.append(["tickets_payment_methods", ",".join(effective_payment_methods(event))])
|
||||
|
||||
# NIP-52 calendar events are replaceable: this d-tag is republished
|
||||
# whenever inventory changes (a ticket sells). Use a strictly-monotonic
|
||||
|
|
|
|||
|
|
@ -35,13 +35,29 @@ window.PageEventsDisplay = {
|
|||
async created() {
|
||||
this.eventId = this.$route.params.id
|
||||
this.event = await this.getEvent()
|
||||
// Default to the first rail the organizer accepts (a card-only event
|
||||
// must not submit "lightning").
|
||||
this.formDialog.data.payment_method = this.paymentMethods[0] || 'lightning'
|
||||
},
|
||||
computed: {
|
||||
formatDescription() {
|
||||
return LNbits.utils.convertMarkdown(this.event?.info || '')
|
||||
},
|
||||
paymentMethods() {
|
||||
// Mirrors `effective_payment_methods` on the backend: an explicit
|
||||
// extra.payment_methods list wins, else Lightning + fiat-if-allow_fiat.
|
||||
const explicit = this.event?.extra?.payment_methods || []
|
||||
if (explicit.length) return explicit
|
||||
return ['lightning', ...(this.event?.allow_fiat ? ['fiat'] : [])]
|
||||
},
|
||||
allowFiatCheckout() {
|
||||
return Boolean(this.event?.allow_fiat)
|
||||
return this.paymentMethods.includes('fiat')
|
||||
},
|
||||
paymentMethodOptions() {
|
||||
return this.paymentMethods.map(method => ({
|
||||
value: method,
|
||||
label: method === 'fiat' ? this.fiatCheckoutLabel : 'Lightning'
|
||||
}))
|
||||
},
|
||||
fiatCheckoutLabel() {
|
||||
if (!this.allowFiatCheckout) return 'Fiat'
|
||||
|
|
@ -78,7 +94,8 @@ window.PageEventsDisplay = {
|
|||
this.formDialog.data.email = ''
|
||||
this.formDialog.data.refund = ''
|
||||
this.formDialog.data.nostr_identifier = ''
|
||||
this.formDialog.data.payment_method = 'lightning'
|
||||
this.formDialog.data.payment_method =
|
||||
this.paymentMethods[0] || 'lightning'
|
||||
},
|
||||
|
||||
closeReceiveDialog() {
|
||||
|
|
@ -112,7 +129,8 @@ window.PageEventsDisplay = {
|
|||
this.formDialog.data.email = ''
|
||||
this.formDialog.data.refund = ''
|
||||
this.formDialog.data.nostr_identifier = ''
|
||||
this.formDialog.data.payment_method = 'lightning'
|
||||
this.formDialog.data.payment_method =
|
||||
this.paymentMethods[0] || 'lightning'
|
||||
Quasar.Notify.create({
|
||||
type: 'positive',
|
||||
message: 'Sent, thank you!',
|
||||
|
|
|
|||
|
|
@ -90,20 +90,18 @@
|
|||
:hint="`If minimum tickets (${event.extra?.min_tickets}) are not met, refund will be sent.`"
|
||||
></q-input>
|
||||
<div class="row q-col-gutter-md q-pt-lg items-center">
|
||||
<div v-if="allowFiatCheckout" class="col-auto">
|
||||
<div v-if="paymentMethods.length > 1" class="col-auto">
|
||||
<q-option-group
|
||||
v-model="formDialog.data.payment_method"
|
||||
inline
|
||||
:options="[
|
||||
{label: 'Lightning', value: 'lightning'},
|
||||
{
|
||||
label: fiatCheckoutLabel,
|
||||
value: 'fiat'
|
||||
}
|
||||
]"
|
||||
:options="paymentMethodOptions"
|
||||
></q-option-group>
|
||||
</div>
|
||||
<div :class="allowFiatCheckout ? 'col-12 col-md-3' : 'col-12'">
|
||||
<div
|
||||
:class="
|
||||
paymentMethods.length > 1 ? 'col-12 col-md-3' : 'col-12'
|
||||
"
|
||||
>
|
||||
<q-input
|
||||
filled
|
||||
dense
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ window.PageEvents = {
|
|||
allow_fiat: false,
|
||||
fiat_currency: 'GBP',
|
||||
extra: {
|
||||
payment_methods: ['lightning'],
|
||||
promo_codes: [],
|
||||
notification_subject: '',
|
||||
notification_body: ''
|
||||
|
|
@ -271,12 +272,7 @@ window.PageEvents = {
|
|||
},
|
||||
saveSettings() {
|
||||
LNbits.api
|
||||
.request(
|
||||
'PUT',
|
||||
'/events/api/v1/events/settings',
|
||||
null,
|
||||
this.settings
|
||||
)
|
||||
.request('PUT', '/events/api/v1/events/settings', null, this.settings)
|
||||
.then(() => {
|
||||
Quasar.Notify.create({type: 'positive', message: 'Settings saved'})
|
||||
})
|
||||
|
|
@ -351,9 +347,7 @@ window.PageEvents = {
|
|||
},
|
||||
republishMyEvents() {
|
||||
LNbits.utils
|
||||
.confirmDialog(
|
||||
'Re-emit your approved events to Nostr relays?'
|
||||
)
|
||||
.confirmDialog('Re-emit your approved events to Nostr relays?')
|
||||
.onOk(() => {
|
||||
this.republishingMine = true
|
||||
LNbits.api
|
||||
|
|
@ -420,6 +414,17 @@ window.PageEvents = {
|
|||
code: code.code.trim().toUpperCase()
|
||||
}))
|
||||
}
|
||||
const methods = data.extra?.payment_methods || []
|
||||
if (methods.length === 0) {
|
||||
Quasar.Notify.create({
|
||||
type: 'warning',
|
||||
message: 'Select at least one payment method.'
|
||||
})
|
||||
return
|
||||
}
|
||||
// allow_fiat stays the fiat-currency carrier the backend and the
|
||||
// NIP-52 tags read; keep it in lockstep with the checkbox list.
|
||||
data.allow_fiat = methods.includes('fiat')
|
||||
if (!this.isFiatCurrency(data.currency)) {
|
||||
if (!data.allow_fiat) {
|
||||
data.fiat_currency = 'GBP'
|
||||
|
|
@ -439,6 +444,15 @@ window.PageEvents = {
|
|||
const end = this.splitDateTime(data.event_end_date)
|
||||
this.formDialog.data = {
|
||||
...data,
|
||||
extra: {
|
||||
...(data.extra || {}),
|
||||
// Events created before extra.payment_methods existed carry an
|
||||
// empty list; show the rails the backend actually accepts for
|
||||
// them (Lightning always, fiat when allow_fiat).
|
||||
payment_methods: data.extra?.payment_methods?.length
|
||||
? data.extra.payment_methods
|
||||
: ['lightning', ...(data.allow_fiat ? ['fiat'] : [])]
|
||||
},
|
||||
event_start_day: start.day,
|
||||
event_start_time: start.time,
|
||||
event_end_day: end.day,
|
||||
|
|
@ -454,6 +468,7 @@ window.PageEvents = {
|
|||
event_end_day: '',
|
||||
event_end_time: '',
|
||||
extra: {
|
||||
payment_methods: ['lightning'],
|
||||
conditional: false,
|
||||
min_tickets: 1,
|
||||
email_notifications: false,
|
||||
|
|
@ -473,6 +488,7 @@ window.PageEvents = {
|
|||
allow_fiat: false,
|
||||
fiat_currency: 'GBP',
|
||||
extra: {
|
||||
payment_methods: ['lightning'],
|
||||
email_notifications: false,
|
||||
nostr_notifications: false,
|
||||
promo_codes: [],
|
||||
|
|
|
|||
|
|
@ -20,10 +20,10 @@
|
|||
<div class="col">
|
||||
<span class="text-subtitle2">Republish to Nostr</span>
|
||||
<div class="text-caption text-grey-7" style="color: #aaa">
|
||||
Re-emit every approved event so connected clients pick
|
||||
up the latest tag set. Useful after the extension
|
||||
publisher changes (e.g. new tickets_* tags) so existing
|
||||
events don't need a per-event edit.
|
||||
Re-emit every approved event so connected clients pick up the
|
||||
latest tag set. Useful after the extension publisher changes
|
||||
(e.g. new tickets_* tags) so existing events don't need a
|
||||
per-event edit.
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-auto">
|
||||
|
|
@ -56,8 +56,8 @@
|
|||
></q-btn>
|
||||
</div>
|
||||
<div class="text-caption q-mt-sm" style="color: #aaa">
|
||||
Re-emit your approved events to Nostr relays. Useful after
|
||||
a publisher upgrade or if a relay dropped your events.
|
||||
Re-emit your approved events to Nostr relays. Useful after a
|
||||
publisher upgrade or if a relay dropped your events.
|
||||
</div>
|
||||
</q-card-section>
|
||||
</q-card>
|
||||
|
|
@ -228,7 +228,13 @@
|
|||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
<q-badge
|
||||
v-if="col.name === 'status'"
|
||||
:color="col.value === 'approved' ? 'green' : col.value === 'proposed' ? 'orange' : 'red'"
|
||||
:color="
|
||||
col.value === 'approved'
|
||||
? 'green'
|
||||
: col.value === 'proposed'
|
||||
? 'orange'
|
||||
: 'red'
|
||||
"
|
||||
:label="col.value"
|
||||
></q-badge>
|
||||
<span v-else v-text="col.value"></span>
|
||||
|
|
@ -399,7 +405,13 @@
|
|||
<q-td v-for="col in props.cols" :key="col.name" :props="props">
|
||||
<q-badge
|
||||
v-if="col.name === 'status'"
|
||||
:color="col.value === 'approved' ? 'green' : col.value === 'proposed' ? 'orange' : 'red'"
|
||||
:color="
|
||||
col.value === 'approved'
|
||||
? 'green'
|
||||
: col.value === 'proposed'
|
||||
? 'orange'
|
||||
: 'red'
|
||||
"
|
||||
:label="col.value"
|
||||
></q-badge>
|
||||
<span v-else v-text="col.value"></span>
|
||||
|
|
@ -587,15 +599,22 @@
|
|||
></q-input>
|
||||
</div>
|
||||
</div>
|
||||
<q-toggle
|
||||
v-model="formDialog.data.allow_fiat"
|
||||
label="Allow fiat checkout"
|
||||
left-label
|
||||
hint="Lets attendees pay through a configured fiat provider using the event currency."
|
||||
></q-toggle>
|
||||
<div class="q-mt-sm">
|
||||
<div class="text-caption text-grey-7">Payment methods *</div>
|
||||
<q-option-group
|
||||
v-model="formDialog.data.extra.payment_methods"
|
||||
type="checkbox"
|
||||
inline
|
||||
:options="paymentMethodOptions"
|
||||
></q-option-group>
|
||||
<div class="text-caption text-grey-7">
|
||||
Card / fiat checkout goes through the fiat provider configured on
|
||||
this LNbits instance. Untick Lightning for a card-only event.
|
||||
</div>
|
||||
</div>
|
||||
<q-select
|
||||
v-if="
|
||||
formDialog.data.allow_fiat &&
|
||||
acceptsFiat &&
|
||||
['sat', 'sats'].includes(
|
||||
(formDialog.data.currency || '').toLowerCase()
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue