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

110 lines
5.5 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.