Compare commits
11 commits
v1.6.8-aio
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| cadf30b704 | |||
| 0e9cc76fef | |||
| b4ca9fe65d | |||
| 69c9a4f757 | |||
| 33501dfc90 | |||
| b55d6866d6 | |||
| fe40d2ab14 | |||
| 26c0a5c429 | |||
| e706b46003 | |||
| 209a895415 | |||
| 064795c62a |
8 changed files with 327 additions and 25 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"id": "events",
|
"id": "events",
|
||||||
"version": "1.6.8-aio.1",
|
"version": "1.6.8-aio.4",
|
||||||
"name": "Events",
|
"name": "Events",
|
||||||
"repo": "https://git.atitlan.io/aiolabs/events",
|
"repo": "https://git.atitlan.io/aiolabs/events",
|
||||||
"short_description": "Sell and register event tickets",
|
"short_description": "Sell and register event tickets",
|
||||||
|
|
|
||||||
30
models.py
30
models.py
|
|
@ -58,7 +58,8 @@ class TicketWave(BaseModel):
|
||||||
|
|
||||||
|
|
||||||
class EventExtraBase(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,
|
the organizer-only promo codes on top; `PublicEventExtra` is this base,
|
||||||
so anonymous responses can never carry them."""
|
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
|
# `effective_payment_methods`. Same field name/shape as upstream v2 so the
|
||||||
# eventual rebase (#33) merges cleanly.
|
# eventual rebase (#33) merges cleanly.
|
||||||
payment_methods: list[str] = Field(default_factory=list)
|
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)
|
@validator("payment_methods", pre=True)
|
||||||
def normalize_payment_methods(cls, v):
|
def normalize_payment_methods(cls, v):
|
||||||
|
|
@ -92,12 +106,6 @@ class EventExtraBase(BaseModel):
|
||||||
|
|
||||||
class EventExtra(EventExtraBase):
|
class EventExtra(EventExtraBase):
|
||||||
promo_codes: list[PromoCode] = Field(default_factory=list)
|
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
|
PublicEventExtra = EventExtraBase
|
||||||
|
|
@ -216,6 +224,14 @@ def effective_payment_methods(
|
||||||
"""
|
"""
|
||||||
explicit = list(getattr(event.extra, "payment_methods", []) or [])
|
explicit = list(getattr(event.extra, "payment_methods", []) or [])
|
||||||
if explicit:
|
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
|
return explicit
|
||||||
methods = ["lightning"]
|
methods = ["lightning"]
|
||||||
if wave.allow_fiat if wave is not None else event.allow_fiat:
|
if wave.allow_fiat if wave is not None else event.allow_fiat:
|
||||||
|
|
|
||||||
|
|
@ -414,6 +414,26 @@ window.PageEvents = {
|
||||||
this.ticketImageUploadTarget = null
|
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) {
|
soldTicketsForWave(eventId, waveId) {
|
||||||
return this.allPaidTickets.filter(
|
return this.allPaidTickets.filter(
|
||||||
ticket =>
|
ticket =>
|
||||||
|
|
|
||||||
|
|
@ -271,23 +271,7 @@
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
style="white-space: normal; line-height: 1.3"
|
style="white-space: normal; line-height: 1.3"
|
||||||
v-text="
|
v-text="waveSummary(props.row.id, wave)"
|
||||||
`${wave.title} - ${wave.opening_date} to ${
|
|
||||||
wave.closing_date
|
|
||||||
} - ${
|
|
||||||
isFiatCurrency(wave.currency)
|
|
||||||
? LNbits.utils.formatCurrency(
|
|
||||||
Number(
|
|
||||||
wave.price_per_ticket || 0
|
|
||||||
).toFixed(2),
|
|
||||||
wave.currency
|
|
||||||
)
|
|
||||||
: `${wave.price_per_ticket} sats`
|
|
||||||
} - ${wave.amount_tickets} tickets - ${soldTicketsForWave(
|
|
||||||
props.row.id,
|
|
||||||
wave.id
|
|
||||||
)} sold`
|
|
||||||
"
|
|
||||||
></span>
|
></span>
|
||||||
</q-chip>
|
</q-chip>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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
|
# Published while nothing is on sale — a real state, distinct from the
|
||||||
# NULL that means "never published".
|
# NULL that means "never published".
|
||||||
assert advertised_wave_key(_event([CLOSED_EARLY_BIRD])) == ""
|
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"
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,10 @@ from ..models import (
|
||||||
CreateEvent,
|
CreateEvent,
|
||||||
CreateTicket,
|
CreateTicket,
|
||||||
EventExtra,
|
EventExtra,
|
||||||
|
PromoCode,
|
||||||
|
PublicEvent,
|
||||||
|
PublicEventExtra,
|
||||||
|
TicketWave,
|
||||||
effective_payment_methods,
|
effective_payment_methods,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -108,3 +112,108 @@ def test_payment_methods_are_normalised_and_deduplicated():
|
||||||
def test_unknown_payment_method_is_rejected():
|
def test_unknown_payment_method_is_rejected():
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
EventExtra(payment_methods=["cash"])
|
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"]
|
||||||
|
|
||||||
|
|
||||||
|
# --- 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)) == []
|
||||||
|
|
|
||||||
124
tests/test_wave_preservation_on_edit.py
Normal file
124
tests/test_wave_preservation_on_edit.py
Normal file
|
|
@ -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"
|
||||||
14
views_api.py
14
views_api.py
|
|
@ -416,6 +416,20 @@ async def api_event_update(
|
||||||
if event.wallet != wallet.wallet.id:
|
if event.wallet != wallet.wallet.id:
|
||||||
raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your event.")
|
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)
|
_validate_wave_capacity(data, event)
|
||||||
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue