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
82
crud.py
82
crud.py
|
|
@ -1,10 +1,19 @@
|
|||
import json
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import cast
|
||||
|
||||
from lnbits.db import Database
|
||||
from lnbits.db import Database, Filters, Page
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
|
||||
from .models import CreateEvent, Event, EventsSettings, Ticket, TicketExtra
|
||||
from .models import (
|
||||
CreateEvent,
|
||||
Event,
|
||||
EventsSettings,
|
||||
Ticket,
|
||||
TicketExtra,
|
||||
TicketFilters,
|
||||
sync_event_ticket_waves,
|
||||
)
|
||||
|
||||
db = Database("ext_events")
|
||||
|
||||
|
|
@ -155,6 +164,31 @@ async def get_tickets_by_user_id(user_id: str) -> list[Ticket]:
|
|||
return [Ticket(**_parse_ticket_row(row)) for row in rows]
|
||||
|
||||
|
||||
async def get_tickets_paginated(
|
||||
wallet_ids: str | list[str], filters: Filters[TicketFilters] | None = None
|
||||
) -> Page[Ticket]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
|
||||
wallet_where = []
|
||||
values = {}
|
||||
for idx, wallet_id in enumerate(wallet_ids):
|
||||
key = f"wallet_id_{idx}"
|
||||
wallet_where.append(f":{key}")
|
||||
values[key] = wallet_id
|
||||
|
||||
where = [f"wallet IN ({', '.join(wallet_where)})", "paid = true"]
|
||||
|
||||
return await db.fetch_page(
|
||||
"SELECT * FROM events.ticket",
|
||||
where=where,
|
||||
values=values,
|
||||
filters=filters,
|
||||
model=Ticket,
|
||||
table_name="events.ticket",
|
||||
)
|
||||
|
||||
|
||||
async def delete_ticket(payment_hash: str) -> None:
|
||||
await db.execute("DELETE FROM events.ticket WHERE id = :id", {"id": payment_hash})
|
||||
|
||||
|
|
@ -184,44 +218,55 @@ async def create_event(data: CreateEvent) -> Event:
|
|||
if not data.closing_date:
|
||||
data.closing_date = data.event_end_date
|
||||
event = Event(id=event_id, time=datetime.now(timezone.utc), **data.dict())
|
||||
event = cast(Event, sync_event_ticket_waves(event))
|
||||
await db.insert("events.events", event)
|
||||
return event
|
||||
|
||||
|
||||
async def update_event(event: Event) -> Event:
|
||||
event = cast(Event, sync_event_ticket_waves(event))
|
||||
await db.update("events.events", event)
|
||||
return event
|
||||
|
||||
|
||||
async def get_event(event_id: str) -> Event | None:
|
||||
return await db.fetchone(
|
||||
event = await db.fetchone(
|
||||
"SELECT * FROM events.events WHERE id = :id",
|
||||
{"id": event_id},
|
||||
Event,
|
||||
)
|
||||
return cast(Event, sync_event_ticket_waves(event)) if event else None
|
||||
|
||||
|
||||
async def get_events(wallet_ids: str | list[str]) -> list[Event]:
|
||||
if isinstance(wallet_ids, str):
|
||||
wallet_ids = [wallet_ids]
|
||||
q = ",".join([f"'{wallet_id}'" for wallet_id in wallet_ids])
|
||||
return await db.fetchall(
|
||||
events = await db.fetchall(
|
||||
f"SELECT * FROM events.events WHERE wallet IN ({q})",
|
||||
model=Event,
|
||||
)
|
||||
return [cast(Event, sync_event_ticket_waves(event)) for event in events]
|
||||
|
||||
|
||||
async def get_all_events() -> list[Event]:
|
||||
"""All events, no wallet filter. Admin-only callers."""
|
||||
return await db.fetchall(
|
||||
events = await db.fetchall(
|
||||
"SELECT * FROM events.events ORDER BY time DESC",
|
||||
model=Event,
|
||||
)
|
||||
# Wave-sync on read, exactly as upstream's `get_event` / `get_events`
|
||||
# do. These four getters are fork-only, so upstream's v1.6.8 diff
|
||||
# never reached them — and two of them publish: `get_all_events`
|
||||
# backs /republish-all and `get_events_pending_republish` drives the
|
||||
# #55 sweep. Without this they would emit the stale roll-up rather
|
||||
# than the current per-wave figures.
|
||||
return [cast(Event, sync_event_ticket_waves(e)) for e in events]
|
||||
|
||||
|
||||
async def get_public_events() -> list[Event]:
|
||||
"""Approved, non-canceled events for the public listing."""
|
||||
return await db.fetchall(
|
||||
events = await db.fetchall(
|
||||
"""
|
||||
SELECT * FROM events.events
|
||||
WHERE status = 'approved' AND canceled = FALSE
|
||||
|
|
@ -229,14 +274,28 @@ async def get_public_events() -> list[Event]:
|
|||
""",
|
||||
model=Event,
|
||||
)
|
||||
# Wave-sync on read, exactly as upstream's `get_event` / `get_events`
|
||||
# do. These four getters are fork-only, so upstream's v1.6.8 diff
|
||||
# never reached them — and two of them publish: `get_all_events`
|
||||
# backs /republish-all and `get_events_pending_republish` drives the
|
||||
# #55 sweep. Without this they would emit the stale roll-up rather
|
||||
# than the current per-wave figures.
|
||||
return [cast(Event, sync_event_ticket_waves(e)) for e in events]
|
||||
|
||||
|
||||
async def get_pending_events() -> list[Event]:
|
||||
"""Proposed events awaiting admin approval."""
|
||||
return await db.fetchall(
|
||||
events = await db.fetchall(
|
||||
"SELECT * FROM events.events WHERE status = 'proposed' ORDER BY time DESC",
|
||||
model=Event,
|
||||
)
|
||||
# Wave-sync on read, exactly as upstream's `get_event` / `get_events`
|
||||
# do. These four getters are fork-only, so upstream's v1.6.8 diff
|
||||
# never reached them — and two of them publish: `get_all_events`
|
||||
# backs /republish-all and `get_events_pending_republish` drives the
|
||||
# #55 sweep. Without this they would emit the stale roll-up rather
|
||||
# than the current per-wave figures.
|
||||
return [cast(Event, sync_event_ticket_waves(e)) for e in events]
|
||||
|
||||
|
||||
async def get_events_pending_republish() -> list[Event]:
|
||||
|
|
@ -249,7 +308,7 @@ async def get_events_pending_republish() -> list[Event]:
|
|||
|
||||
Ordered oldest-first so a backlog drains in the order it accrued.
|
||||
"""
|
||||
return await db.fetchall(
|
||||
events = await db.fetchall(
|
||||
"""
|
||||
SELECT * FROM events.events
|
||||
WHERE nostr_publish_pending = TRUE
|
||||
|
|
@ -257,6 +316,13 @@ async def get_events_pending_republish() -> list[Event]:
|
|||
""",
|
||||
model=Event,
|
||||
)
|
||||
# Wave-sync on read, exactly as upstream's `get_event` / `get_events`
|
||||
# do. These four getters are fork-only, so upstream's v1.6.8 diff
|
||||
# never reached them — and two of them publish: `get_all_events`
|
||||
# backs /republish-all and `get_events_pending_republish` drives the
|
||||
# #55 sweep. Without this they would emit the stale roll-up rather
|
||||
# than the current per-wave figures.
|
||||
return [cast(Event, sync_event_ticket_waves(e)) for e in events]
|
||||
|
||||
|
||||
async def get_settings() -> EventsSettings:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue