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>
110 lines
5.5 KiB
Markdown
110 lines
5.5 KiB
Markdown
# Chatelet — Data Model
|
||
|
||
Tables live in `ext_chatelet.sqlite3` (SQLite) or the `chatelet` Postgres
|
||
schema. Defined in [`../migrations.py`](../migrations.py); typed in
|
||
[`../models.py`](../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`](event-flow.md) § Concurrency for the check-then-hold
|
||
locking requirement.
|