diff --git a/models.py b/models.py index 1768b44..92dcd84 100644 --- a/models.py +++ b/models.py @@ -115,12 +115,7 @@ class CreateEvent(BaseModel): currency: str = "sat" allow_fiat: bool = False fiat_currency: str = "GBP" - # Capacity is always required and there is no unlimited (#34): a zero - # 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 + amount_tickets: int = 0 # 0 = unlimited / not ticketed price_per_ticket: float = 0 # 0 = free banner: str | None = None location: str | None = None # venue/address (NIP-52 'location' tag) diff --git a/static/js/index.js b/static/js/index.js index edddaba..f702a4e 100644 --- a/static/js/index.js +++ b/static/js/index.js @@ -901,11 +901,7 @@ window.PageEvents = { primaryWave.fiat_currency || event.fiat_currency || 'GBP', - // `??` not `||`: a sold-out wave legitimately holds 0 and must - // 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, + amount_tickets: wave?.amount_tickets || 0, price_per_ticket: wave?.price_per_ticket || primaryWave.price_per_ticket || diff --git a/static/js/index.vue b/static/js/index.vue index 3ffe9bb..a52d056 100644 --- a/static/js/index.vue +++ b/static/js/index.vue @@ -1094,9 +1094,7 @@ dense v-model.number="ticketWaveDialog.data.amount_tickets" type="number" - min="1" label="Amount of tickets" - hint="Tickets on sale in this wave" >
diff --git a/tests/test_wave_capacity_required.py b/tests/test_wave_capacity_required.py deleted file mode 100644 index 5f40c4c..0000000 --- a/tests/test_wave_capacity_required.py +++ /dev/null @@ -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) diff --git a/views_api.py b/views_api.py index 91a03f3..8a79af9 100644 --- a/views_api.py +++ b/views_api.py @@ -328,37 +328,6 @@ async def api_get_event(event_id: str) -> 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("") async def api_event_create( data: CreateEvent, @@ -372,8 +341,6 @@ async def api_event_create( if not data.wallet: data.wallet = wallet.wallet.id - _validate_wave_capacity(data) - from lnbits.settings import settings ext_settings = await get_settings() @@ -416,8 +383,6 @@ async def api_event_update( if event.wallet != wallet.wallet.id: raise HTTPException(status_code=HTTPStatus.FORBIDDEN, detail="Not your event.") - _validate_wave_capacity(data, event) - from lnbits.settings import settings ext_settings = await get_settings()