diff --git a/migrations.py b/migrations.py new file mode 100644 index 0000000..8f32e80 --- /dev/null +++ b/migrations.py @@ -0,0 +1,113 @@ +"""Chatelet schema migrations. + +This is a brand-new aiolabs-original extension (not a fork of an upstream +LNbits extension), so there is no upstream migrations.py to stay byte- +identical with — we use ordinary migrations here and do NOT need the +migrations_fork.py split (that pattern only earns its keep on forks that +rebase onto upstream). Every migration is still written idempotently so a +partial/re-run boot can't wedge the schema. + +Tables live in ext_chatelet.sqlite3 (SQLite) or the `chatelet` Postgres +schema. Availability is intentionally NOT a table — it is derived from +`bookings` + `blocks` at query time (see crud.is_available). +""" + + +async def m001_initial(db): + """Settings, rooms, bookings, blocks.""" + + # One row: castle/operator-wide config. + await db.execute( + f""" + CREATE TABLE chatelet.settings ( + operator_id TEXT, + relays TEXT NOT NULL DEFAULT '[]', + default_hold_minutes INTEGER NOT NULL DEFAULT 30, + deposit_percent INTEGER NOT NULL DEFAULT 100, + checkin_time TEXT NOT NULL DEFAULT '15:00', + checkout_time TEXT NOT NULL DEFAULT '11:00', + cancellation_policy TEXT NOT NULL DEFAULT '', + publish_availability BOOLEAN NOT NULL DEFAULT true, + created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}, + updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} + ); + """ + ) + + # Rooms == NIP-99 kind:30402 listings. `id` doubles as the listing "d" tag. + await db.execute( + f""" + CREATE TABLE chatelet.rooms ( + id TEXT PRIMARY KEY, + wallet TEXT NOT NULL, + title TEXT NOT NULL, + description TEXT NOT NULL DEFAULT '', + price_amount REAL NOT NULL, + price_currency TEXT NOT NULL DEFAULT 'EUR', + price_frequency TEXT NOT NULL DEFAULT 'night', + max_guests INTEGER NOT NULL DEFAULT 2, + min_nights INTEGER NOT NULL DEFAULT 1, + amenities TEXT NOT NULL DEFAULT '[]', + location TEXT NOT NULL DEFAULT '', + geohash TEXT NOT NULL DEFAULT '', + images TEXT NOT NULL DEFAULT '[]', + status TEXT NOT NULL DEFAULT 'inactive', + listing_event_id TEXT, + created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}, + updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} + ); + """ + ) + + # Bookings == kind:30078 reservation objects. `amount_sat` is canonical. + await db.execute( + f""" + CREATE TABLE chatelet.bookings ( + id TEXT PRIMARY KEY, + room_id TEXT NOT NULL REFERENCES {db.references_schema}rooms (id), + guest_pubkey TEXT NOT NULL, + guest_contact TEXT, + check_in TEXT NOT NULL, + check_out TEXT NOT NULL, + nights INTEGER NOT NULL, + num_guests INTEGER NOT NULL DEFAULT 1, + currency TEXT NOT NULL, + price_fiat REAL NOT NULL, + amount_sat {db.big_int} NOT NULL, + deposit_sat {db.big_int} NOT NULL, + status TEXT NOT NULL DEFAULT 'held', + payment_hash TEXT, + request_event_id TEXT, + reservation_event_id TEXT, + expires_at TIMESTAMP, + created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now}, + updated_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} + ); + """ + ) + # Overlap checks scan by room + status; index the hot path. + await db.execute( + "CREATE INDEX idx_chatelet_bookings_room_status " + "ON chatelet.bookings (room_id, status);" + ) + await db.execute( + "CREATE INDEX idx_chatelet_bookings_payment_hash " + "ON chatelet.bookings (payment_hash);" + ) + + # Manual owner-side blocks (maintenance, personal use). + await db.execute( + f""" + CREATE TABLE chatelet.blocks ( + id TEXT PRIMARY KEY, + room_id TEXT NOT NULL REFERENCES {db.references_schema}rooms (id), + start_date TEXT NOT NULL, + end_date TEXT NOT NULL, + reason TEXT NOT NULL DEFAULT '', + created_at TIMESTAMP NOT NULL DEFAULT {db.timestamp_now} + ); + """ + ) + await db.execute( + "CREATE INDEX idx_chatelet_blocks_room ON chatelet.blocks (room_id);" + )