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:
parent
feaaef93e3
commit
9ab52f2c69
17 changed files with 531 additions and 42 deletions
90
services.py
90
services.py
|
|
@ -16,21 +16,27 @@ import asyncio
|
|||
from collections import defaultdict
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
|
||||
from lnbits.core.crud.wallets import get_wallet
|
||||
from lnbits.core.services import create_invoice
|
||||
from lnbits.exceptions import InvoiceError
|
||||
from lnbits.helpers import urlsafe_short_hash
|
||||
from lnbits.settings import settings as lnbits_settings
|
||||
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
|
||||
|
||||
from . import crud
|
||||
from .models import (
|
||||
HOUSE_RULE_FIELDS,
|
||||
AvailabilityResult,
|
||||
Booking,
|
||||
BookingQuote,
|
||||
BookingRequestData,
|
||||
BookingStatus,
|
||||
DateRange,
|
||||
OperatorSettings,
|
||||
Room,
|
||||
RoomStatus,
|
||||
UnavailableRanges,
|
||||
public_room_dict,
|
||||
)
|
||||
|
||||
# Per-room lock serializing the availability read + the `held` write, so two
|
||||
|
|
@ -61,6 +67,90 @@ async def to_sats(amount: float, currency: str) -> int:
|
|||
return await fiat_amount_as_satoshis(amount, currency)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Operators + the public room view
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
LIGHTNING = "lightning"
|
||||
FIAT = "fiat"
|
||||
|
||||
|
||||
def is_fiat_currency(currency: str) -> bool:
|
||||
return currency.lower() not in ("sat", "sats")
|
||||
|
||||
|
||||
def fiat_providers_for_user(user_id: str) -> list[str]:
|
||||
"""Fiat providers LNbits core will let this user charge with. The one
|
||||
place chatelet consults core about card payments (lnbits#67's per-user
|
||||
Stripe creds land behind this call); module-level so tests can patch it —
|
||||
the pydantic settings object refuses monkeypatched methods."""
|
||||
return lnbits_settings.get_fiat_providers_for_user(user_id)
|
||||
|
||||
|
||||
async def room_owner_id(room: Room) -> str:
|
||||
"""The LNbits user who operates a room (rooms belong to wallets)."""
|
||||
wallet = await get_wallet(room.wallet)
|
||||
if not wallet:
|
||||
raise NotFound("Room's wallet not found")
|
||||
return wallet.user
|
||||
|
||||
|
||||
def payment_methods_for_room(
|
||||
room: Room, owner_id: str, ops: OperatorSettings
|
||||
) -> list[str]:
|
||||
"""Rails a guest may pay this room with. Card needs three things: the
|
||||
operator opted in, LNbits core has a fiat provider for *that user* (the
|
||||
single seam lnbits#67's per-user Stripe creds will plug into — chatelet
|
||||
never sees credentials), and a fiat-denominated price (core cannot bill
|
||||
a sat amount through a fiat provider)."""
|
||||
rails = [LIGHTNING]
|
||||
if (
|
||||
ops.accept_fiat
|
||||
and is_fiat_currency(room.price_currency)
|
||||
and fiat_providers_for_user(owner_id)
|
||||
):
|
||||
rails.append(FIAT)
|
||||
return rails
|
||||
|
||||
|
||||
def house_rules_dict(ops: OperatorSettings) -> dict:
|
||||
return {k: getattr(ops, k) for k in HOUSE_RULE_FIELDS}
|
||||
|
||||
|
||||
async def public_room_view(room: Room) -> dict:
|
||||
"""public_room_dict + the owner's house rules and rails. Used by both
|
||||
guest doors so a room looks the same over HTTP and RPC."""
|
||||
owner = await room_owner_id(room)
|
||||
ops = await crud.get_or_create_operator_settings(owner)
|
||||
return public_room_dict(
|
||||
room,
|
||||
house_rules=house_rules_dict(ops),
|
||||
payment_methods=payment_methods_for_room(room, owner, ops),
|
||||
)
|
||||
|
||||
|
||||
async def public_room_views(rooms: list[Room]) -> list[dict]:
|
||||
"""Batch form: one owner/settings lookup per distinct wallet."""
|
||||
owners: dict[str, str] = {}
|
||||
ops_by_owner: dict[str, OperatorSettings] = {}
|
||||
out = []
|
||||
for room in rooms:
|
||||
if room.wallet not in owners:
|
||||
owners[room.wallet] = await room_owner_id(room)
|
||||
owner = owners[room.wallet]
|
||||
if owner not in ops_by_owner:
|
||||
ops_by_owner[owner] = await crud.get_or_create_operator_settings(owner)
|
||||
ops = ops_by_owner[owner]
|
||||
out.append(
|
||||
public_room_dict(
|
||||
room,
|
||||
house_rules=house_rules_dict(ops),
|
||||
payment_methods=payment_methods_for_room(room, owner, ops),
|
||||
)
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
# A guest calendar asks for a year by default; cap the window so a bad client
|
||||
# can't make us scan and ship an unbounded span.
|
||||
DEFAULT_CALENDAR_DAYS = 365
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue