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.
This commit is contained in:
commit
ae5affa44f
14 changed files with 2228 additions and 275 deletions
|
|
@ -13,6 +13,7 @@ window.PageEventsDisplay = {
|
|||
email: '',
|
||||
refund: '',
|
||||
nostr_identifier: '',
|
||||
ticket_wave_id: null,
|
||||
payment_method: 'lightning'
|
||||
}
|
||||
},
|
||||
|
|
@ -46,26 +47,68 @@ window.PageEventsDisplay = {
|
|||
paymentMethods() {
|
||||
// Mirrors `effective_payment_methods` on the backend: an explicit
|
||||
// extra.payment_methods list wins, else Lightning + fiat-if-allow_fiat.
|
||||
// Since v1.6.8 `allow_fiat` is a per-wave flag and `event.allow_fiat`
|
||||
// is only the PRIMARY wave's, so prefer the active wave when there is
|
||||
// one — otherwise a later fiat-enabled wave would not offer fiat.
|
||||
const explicit = this.event?.extra?.payment_methods || []
|
||||
if (explicit.length) return explicit
|
||||
return ['lightning', ...(this.event?.allow_fiat ? ['fiat'] : [])]
|
||||
const allowFiat = this.selectedTicketWave
|
||||
? this.selectedTicketWave.allow_fiat
|
||||
: this.event?.allow_fiat
|
||||
return ['lightning', ...(allowFiat ? ['fiat'] : [])]
|
||||
},
|
||||
activeTicketWaves() {
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
return (this.event?.extra?.ticket_waves || []).filter(
|
||||
wave =>
|
||||
wave.amount_tickets > 0 &&
|
||||
wave.opening_date <= today &&
|
||||
wave.closing_date >= today
|
||||
)
|
||||
},
|
||||
selectedTicketWave() {
|
||||
return (
|
||||
this.activeTicketWaves.find(
|
||||
wave => wave.id === this.formDialog.data.ticket_wave_id
|
||||
) ||
|
||||
this.activeTicketWaves[0] ||
|
||||
null
|
||||
)
|
||||
},
|
||||
showTicketWaveSelector() {
|
||||
return this.activeTicketWaves.length > 1
|
||||
},
|
||||
allowFiatCheckout() {
|
||||
return this.paymentMethods.includes('fiat')
|
||||
},
|
||||
paymentMethodOptions() {
|
||||
// Options come from `paymentMethods` — the same list the backend
|
||||
// validates against in `effective_payment_methods` — rather than being
|
||||
// re-derived from per-method booleans. v1.6.8 added a second copy of
|
||||
// this computed that built the list from `allow_fiat`/`onchain_enabled`
|
||||
// instead; because it was defined later in the object it silently won
|
||||
// the merge, and it offers a "Bitcoin" option for any event with
|
||||
// `extra.onchain_enabled` even though the backend rejects `onchain`
|
||||
// unless it is in `extra.payment_methods` (views_api: "not in
|
||||
// effective_payment_methods"). Labels below are upstream's; the source
|
||||
// of the list is ours.
|
||||
const labels = {
|
||||
lightning: 'Lightning',
|
||||
fiat: this.fiatCheckoutLabel,
|
||||
onchain: 'Bitcoin'
|
||||
}
|
||||
return this.paymentMethods.map(method => ({
|
||||
value: method,
|
||||
label: method === 'fiat' ? this.fiatCheckoutLabel : 'Lightning'
|
||||
label: labels[method] || method
|
||||
}))
|
||||
},
|
||||
fiatCheckoutLabel() {
|
||||
if (!this.allowFiatCheckout) return 'Fiat'
|
||||
const unit = ['sat', 'sats'].includes(
|
||||
(this.event?.currency || '').toLowerCase()
|
||||
(this.selectedTicketWave?.currency || '').toLowerCase()
|
||||
)
|
||||
? this.event?.fiat_currency
|
||||
: this.event?.currency
|
||||
? this.selectedTicketWave?.fiat_currency
|
||||
: this.selectedTicketWave?.currency
|
||||
return `Fiat (${(unit || 'GBP').toUpperCase()})`
|
||||
},
|
||||
allowEmailNotifications() {
|
||||
|
|
@ -82,6 +125,16 @@ window.PageEventsDisplay = {
|
|||
'GET',
|
||||
`/events/api/v1/events/${this.eventId}`
|
||||
)
|
||||
const activeWaves = (data.extra?.ticket_waves || []).filter(wave => {
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
return (
|
||||
wave.amount_tickets > 0 &&
|
||||
wave.opening_date <= today &&
|
||||
wave.closing_date >= today
|
||||
)
|
||||
})
|
||||
this.formDialog.data.ticket_wave_id =
|
||||
activeWaves.length === 1 ? activeWaves[0].id : null
|
||||
return data
|
||||
} catch (error) {
|
||||
this.eventErrorLabel = 'Event unavailable.'
|
||||
|
|
@ -94,6 +147,10 @@ window.PageEventsDisplay = {
|
|||
this.formDialog.data.email = ''
|
||||
this.formDialog.data.refund = ''
|
||||
this.formDialog.data.nostr_identifier = ''
|
||||
this.formDialog.data.ticket_wave_id =
|
||||
this.activeTicketWaves.length === 1
|
||||
? this.activeTicketWaves[0].id
|
||||
: null
|
||||
this.formDialog.data.promo_code = ''
|
||||
this.formDialog.data.payment_method =
|
||||
this.paymentMethods[0] || 'lightning'
|
||||
|
|
@ -108,6 +165,13 @@ window.PageEventsDisplay = {
|
|||
this.paymentWebsocket.close()
|
||||
this.paymentWebsocket = null
|
||||
}
|
||||
this.paymentReq = null
|
||||
this.receive = {
|
||||
show: false,
|
||||
status: 'pending',
|
||||
paymentReq: null,
|
||||
isFiat: false
|
||||
}
|
||||
},
|
||||
nameValidation(val) {
|
||||
const regex = /[`!@#$%^&*()_+\-=\[\]{};':"\\|,.<>\/?~]/g
|
||||
|
|
@ -130,6 +194,10 @@ window.PageEventsDisplay = {
|
|||
this.formDialog.data.email = ''
|
||||
this.formDialog.data.refund = ''
|
||||
this.formDialog.data.nostr_identifier = ''
|
||||
this.formDialog.data.ticket_wave_id =
|
||||
this.activeTicketWaves.length === 1
|
||||
? this.activeTicketWaves[0].id
|
||||
: null
|
||||
this.formDialog.data.payment_method =
|
||||
this.paymentMethods[0] || 'lightning'
|
||||
Quasar.Notify.create({
|
||||
|
|
@ -160,12 +228,26 @@ window.PageEventsDisplay = {
|
|||
{
|
||||
name: this.formDialog.data.name,
|
||||
email: this.formDialog.data.email,
|
||||
ticket_wave_id: this.formDialog.data.ticket_wave_id || null,
|
||||
promo_code: this.formDialog.data.promo_code || null,
|
||||
refund_address: this.formDialog.data.refund || null,
|
||||
nostr_identifier: this.formDialog.data.nostr_identifier || null,
|
||||
payment_method: this.formDialog.data.payment_method
|
||||
// Gate matches the template's (`paymentMethods.length > 1`).
|
||||
// v1.6.8 gated on `showPaymentMethodSelector`
|
||||
// (`allowFiatCheckout || allowOnchain`), a different condition:
|
||||
// where the two disagreed the buyer's visible choice was
|
||||
// silently replaced with 'lightning'.
|
||||
payment_method:
|
||||
this.paymentMethods.length > 1
|
||||
? this.formDialog.data.payment_method
|
||||
: this.paymentMethods[0] || 'lightning'
|
||||
}
|
||||
)
|
||||
if (data.satspay_charge_url) {
|
||||
window.location.href = data.satspay_charge_url
|
||||
return
|
||||
}
|
||||
|
||||
const isFiat = Boolean(data.is_fiat)
|
||||
this.paymentReq = isFiat
|
||||
? data.fiat_payment_request || null
|
||||
|
|
@ -197,7 +279,7 @@ window.PageEventsDisplay = {
|
|||
|
||||
const url = new URL(window.location)
|
||||
url.protocol = url.protocol === 'https:' ? 'wss:' : 'ws:'
|
||||
url.pathname = `/api/v1/ws/${paymentHash}`
|
||||
url.pathname = `/events/api/v1/tickets/ws/${paymentHash}`
|
||||
url.search = ''
|
||||
url.hash = ''
|
||||
|
||||
|
|
@ -206,7 +288,7 @@ window.PageEventsDisplay = {
|
|||
|
||||
ws.onmessage = event => {
|
||||
const data = JSON.parse(event.data)
|
||||
if (data.pending === false) {
|
||||
if (data.paid === true) {
|
||||
this.paymentSuccess(paymentHash)
|
||||
ws.close()
|
||||
}
|
||||
|
|
@ -215,8 +297,12 @@ window.PageEventsDisplay = {
|
|||
console.error('WebSocket error:', error)
|
||||
}
|
||||
ws.onclose = () => {
|
||||
if (this.paymentWebsocket === ws) {
|
||||
this.paymentWebsocket = null
|
||||
if (this.paymentWebsocket !== ws) return
|
||||
this.paymentWebsocket = null
|
||||
if (this.receive.show) {
|
||||
setTimeout(() => {
|
||||
if (this.receive.show) this.paymentWatcher(paymentHash)
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue