Compare commits
No commits in common. "09c81c377b7868762a0bb4230358eb6cc89f6236" and "317a4748162a2f596a6babdf64f38c1637212fed" have entirely different histories.
09c81c377b
...
317a474816
5 changed files with 2 additions and 166 deletions
|
|
@ -115,12 +115,7 @@ class CreateEvent(BaseModel):
|
||||||
currency: str = "sat"
|
currency: str = "sat"
|
||||||
allow_fiat: bool = False
|
allow_fiat: bool = False
|
||||||
fiat_currency: str = "GBP"
|
fiat_currency: str = "GBP"
|
||||||
# Capacity is always required and there is no unlimited (#34): a zero
|
amount_tickets: int = 0 # 0 = unlimited / not ticketed
|
||||||
# here means sold out / not sellable, which `api_get_event` and
|
|
||||||
# `api_ticket_create` both enforce with a 410. Under v1.6.8 waves this
|
|
||||||
# is only the seed for the primary wave — `sync_event_ticket_waves`
|
|
||||||
# recomputes it as the sum of every wave's remaining stock.
|
|
||||||
amount_tickets: int = 0
|
|
||||||
price_per_ticket: float = 0 # 0 = free
|
price_per_ticket: float = 0 # 0 = free
|
||||||
banner: str | None = None
|
banner: str | None = None
|
||||||
location: str | None = None # venue/address (NIP-52 'location' tag)
|
location: str | None = None # venue/address (NIP-52 'location' tag)
|
||||||
|
|
|
||||||
|
|
@ -901,11 +901,7 @@ window.PageEvents = {
|
||||||
primaryWave.fiat_currency ||
|
primaryWave.fiat_currency ||
|
||||||
event.fiat_currency ||
|
event.fiat_currency ||
|
||||||
'GBP',
|
'GBP',
|
||||||
// `??` not `||`: a sold-out wave legitimately holds 0 and must
|
amount_tickets: wave?.amount_tickets || 0,
|
||||||
// show it rather than silently regaining stock on save. A NEW
|
|
||||||
// wave opens at 1, the smallest capacity the backend accepts —
|
|
||||||
// there is no unlimited (#34).
|
|
||||||
amount_tickets: isEditing ? (wave?.amount_tickets ?? 0) : 1,
|
|
||||||
price_per_ticket:
|
price_per_ticket:
|
||||||
wave?.price_per_ticket ||
|
wave?.price_per_ticket ||
|
||||||
primaryWave.price_per_ticket ||
|
primaryWave.price_per_ticket ||
|
||||||
|
|
|
||||||
|
|
@ -1094,9 +1094,7 @@
|
||||||
dense
|
dense
|
||||||
v-model.number="ticketWaveDialog.data.amount_tickets"
|
v-model.number="ticketWaveDialog.data.amount_tickets"
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
|
||||||
label="Amount of tickets"
|
label="Amount of tickets"
|
||||||
hint="Tickets on sale in this wave"
|
|
||||||
></q-input>
|
></q-input>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
|
|
|
||||||
|
|
@ -1,118 +0,0 @@
|
||||||
"""Capacity is required per ticket wave (#34, #62).
|
|
||||||
|
|
||||||
"Capacity is always required, there is no unlimited" was settled when
|
|
||||||
capacity was one number on the event. Since v1.6.8 it is per-wave and
|
|
||||||
`event.amount_tickets` is a derived roll-up, so the rule has to bite where
|
|
||||||
the organiser sets it. A zero-capacity wave can never be active
|
|
||||||
(`get_active_ticket_waves` requires `> 0`), so it is the wave-level form of
|
|
||||||
the trap #62 removed: an event that looks on sale but refuses every
|
|
||||||
purchase.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from datetime import datetime, timedelta, timezone
|
|
||||||
from http import HTTPStatus
|
|
||||||
|
|
||||||
import pytest
|
|
||||||
from fastapi import HTTPException
|
|
||||||
|
|
||||||
from ..models import CreateEvent, Event, EventExtra, TicketWave
|
|
||||||
from ..views_api import _validate_wave_capacity
|
|
||||||
|
|
||||||
TODAY = datetime.now(timezone.utc).date()
|
|
||||||
|
|
||||||
|
|
||||||
def _day(offset: int) -> str:
|
|
||||||
return (TODAY + timedelta(days=offset)).isoformat()
|
|
||||||
|
|
||||||
|
|
||||||
def _wave(wave_id: str, stock: int) -> TicketWave:
|
|
||||||
return TicketWave(
|
|
||||||
id=wave_id,
|
|
||||||
title=wave_id,
|
|
||||||
opening_date=_day(0),
|
|
||||||
closing_date=_day(20),
|
|
||||||
currency="sat",
|
|
||||||
price_per_ticket=10,
|
|
||||||
amount_tickets=stock,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _create(waves=None, amount_tickets=10) -> CreateEvent:
|
|
||||||
return CreateEvent(
|
|
||||||
wallet="w",
|
|
||||||
name="Fete",
|
|
||||||
info="",
|
|
||||||
event_start_date=_day(30),
|
|
||||||
currency="sat",
|
|
||||||
price_per_ticket=10,
|
|
||||||
amount_tickets=amount_tickets,
|
|
||||||
extra=EventExtra(ticket_waves=waves or []),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _stored(waves) -> Event:
|
|
||||||
return Event(
|
|
||||||
id="evt",
|
|
||||||
wallet="w",
|
|
||||||
name="Fete",
|
|
||||||
info="",
|
|
||||||
closing_date=_day(30),
|
|
||||||
event_start_date=_day(30),
|
|
||||||
currency="sat",
|
|
||||||
price_per_ticket=10,
|
|
||||||
amount_tickets=sum(w.amount_tickets for w in waves),
|
|
||||||
time=datetime.now(timezone.utc),
|
|
||||||
extra=EventExtra(ticket_waves=waves),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _detail(exc_info) -> str:
|
|
||||||
assert exc_info.value.status_code == HTTPStatus.BAD_REQUEST
|
|
||||||
return exc_info.value.detail
|
|
||||||
|
|
||||||
|
|
||||||
def test_wave_with_capacity_is_accepted():
|
|
||||||
_validate_wave_capacity(_create([_wave("early", 5), _wave("late", 10)]))
|
|
||||||
|
|
||||||
|
|
||||||
def test_zero_capacity_wave_is_rejected_on_create():
|
|
||||||
with pytest.raises(HTTPException) as exc_info:
|
|
||||||
_validate_wave_capacity(_create([_wave("early", 5), _wave("late", 0)]))
|
|
||||||
assert "late" in _detail(exc_info)
|
|
||||||
|
|
||||||
|
|
||||||
def test_event_submitted_without_waves_is_checked_through_its_primary_wave():
|
|
||||||
"""No waves means the event gets a synthesized primary wave seeded from
|
|
||||||
`amount_tickets`, so the rule must see that rather than an empty list."""
|
|
||||||
_validate_wave_capacity(_create(waves=None, amount_tickets=10))
|
|
||||||
|
|
||||||
with pytest.raises(HTTPException) as exc_info:
|
|
||||||
_validate_wave_capacity(_create(waves=None, amount_tickets=0))
|
|
||||||
assert "Primary wave" in _detail(exc_info)
|
|
||||||
|
|
||||||
|
|
||||||
def test_selling_a_wave_out_does_not_block_later_edits():
|
|
||||||
"""Zero is where a wave legitimately ends up. Rejecting it on edit would
|
|
||||||
make a sold-out event uneditable."""
|
|
||||||
sold_out = _wave("early", 0)
|
|
||||||
existing = _stored([sold_out, _wave("late", 10)])
|
|
||||||
|
|
||||||
_validate_wave_capacity(_create([sold_out, _wave("late", 10)]), existing)
|
|
||||||
|
|
||||||
|
|
||||||
def test_new_wave_added_by_an_edit_still_needs_capacity():
|
|
||||||
existing = _stored([_wave("early", 5)])
|
|
||||||
|
|
||||||
with pytest.raises(HTTPException) as exc_info:
|
|
||||||
_validate_wave_capacity(
|
|
||||||
_create([_wave("early", 5), _wave("brand-new", 0)]), existing
|
|
||||||
)
|
|
||||||
assert "brand-new" in _detail(exc_info)
|
|
||||||
|
|
||||||
|
|
||||||
def test_legacy_zero_capacity_event_stays_editable():
|
|
||||||
"""An event stored before the rule existed keeps its primary wave id, so
|
|
||||||
an edit is not blocked — otherwise the only way to fix it would be
|
|
||||||
barred."""
|
|
||||||
existing = _stored([_wave("primary", 0)])
|
|
||||||
_validate_wave_capacity(_create([_wave("primary", 0)]), existing)
|
|
||||||
35
views_api.py
35
views_api.py
|
|
@ -328,37 +328,6 @@ async def api_get_event(event_id: str) -> Event:
|
||||||
return event
|
return event
|
||||||
|
|
||||||
|
|
||||||
def _validate_wave_capacity(data: CreateEvent, existing: Event | None = None) -> None:
|
|
||||||
"""Every ticket wave must state a real capacity.
|
|
||||||
|
|
||||||
"Capacity is always required, there is no unlimited" (#34, #62) was
|
|
||||||
settled when capacity was a single number on the event. Since v1.6.8 it
|
|
||||||
is per-wave and `event.amount_tickets` is a derived roll-up, so the rule
|
|
||||||
has to be enforced where the organiser actually sets it — otherwise it
|
|
||||||
only survives as a `min="1"` on one HTML input, which no API client is
|
|
||||||
bound by.
|
|
||||||
|
|
||||||
A zero-capacity wave can never be active (`get_active_ticket_waves`
|
|
||||||
requires `amount_tickets > 0`), so it is the wave-level form of exactly
|
|
||||||
what #62 removed: an event that looks on sale but refuses every
|
|
||||||
purchase.
|
|
||||||
|
|
||||||
Selling out is the one legitimate route to zero, so an edit only checks
|
|
||||||
waves that are NEW to the event; waves already stored keep whatever
|
|
||||||
sales decremented them to. `ensure_ticket_waves` is used rather than the
|
|
||||||
raw list so an event submitted with no waves is checked through the
|
|
||||||
primary wave it will be given.
|
|
||||||
"""
|
|
||||||
known = {wave.id for wave in (existing.extra.ticket_waves if existing else [])}
|
|
||||||
for wave in ensure_ticket_waves(data):
|
|
||||||
if wave.id in known or wave.amount_tickets >= 1:
|
|
||||||
continue
|
|
||||||
raise HTTPException(
|
|
||||||
status_code=HTTPStatus.BAD_REQUEST,
|
|
||||||
detail=f"Ticket wave '{wave.title}' needs a capacity of at least 1.",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@events_api_router.post("")
|
@events_api_router.post("")
|
||||||
async def api_event_create(
|
async def api_event_create(
|
||||||
data: CreateEvent,
|
data: CreateEvent,
|
||||||
|
|
@ -372,8 +341,6 @@ async def api_event_create(
|
||||||
if not data.wallet:
|
if not data.wallet:
|
||||||
data.wallet = wallet.wallet.id
|
data.wallet = wallet.wallet.id
|
||||||
|
|
||||||
_validate_wave_capacity(data)
|
|
||||||
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
ext_settings = await get_settings()
|
ext_settings = await get_settings()
|
||||||
|
|
@ -416,8 +383,6 @@ 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.")
|
||||||
|
|
||||||
_validate_wave_capacity(data, event)
|
|
||||||
|
|
||||||
from lnbits.settings import settings
|
from lnbits.settings import settings
|
||||||
|
|
||||||
ext_settings = await get_settings()
|
ext_settings = await get_settings()
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue