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

@ -22,11 +22,12 @@ from .models import (
ChateletSettings,
CreateBlockData,
CreateRoomData,
OperatorSettings,
Room,
RoomStatus,
UnavailableRanges,
UpdateOperatorSettings,
public_booking_dict,
public_room_dict,
)
from .nostr import service as nostr
@ -192,17 +193,51 @@ async def api_update_settings(
return await crud.update_settings(settings)
# --- operator settings (per LNbits user; admin key → wallet → user) ----------
def _with_providers(ops: OperatorSettings) -> dict:
"""The row plus what core would actually let this user charge with, so
the admin UI can explain a card toggle that has no provider behind it."""
d = ops.dict()
d["available_fiat_providers"] = services.fiat_providers_for_user(ops.user_id)
return d
@chatelet_api_router.get("/api/v1/operator")
async def api_get_operator_settings(
key: WalletTypeInfo = Depends(require_admin_key),
) -> dict:
ops = await crud.get_or_create_operator_settings(key.wallet.user)
return _with_providers(ops)
@chatelet_api_router.put("/api/v1/operator")
async def api_update_operator_settings(
data: UpdateOperatorSettings, key: WalletTypeInfo = Depends(require_admin_key)
) -> dict:
ops = await crud.get_or_create_operator_settings(key.wallet.user)
for field in UpdateOperatorSettings.__fields__:
setattr(ops, field, getattr(data, field))
ops = await crud.update_operator_settings(ops)
# Rails/house rules ride on the public listing — refresh the owner's rooms.
for room in await crud.get_rooms():
if room.status == RoomStatus.active:
owner = await services.room_owner_id(room)
if owner == ops.user_id:
await nostr.publish_listing(room)
return _with_providers(ops)
# --- public guest discovery (no auth) --------------------------------------
@chatelet_api_router.get("/api/v1/public/rooms")
async def api_public_rooms() -> list[dict]:
"""Active rooms for guest browsing — operator-private fields stripped."""
return [
public_room_dict(r)
for r in await crud.get_rooms()
if r.status == RoomStatus.active
]
"""Active rooms for guests — wallet + check-in instructions stripped,
owner's house rules + accepted rails attached."""
rooms = [r for r in await crud.get_rooms() if r.status == RoomStatus.active]
return await services.public_room_views(rooms)
@chatelet_api_router.get("/api/v1/public/rooms/{room_id}")
@ -210,7 +245,7 @@ async def api_public_room(room_id: str) -> dict:
room = await crud.get_room(room_id)
if not room or room.status != RoomStatus.active:
raise HTTPException(404, "Room not available")
return public_room_dict(room)
return await services.public_room_view(room)
@chatelet_api_router.get("/api/v1/public/rooms/{room_id}/unavailable")