feat(api): keyless unavailable-ranges feed for the guest calendar
GET /api/v1/public/rooms/{id}/unavailable?start=&end= and its RPC twin
chatelet_room_unavailable return the nights a guest cannot book over a
window (default today → +365 d, capped at 400 d): occupying bookings —
live holds included, so the feed always agrees with POST /availability —
and manual blocks, clipped, sorted and coalesced into anonymous half-open
spans. Adjacent spans merge on purpose so a guest cannot tell where one
occupant's dates end and the next begin, nor a block from a stay.
is_available now uses the OCCUPYING_STATUSES constant it was inlining.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
64607a00bc
commit
feaaef93e3
8 changed files with 278 additions and 8 deletions
44
services.py
44
services.py
|
|
@ -14,7 +14,7 @@ backend failure and surfaces as a generic error over RPC (logged server-side).
|
|||
|
||||
import asyncio
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from datetime import date, datetime, timedelta, timezone
|
||||
|
||||
from lnbits.core.services import create_invoice
|
||||
from lnbits.exceptions import InvoiceError
|
||||
|
|
@ -28,7 +28,9 @@ from .models import (
|
|||
BookingQuote,
|
||||
BookingRequestData,
|
||||
BookingStatus,
|
||||
DateRange,
|
||||
RoomStatus,
|
||||
UnavailableRanges,
|
||||
)
|
||||
|
||||
# Per-room lock serializing the availability read + the `held` write, so two
|
||||
|
|
@ -59,6 +61,46 @@ async def to_sats(amount: float, currency: str) -> int:
|
|||
return await fiat_amount_as_satoshis(amount, currency)
|
||||
|
||||
|
||||
# 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
|
||||
MAX_CALENDAR_DAYS = 400
|
||||
|
||||
|
||||
async def get_unavailable_ranges(
|
||||
room_id: str, start: str | None = None, end: str | None = None
|
||||
) -> UnavailableRanges:
|
||||
"""Occupied/blocked nights for one active room over [start, end).
|
||||
Defaults to today → +365 days (UTC dates)."""
|
||||
room = await crud.get_room(room_id)
|
||||
if not room or room.status != RoomStatus.active:
|
||||
raise NotFound("Room not available")
|
||||
try:
|
||||
d_start = (
|
||||
date.fromisoformat(start) if start else datetime.now(timezone.utc).date()
|
||||
)
|
||||
d_end = (
|
||||
date.fromisoformat(end)
|
||||
if end
|
||||
else d_start + timedelta(days=DEFAULT_CALENDAR_DAYS)
|
||||
)
|
||||
except ValueError as exc:
|
||||
raise ValueError("Dates must be YYYY-MM-DD") from exc
|
||||
if d_end <= d_start:
|
||||
raise ValueError("end must be after start")
|
||||
if (d_end - d_start).days > MAX_CALENDAR_DAYS:
|
||||
raise ValueError(f"Window may span at most {MAX_CALENDAR_DAYS} days")
|
||||
spans = await crud.get_occupied_ranges(
|
||||
room_id, d_start.isoformat(), d_end.isoformat()
|
||||
)
|
||||
return UnavailableRanges(
|
||||
room_id=room_id,
|
||||
start=d_start.isoformat(),
|
||||
end=d_end.isoformat(),
|
||||
ranges=[DateRange(start=s, end=e) for s, e in spans],
|
||||
)
|
||||
|
||||
|
||||
async def get_availability(
|
||||
room_id: str, check_in: str, check_out: str
|
||||
) -> AvailabilityResult:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue