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>
This commit is contained in:
Padreug 2026-09-16 12:16:14 +02:00
commit 9ab52f2c69
17 changed files with 531 additions and 42 deletions

View file

@ -30,7 +30,13 @@ from lnbits.core.models.wallets import WalletTypeInfo
from lnbits.core.services.nostr_transport.models import NostrRpcRequest
from . import crud, services
from .models import BookingRequestData, CreateBlockData, CreateRoomData, RoomStatus
from .models import (
BookingRequestData,
CreateBlockData,
CreateRoomData,
RoomStatus,
UpdateOperatorSettings,
)
# Fields a client may patch on a room via chatelet_room_update. Identity /
# counter fields (id, wallet, listing_event_id, created_at) are not mutable;
@ -99,20 +105,36 @@ async def handle_block_create(auth: WalletTypeInfo, request: NostrRpcRequest) ->
return _to_dict(block)
async def handle_operator_get(auth: WalletTypeInfo, request: NostrRpcRequest) -> dict:
ops = await crud.get_or_create_operator_settings(auth.wallet.user)
return _to_dict(ops)
async def handle_operator_update(
auth: WalletTypeInfo, request: NostrRpcRequest
) -> dict:
ops = await crud.get_or_create_operator_settings(auth.wallet.user)
for k, v in (request.body or {}).items():
if k in UpdateOperatorSettings.__fields__:
setattr(ops, k, v)
return _to_dict(await crud.update_operator_settings(ops))
# --- public: discovery + booking (AUTH_NONE) -------------------------------
async def handle_room_list(auth: None, request: NostrRpcRequest) -> list[dict]:
"""Active rooms only, wallet id stripped (public discovery)."""
rooms = await crud.get_rooms()
return [_public_room(r) for r in rooms if r.status == RoomStatus.active]
"""Active rooms only, wallet id stripped, owner's house rules + rails
attached (public discovery) — same view as the HTTP door."""
rooms = [r for r in await crud.get_rooms() if r.status == RoomStatus.active]
return await services.public_room_views(rooms)
async def handle_room_get(auth: None, request: NostrRpcRequest) -> dict:
room = await crud.get_room(_require_id(request))
if not room or room.status != RoomStatus.active:
raise ValueError("Room not available")
return _public_room(room)
return await services.public_room_view(room)
async def handle_room_unavailable(auth: None, request: NostrRpcRequest) -> dict:
@ -204,11 +226,3 @@ async def _require_owned_room(room_id: str, wallet_id: str):
def _to_dict(obj) -> dict:
return json.loads(obj.json())
def _public_room(room) -> dict:
# Shared with the HTTP door; strips wallet id AND checkin_instructions
# (the latter was leaking to guests before — added after this file's
# original public dict).
from .models import public_room_dict
return public_room_dict(room)