Chatelet is multi-tenant: any LNbits user can host rooms. What an operator decides for all their rooms now lives in chatelet.operator_settings, keyed by user id and created lazily (m003, which also indexes bookings by guest): check-in/out times, cancellation policy, and accept_fiat. Guests see it: the public room view (both doors) gains house_rules and payment_methods, and the kind:30402 listing carries payment_methods, checkin_time and checkout_time tags so a generic Nostr client can render the right pay buttons and rules without our RPC. The check-in DM reads the room owner's rules instead of the instance row. Card is offered only when the operator opted in, the room is fiat-priced, and LNbits core has a fiat provider for that user — resolved through settings.get_fiat_providers_for_user(owner), the one seam lnbits#67's per-user Stripe credentials will plug into; chatelet never sees creds. Operator endpoints: GET/PUT /api/v1/operator (admin key → wallet user) and RPC twins chatelet_operator_get/update (AUTH_WALLET); saving re-publishes the owner's active listings. Admin UI moves the house-rule inputs into a per-operator card with the card toggle and a provider hint. The old house-rule columns on settings stay for old rows but are no longer read. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
5.5 KiB
Chatelet — Data Model
Tables live in ext_chatelet.sqlite3 (SQLite) or the chatelet Postgres
schema. Defined in ../migrations.py; typed in
../models.py.
Entities
settings (1 row)
│
rooms ─────────────< bookings
│ (guest reservations; amount_sat canonical)
└──────────────< blocks
(manual owner-side unavailability)
settings — one row, castle/operator-wide
| Field | Purpose |
|---|---|
operator_id |
LNbits account whose Nostr signer publishes on the castle's behalf (resolved via resolve_signer — LocalSigner now, NIP-46 bunker later) |
relays |
relays we publish to / subscribe on |
default_hold_minutes |
how long a held booking survives before the sweep expires it |
deposit_percent |
100 = full prepay; <100 = deposit now, balance later |
checkin_time / checkout_time |
surfaced in listing + check-in DM |
cancellation_policy |
free-form markdown |
publish_availability |
mirror blocked dates to a public NIP-52 calendar |
rooms — the rentable unit (one NIP-99 kind:30402 listing each)
id (short hash) doubles as the listing's d tag, so re-publishing
replaces the same addressable event. Holds price (amount/currency/
frequency map straight onto the NIP-99 price tag), capacity
(max_guests, min_nights), presentation (images, amenities→t tags,
location, geohash→g tag), status (active/inactive), and
listing_event_id (last published event id).
bookings — a reservation (one NIP-78 kind:30078 object each)
id doubles as the reservation object's d tag. Key fields:
amount_sat— canonical total. Computed once at hold/quote time from price × nights × FX, then propagated as-is to the invoice, the reservation event, and any receipt. Never re-derived fromprice_fiatdownstream (FX drifts, rounding accumulates — workspace source-of-truth rule).price_fiatis a display snapshot only.deposit_sat— portion invoiced now (== amount_satwhen deposit is 100%).status— lifecycle below.payment_hash— LNbits invoice covering the deposit.request_event_id/reservation_event_id— inbound request and our published reservation object.expires_at— hold expiry (set whileheld/awaiting_payment).
operator_settings — per LNbits user (multi-tenant, m003)
Every LNbits user may host rooms; what they decide for all their rooms lives here, keyed by user id and created on first read:
| Field | Meaning |
|---|---|
accept_fiat |
operator wants card payments. Only offered when LNbits core also has a fiat provider for this user (settings.get_fiat_providers_for_user) and the room is fiat-priced — chatelet never stores provider credentials (lnbits#67 plugs per-user Stripe creds into that same call) |
checkin_time, checkout_time, cancellation_policy |
house rules — shown to guests (house_rules on the public room view, checkin_time/checkout_time tags on the kind:30402 listing) and put in the check-in DM |
Rooms resolve their operator via get_wallet(room.wallet).user. The old
house-rule columns on settings are kept for old rows but no longer read.
blocks — manual owner unavailability
Maintenance, personal use, off-season. Half-open [start_date, end_date).
Booking lifecycle
request
│
▼
┌───────────────────┐ invoice sent ┌────────────────────┐
│ held │ ────────────────▶ │ awaiting_payment │
└───────────────────┘ └────────────────────┘
│ │ │ │
hold │ │ declined paid │ │ hold
expires│ ▼ ▼ │ expires
│ ┌──────────┐ ┌────────────┐ │
└─▶│ expired │ │ confirmed │◀───┘ (paid before expiry)
└──────────┘ └────────────┘
│
check-in │ cancelled
▼
┌────────────┐
│ checked_in │──▶ completed
└────────────┘
OCCUPYING_STATUSES = {held, awaiting_payment, confirmed, checked_in} — the
statuses that block a room's calendar. Terminal negatives (declined,
cancelled, expired) free the dates.
Availability is derived, not stored
There is no availability table. crud.is_available(room, in, out)
returns true iff the room is active and no occupying booking and no
block overlaps [check_in, check_out). Half-open intervals mean
back-to-back stays (one guest out, next guest in, same day) don't collide.
This keeps a single source of truth: the only way to make dates unavailable
is to write a booking or a block. See
event-flow.md § Concurrency for the check-then-hold
locking requirement.