chatelet/views_api.py
Padreug 5cfc9b893d refactor: extract booking flow into services.py
Move availability quoting + the check-then-hold + invoice orchestration out
of views_api.py into a transport-agnostic services.py, with typed errors
(NotFound/Unavailable/ValueError/BookingError). views_api becomes a thin
HTTP door that maps those to status codes. No behavior change — this is so
the incoming nostr-transport door can share one booking flow instead of
duplicating it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VUQCfdqiLSsFS2jcGnaFD
2026-07-19 01:30:13 +02:00

114 lines
3.6 KiB
Python

"""Chatelet REST API.
The REST surface and the Nostr-transport surface (transport_rpcs.py) are two
doors into the SAME booking flow — both delegate to services.py so
availability arbitration + quoting live in one place. Per the aiolabs
long-term direction (webapp<->lnbits over Nostr), REST is the transitional
door; keep new booking logic in services.py, not here.
"""
from fastapi import APIRouter, Depends, HTTPException
from lnbits.core.models import WalletTypeInfo
from lnbits.decorators import require_admin_key, require_invoice_key
from . import crud, services
from .models import (
AvailabilityQuery,
AvailabilityResult,
Booking,
BookingQuote,
BookingRequestData,
CreateBlockData,
CreateRoomData,
Room,
)
from .nostr import service as nostr
chatelet_api_router = APIRouter()
def _to_http(exc: ValueError) -> HTTPException:
"""Map a services-layer ValueError subclass to an HTTP status."""
if isinstance(exc, services.NotFound):
return HTTPException(404, str(exc))
if isinstance(exc, services.Unavailable):
return HTTPException(409, str(exc))
return HTTPException(400, str(exc))
# --- rooms (operator; admin-key scoped to own wallet) ----------------------
@chatelet_api_router.post("/api/v1/rooms", status_code=201)
async def api_create_room(
data: CreateRoomData, key: WalletTypeInfo = Depends(require_admin_key)
) -> Room:
data.wallet = data.wallet or key.wallet.id
return await crud.create_room(data)
@chatelet_api_router.get("/api/v1/rooms")
async def api_list_rooms() -> list[Room]:
return await crud.get_rooms()
@chatelet_api_router.post("/api/v1/rooms/{room_id}/publish")
async def api_publish_room(
room_id: str, key: WalletTypeInfo = Depends(require_admin_key)
) -> Room:
room = await crud.get_room(room_id)
if not room:
raise HTTPException(404, "Room not found")
room.status = room.status.active
room.listing_event_id = await nostr.publish_listing(room) or room.listing_event_id
return await crud.update_room(room)
# --- availability (public read) --------------------------------------------
@chatelet_api_router.post("/api/v1/availability")
async def api_check_availability(q: AvailabilityQuery) -> AvailabilityResult:
try:
return await services.get_availability(q.room_id, q.check_in, q.check_out)
except ValueError as exc:
raise _to_http(exc) from exc
# --- booking (public write; guest-initiated) -------------------------------
@chatelet_api_router.post("/api/v1/bookings", status_code=201)
async def api_request_booking(data: BookingRequestData) -> BookingQuote:
try:
return await services.request_booking(data)
except services.BookingError as exc:
raise HTTPException(502, str(exc)) from exc
except ValueError as exc:
raise _to_http(exc) from exc
@chatelet_api_router.get("/api/v1/bookings/{booking_id}")
async def api_get_booking(
booking_id: str, key: WalletTypeInfo = Depends(require_invoice_key)
) -> Booking:
booking = await crud.get_booking(booking_id)
if not booking:
raise HTTPException(404, "Booking not found")
return booking
# --- blocks (operator) -----------------------------------------------------
@chatelet_api_router.post("/api/v1/blocks", status_code=201)
async def api_create_block(
data: CreateBlockData, key: WalletTypeInfo = Depends(require_admin_key)
):
block = await crud.create_block(data)
room = await crud.get_room(data.room_id)
if room:
await nostr.publish_block_calendar(
room, block.start_date, block.end_date, block.id
)
return block