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>
136 lines
5 KiB
Python
136 lines
5 KiB
Python
"""Per-operator settings (multi-tenant): what a guest sees on a room and how
|
|
the rails are derived. Chatelet never decides *whether* a user may charge
|
|
card — it asks LNbits core per owner — only whether the operator wants to."""
|
|
|
|
import asyncio
|
|
from types import SimpleNamespace
|
|
|
|
from .. import crud, services, transport_rpcs, views_api
|
|
from ..models import OperatorSettings, RoomStatus, UpdateOperatorSettings
|
|
from ..nostr import events
|
|
from .conftest import make_room, patch_owner
|
|
|
|
|
|
def _key(wallet_id="w1", user="u1"):
|
|
return SimpleNamespace(wallet=SimpleNamespace(id=wallet_id, user=user))
|
|
|
|
|
|
def test_rails_need_flag_provider_and_fiat_price(monkeypatch):
|
|
room = make_room(price=100.0, currency="EUR")
|
|
on = OperatorSettings(user_id="u1", accept_fiat=True)
|
|
off = OperatorSettings(user_id="u1", accept_fiat=False)
|
|
lightning_only = ["lightning"]
|
|
|
|
monkeypatch.setattr(services, "fiat_providers_for_user", lambda _u: ["stripe"])
|
|
both = ["lightning", "fiat"]
|
|
assert services.payment_methods_for_room(room, "u1", on) == both
|
|
assert services.payment_methods_for_room(room, "u1", off) == lightning_only
|
|
sat_room = make_room(price=1000.0, currency="sat")
|
|
assert services.payment_methods_for_room(sat_room, "u1", on) == lightning_only
|
|
monkeypatch.setattr(services, "fiat_providers_for_user", lambda _u: [])
|
|
assert services.payment_methods_for_room(room, "u1", on) == lightning_only
|
|
|
|
|
|
def test_public_room_view_attaches_rules_and_rails(monkeypatch):
|
|
patch_owner(monkeypatch, accept_fiat=True, providers=["stripe"])
|
|
room = make_room("a", status=RoomStatus.active, currency="EUR")
|
|
out = asyncio.run(services.public_room_view(room))
|
|
assert out["payment_methods"] == ["lightning", "fiat"]
|
|
assert out["house_rules"] == {
|
|
"checkin_time": "15:00",
|
|
"checkout_time": "11:00",
|
|
"cancellation_policy": "",
|
|
}
|
|
assert "wallet" not in out and "checkin_instructions" not in out
|
|
|
|
|
|
def test_batch_view_looks_owner_up_once_per_wallet(monkeypatch):
|
|
calls: list[str] = []
|
|
|
|
async def get_wallet(wallet_id):
|
|
calls.append(wallet_id)
|
|
return SimpleNamespace(user="u1")
|
|
|
|
async def ops(uid):
|
|
return OperatorSettings(user_id=uid)
|
|
|
|
monkeypatch.setattr(services, "get_wallet", get_wallet)
|
|
monkeypatch.setattr(crud, "get_or_create_operator_settings", ops)
|
|
monkeypatch.setattr(services, "fiat_providers_for_user", lambda _u: [])
|
|
rooms = [
|
|
make_room("a", wallet="w1"),
|
|
make_room("b", wallet="w1"),
|
|
make_room("c", wallet="w2"),
|
|
]
|
|
out = asyncio.run(services.public_room_views(rooms))
|
|
assert [r["id"] for r in out] == ["a", "b", "c"]
|
|
assert sorted(calls) == ["w1", "w2"]
|
|
|
|
|
|
def test_listing_event_carries_rails_and_times():
|
|
room = make_room("a")
|
|
ev = events.build_listing_event(
|
|
room,
|
|
payment_methods=["lightning", "fiat"],
|
|
house_rules={
|
|
"checkin_time": "16:00",
|
|
"checkout_time": "10:00",
|
|
"cancellation_policy": "x",
|
|
},
|
|
)
|
|
wanted = ("payment_methods", "checkin_time", "checkout_time")
|
|
tags = {t[0]: t[1:] for t in ev["tags"] if t[0] in wanted}
|
|
assert tags == {
|
|
"payment_methods": ["lightning,fiat"],
|
|
"checkin_time": ["16:00"],
|
|
"checkout_time": ["10:00"],
|
|
}
|
|
# long-form policy text stays off the listing tags
|
|
assert not any(t[0] == "cancellation_policy" for t in ev["tags"])
|
|
|
|
|
|
def test_operator_endpoints_scope_to_calling_user(monkeypatch):
|
|
store: dict[str, OperatorSettings] = {}
|
|
|
|
async def get_or_create(uid):
|
|
return store.setdefault(uid, OperatorSettings(user_id=uid))
|
|
|
|
async def update(ops):
|
|
store[ops.user_id] = ops
|
|
return ops
|
|
|
|
async def no_rooms():
|
|
return []
|
|
|
|
monkeypatch.setattr(crud, "get_or_create_operator_settings", get_or_create)
|
|
monkeypatch.setattr(crud, "update_operator_settings", update)
|
|
monkeypatch.setattr(crud, "get_rooms", no_rooms)
|
|
monkeypatch.setattr(
|
|
services, "fiat_providers_for_user", lambda u: ["stripe"] if u == "u1" else []
|
|
)
|
|
|
|
first = asyncio.run(views_api.api_get_operator_settings(key=_key(user="u1")))
|
|
assert first["user_id"] == "u1" and first["accept_fiat"] is False
|
|
assert first["available_fiat_providers"] == ["stripe"]
|
|
|
|
updated = asyncio.run(
|
|
views_api.api_update_operator_settings(
|
|
UpdateOperatorSettings(accept_fiat=True, checkin_time="16:00"),
|
|
key=_key(user="u1"),
|
|
)
|
|
)
|
|
assert updated["accept_fiat"] is True and updated["checkin_time"] == "16:00"
|
|
|
|
other = asyncio.run(views_api.api_get_operator_settings(key=_key(user="u2")))
|
|
assert other["accept_fiat"] is False and other["available_fiat_providers"] == []
|
|
|
|
# RPC twin reads the same row for the same wallet user
|
|
rpc = asyncio.run(
|
|
transport_rpcs.handle_operator_get(
|
|
_key(user="u1"),
|
|
transport_rpcs.NostrRpcRequest(
|
|
rpc_name="chatelet_operator_get", request_id="r", body={}
|
|
),
|
|
)
|
|
)
|
|
assert rpc["accept_fiat"] is True and rpc["checkin_time"] == "16:00"
|