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>
134 lines
4 KiB
Python
134 lines
4 KiB
Python
"""Public guest discovery endpoints + the operator-private field strip
|
|
(privacy: check-in instructions must never reach a guest)."""
|
|
|
|
import asyncio
|
|
from typing import Any
|
|
|
|
import pytest
|
|
from fastapi import HTTPException
|
|
|
|
from .. import crud, views_api
|
|
from ..models import (
|
|
Booking,
|
|
BookingStatus,
|
|
RoomStatus,
|
|
public_booking_dict,
|
|
public_room_dict,
|
|
)
|
|
from .conftest import make_room, patch_owner
|
|
|
|
|
|
def test_public_room_dict_strips_private_fields():
|
|
room = make_room(wallet="w1")
|
|
room.checkin_instructions = "gate code 4213, door on the left"
|
|
d = public_room_dict(room)
|
|
assert "wallet" not in d # operator-internal
|
|
assert "checkin_instructions" not in d # private, DM-only
|
|
assert d["title"] == "Tower Room" # public fields survive
|
|
assert d["price_amount"] == 100
|
|
|
|
|
|
def test_public_rooms_lists_active_only_and_stripped(monkeypatch):
|
|
active = make_room("a", status=RoomStatus.active)
|
|
active.checkin_instructions = "secret"
|
|
inactive = make_room("b", status=RoomStatus.inactive)
|
|
|
|
async def gr():
|
|
return [active, inactive]
|
|
|
|
monkeypatch.setattr(crud, "get_rooms", gr)
|
|
patch_owner(monkeypatch)
|
|
out = asyncio.run(views_api.api_public_rooms())
|
|
assert [r["id"] for r in out] == ["a"] # inactive hidden from guests
|
|
assert "checkin_instructions" not in out[0]
|
|
assert "wallet" not in out[0]
|
|
assert out[0]["house_rules"]["checkin_time"] == "15:00"
|
|
assert out[0]["payment_methods"] == ["lightning"]
|
|
|
|
|
|
def test_public_room_404_when_inactive(monkeypatch):
|
|
async def gr(_):
|
|
return make_room(status=RoomStatus.inactive)
|
|
|
|
monkeypatch.setattr(crud, "get_room", gr)
|
|
with pytest.raises(HTTPException) as e:
|
|
asyncio.run(views_api.api_public_room("x"))
|
|
assert e.value.status_code == 404
|
|
|
|
|
|
def test_public_room_returns_stripped_when_active(monkeypatch):
|
|
room = make_room("a", status=RoomStatus.active)
|
|
room.checkin_instructions = "gate"
|
|
|
|
async def gr(_):
|
|
return room
|
|
|
|
monkeypatch.setattr(crud, "get_room", gr)
|
|
patch_owner(monkeypatch)
|
|
out = asyncio.run(views_api.api_public_room("a"))
|
|
assert out["id"] == "a"
|
|
assert "checkin_instructions" not in out
|
|
assert "wallet" not in out
|
|
|
|
|
|
def _booking(**overrides: Any) -> Booking:
|
|
base: dict[str, Any] = {
|
|
"id": "bk_1234567",
|
|
"room_id": "a",
|
|
"guest_pubkey": "ab" * 32,
|
|
"guest_contact": "guest@example.com",
|
|
"check_in": "2026-10-05",
|
|
"check_out": "2026-10-07",
|
|
"nights": 2,
|
|
"num_guests": 1,
|
|
"currency": "EUR",
|
|
"price_fiat": 200.0,
|
|
"amount_sat": 300000,
|
|
"deposit_sat": 300000,
|
|
"status": BookingStatus.awaiting_payment,
|
|
"payment_hash": "ph_1",
|
|
"request_event_id": "req_ev",
|
|
"reservation_event_id": "res_ev",
|
|
}
|
|
base.update(overrides)
|
|
return Booking(**base)
|
|
|
|
|
|
def test_public_booking_dict_strips_identity_and_plumbing():
|
|
d = public_booking_dict(_booking())
|
|
for private in (
|
|
"guest_pubkey",
|
|
"guest_contact",
|
|
"payment_hash",
|
|
"request_event_id",
|
|
"reservation_event_id",
|
|
):
|
|
assert private not in d
|
|
# What a guest client needs to render "waiting" / "booked" / "expired".
|
|
assert d["id"] == "bk_1234567"
|
|
assert d["status"] == "awaiting_payment"
|
|
assert d["check_in"] == "2026-10-05"
|
|
assert d["nights"] == 2
|
|
assert d["deposit_sat"] == 300000
|
|
assert "expires_at" in d
|
|
|
|
|
|
def test_public_booking_404_when_missing(monkeypatch):
|
|
async def gb(_):
|
|
return None
|
|
|
|
monkeypatch.setattr(crud, "get_booking", gb)
|
|
with pytest.raises(HTTPException) as e:
|
|
asyncio.run(views_api.api_public_booking("nope"))
|
|
assert e.value.status_code == 404
|
|
|
|
|
|
def test_public_booking_returns_stripped(monkeypatch):
|
|
async def gb(_):
|
|
return _booking(status=BookingStatus.confirmed)
|
|
|
|
monkeypatch.setattr(crud, "get_booking", gb)
|
|
out = asyncio.run(views_api.api_public_booking("bk_1234567"))
|
|
assert out["status"] == "confirmed"
|
|
assert "guest_contact" not in out
|
|
assert "payment_hash" not in out
|