Some checks failed
lint.yml / feat(nostr): publish the active ticket wave, not the roll-up (pull_request) Failing after 0s
Since upstream v1.6.8 price, currency and inventory belong to time-boxed ticket waves, and the event-level fields `sync_event_ticket_waves` derives are the PRIMARY wave's price/currency and the SUM of every wave's stock. The NIP-52 publisher read those, so as soon as an organiser created a second wave the public card would advertise the early-bird price after early bird closed and count stock in waves that had not opened. Refs #61. `build_nip52_event` now describes the wave a buyer can actually buy from: - several waves can be open at once, and a publisher has no one to ask which one the buyer wants (the purchase endpoint errors with "Please select a ticket wave"), so it advertises the CHEAPEST open wave — the price a buyer is able to obtain. Deviation recorded in docs/upstream-candidates.md. - with no open wave, `tickets_available` is 0 and never omitted: omission used to mean "unlimited", which #34/#62 removed as a concept. - `tickets_payment_methods` is scoped to the advertised wave too. It was derived from `event.allow_fiat` — the primary wave's — so it could offer a fiat rail while `tickets_allow_fiat` was absent and the purchase endpoint would refuse it. They are the same fact and now come from the same place. Wave boundaries are time-driven, and every republish we have is sale-driven, so nothing fires when early bird ends at midnight. Rather than add a scheduler, a publish records which wave it advertised (`nostr_published_wave_id`, m004) and the reconciliation sweep compares that against the wave that would be advertised now, setting `nostr_publish_pending` on a mismatch — reusing the existing retry path. NULL means "never published", which the sweep leaves alone so an upgrade does not republish the whole table on first boot. The selection rule lives in `models.advertised_ticket_wave` so the publisher and the drift detector cannot disagree about what is on the relay. 17 new tests; 114 pass. ruff, black, prettier clean; mypy error set still identical to HEAD's baseline.
187 lines
7.6 KiB
Python
187 lines
7.6 KiB
Python
"""
|
|
Fork-specific database migrations for the aiolabs events extension.
|
|
|
|
These migrations are tracked separately under `events_fork` in the
|
|
`dbversions` table (loaded by `lnbits/core/helpers.py:migrate_extension_database`),
|
|
so they do not collide with upstream's `m{NNN}_*` numbering in
|
|
`migrations.py`. Keeping the upstream-tracked file untouched means
|
|
`git pull upstream` stays rebase-clean for schema changes.
|
|
|
|
Conventions:
|
|
- Sequential numbering starting from m001.
|
|
- Each migration is `async def m{NNN}_<description>(db)`.
|
|
- DDL must be idempotent: a fresh install runs every migration; an
|
|
install that previously ran the OLD versions of these as
|
|
`m007-m011` in `migrations.py` has the columns/tables already.
|
|
Use `_alter_add_column_safe` / `_create_table_safe` so re-runs are
|
|
no-ops instead of crashes.
|
|
|
|
History compressed into m001 (was m007-m011 in migrations.py pre-v1.6
|
|
rebase):
|
|
- m007 add_user_id_support (ticket.user_id column)
|
|
- m008 add_event_status (events.status column)
|
|
- m009 add_nostr_columns (events.nostr_event_id + created_at)
|
|
- m010 add_events_settings (events.settings singleton table)
|
|
- m011 add_location_and_categories (events.location + categories)
|
|
"""
|
|
|
|
|
|
async def _alter_add_column_safe(db, sql: str) -> None:
|
|
"""ALTER TABLE ADD COLUMN that swallows duplicate-column errors.
|
|
|
|
Re-running the squashed migration on a database that already has
|
|
these columns (from the pre-squash `m007-m011` in migrations.py)
|
|
must be a silent no-op. Same swallow we used in the old migrations.
|
|
"""
|
|
try:
|
|
await db.execute(sql)
|
|
except Exception as exc:
|
|
msg = str(exc).lower()
|
|
if "duplicate column" in msg or "already exists" in msg:
|
|
return
|
|
raise
|
|
|
|
|
|
async def m001_aio_event_schema(db):
|
|
"""
|
|
Apply every aiolabs schema delta on top of upstream events v1.3.0.
|
|
|
|
This is the squashed equivalent of the pre-v1.6 sequence
|
|
m007 → m011. Order matters for the settings table seed insert
|
|
but the individual column adds are independent and idempotent.
|
|
"""
|
|
|
|
# --- ticket.user_id ----------------------------------------------
|
|
# Lets a ticket reference an LNbits user id instead of (name, email).
|
|
# Application logic enforces that exactly one identifier scheme is
|
|
# used per ticket.
|
|
await _alter_add_column_safe(
|
|
db, "ALTER TABLE events.ticket ADD COLUMN user_id TEXT"
|
|
)
|
|
|
|
# --- events.status -----------------------------------------------
|
|
# Proposal / approval workflow. Existing rows default to 'approved'
|
|
# so they stay visible after upgrade.
|
|
await _alter_add_column_safe(
|
|
db,
|
|
"ALTER TABLE events.events ADD COLUMN status TEXT NOT NULL DEFAULT 'approved'",
|
|
)
|
|
|
|
# --- events.nostr_event_id, nostr_event_created_at ---------------
|
|
# Track the most recent NIP-52 calendar event we published, so
|
|
# subsequent edits can issue replaceable updates and NIP-09 deletes
|
|
# against the right addressable coordinate.
|
|
await _alter_add_column_safe(
|
|
db, "ALTER TABLE events.events ADD COLUMN nostr_event_id TEXT"
|
|
)
|
|
await _alter_add_column_safe(
|
|
db, "ALTER TABLE events.events ADD COLUMN nostr_event_created_at INTEGER"
|
|
)
|
|
|
|
# --- events.settings ---------------------------------------------
|
|
# Singleton settings row used by the admin UI to toggle e.g.
|
|
# auto_approve. CREATE TABLE IF NOT EXISTS + a guarded seed keeps
|
|
# this idempotent.
|
|
await db.execute("""
|
|
CREATE TABLE IF NOT EXISTS events.settings (
|
|
id INTEGER PRIMARY KEY DEFAULT 1,
|
|
auto_approve BOOLEAN NOT NULL DEFAULT FALSE
|
|
)
|
|
""")
|
|
await db.execute(
|
|
"INSERT INTO events.settings (id, auto_approve) "
|
|
"SELECT 1, FALSE WHERE NOT EXISTS "
|
|
"(SELECT 1 FROM events.settings WHERE id = 1)"
|
|
)
|
|
|
|
# --- events.location, events.categories --------------------------
|
|
# NIP-52 calendar metadata. `categories` carries a JSON-encoded
|
|
# list of hashtags (the NIP-52 `t` tags).
|
|
await _alter_add_column_safe(
|
|
db, "ALTER TABLE events.events ADD COLUMN location TEXT"
|
|
)
|
|
await _alter_add_column_safe(
|
|
db, "ALTER TABLE events.events ADD COLUMN categories TEXT"
|
|
)
|
|
|
|
|
|
async def m002_ticket_payment_hash(db):
|
|
"""
|
|
Add `ticket.payment_hash` for multi-ticket purchases.
|
|
|
|
Multi-ticket purchases land as N rows sharing one LNbits invoice
|
|
(so each attendee gets a distinct scannable QR but the buyer
|
|
pays once). `ticket.id` stays the row primary key — for legacy
|
|
single-purchase rows it equals payment_hash; for multi-purchase
|
|
children it's a uuid generated at create-time. `payment_hash`
|
|
is the new join key for invoice lookup.
|
|
|
|
Backfill existing rows from id so the
|
|
GET-tickets-by-payment-hash path keeps working for pre-migration
|
|
data (id was the payment_hash by invariant before this column).
|
|
"""
|
|
await _alter_add_column_safe(
|
|
db, "ALTER TABLE events.ticket ADD COLUMN payment_hash TEXT"
|
|
)
|
|
await db.execute(
|
|
"UPDATE events.ticket SET payment_hash = id "
|
|
"WHERE payment_hash IS NULL OR payment_hash = ''"
|
|
)
|
|
|
|
|
|
async def m003_event_nostr_publish_pending(db):
|
|
"""
|
|
Add `events.nostr_publish_pending` — the marker that makes NIP-52
|
|
publish drift queryable instead of invisible.
|
|
|
|
Inventory reaches clients only through the republished calendar
|
|
event. When that publish doesn't land, the relay keeps serving the
|
|
counts it last saw and nothing anywhere records the divergence; it
|
|
has twice been caught only by a human reading a public page
|
|
(aiolabs/events#35, #51).
|
|
|
|
The flag is set before each publish attempt and cleared only on a
|
|
confirmed success, so it covers *both* observed failure shapes:
|
|
an attempt that raised (a signer outage) and an attempt that was
|
|
never made at all (no signer resolved, no NostrClient). A periodic
|
|
sweep republishes whatever is still marked.
|
|
|
|
Existing rows default to FALSE rather than TRUE: on upgrade we have
|
|
no evidence they're stale, and marking the whole table pending would
|
|
stampede the signer with a full-table republish on first boot.
|
|
`/republish-all` is the deliberate way to force that.
|
|
"""
|
|
await _alter_add_column_safe(
|
|
db,
|
|
"ALTER TABLE events.events "
|
|
"ADD COLUMN nostr_publish_pending BOOLEAN NOT NULL DEFAULT FALSE",
|
|
)
|
|
|
|
|
|
async def m004_event_nostr_published_wave(db):
|
|
"""
|
|
Add `events.nostr_published_wave_id` — which ticket wave the last
|
|
successful NIP-52 publish advertised.
|
|
|
|
Since upstream v1.6.8 price and inventory live on time-boxed waves, so
|
|
what a calendar event should advertise changes at a *date* boundary.
|
|
Every republish we have is sale-driven, and no sale happens at
|
|
midnight when early bird ends — so without this the relay keeps
|
|
serving the closed wave's price until the next ticket sells
|
|
(aiolabs/events#61).
|
|
|
|
Recording the advertised wave makes that drift detectable with the
|
|
machinery already in place: the reconciliation sweep compares this
|
|
against the wave that would be advertised now and sets
|
|
`nostr_publish_pending`, reusing the existing retry path rather than
|
|
adding a scheduler.
|
|
|
|
NULL on existing rows means "never published, or published before this
|
|
column existed". The sweep treats NULL as "no evidence of drift" and
|
|
leaves it alone, so an upgrade does not stampede the signer with a
|
|
full-table republish; the first ordinary publish fills it in.
|
|
"""
|
|
await _alter_add_column_safe(
|
|
db,
|
|
"ALTER TABLE events.events ADD COLUMN nostr_published_wave_id TEXT",
|
|
)
|