chatelet/docs/data-model.md
Padreug 9ab52f2c69 feat: per-operator settings — house rules + card acceptance (multi-tenant)
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>
2026-09-16 12:16:14 +02:00

5.5 KiB
Raw Permalink Blame History

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 from price_fiat downstream (FX drifts, rounding accumulates — workspace source-of-truth rule). price_fiat is a display snapshot only.
  • deposit_sat — portion invoiced now (== amount_sat when 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 while held/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.