diff --git a/models.py b/models.py index d2c2b81..78b7e27 100644 --- a/models.py +++ b/models.py @@ -206,3 +206,14 @@ class AvailabilityResult(BaseModel): quote_sat: int | None = None quote_fiat: float | None = None currency: str | None = None + + +class BookingQuote(BaseModel): + """Response to a booking request: the held booking plus the bolt11 the + guest must pay to confirm it. payment_request covers deposit_sat (the + canonical amount already stored on the booking) — the guest never re- + computes what they owe.""" + + booking: Booking + payment_request: str + payment_hash: str diff --git a/views_api.py b/views_api.py index fb6ccda..20ec9a1 100644 --- a/views_api.py +++ b/views_api.py @@ -14,14 +14,18 @@ from datetime import datetime, timedelta, timezone from fastapi import APIRouter, Depends, HTTPException from lnbits.core.models import WalletTypeInfo +from lnbits.core.services import create_invoice from lnbits.decorators import require_admin_key, require_invoice_key +from lnbits.exceptions import InvoiceError from lnbits.helpers import urlsafe_short_hash +from lnbits.utils.exchange_rates import fiat_amount_as_satoshis from . import crud from .models import ( AvailabilityQuery, AvailabilityResult, Booking, + BookingQuote, BookingRequestData, BookingStatus, CreateBlockData, @@ -93,10 +97,10 @@ async def api_check_availability(q: AvailabilityQuery) -> AvailabilityResult: @chatelet_api_router.post("/api/v1/bookings", status_code=201) -async def api_request_booking(data: BookingRequestData) -> Booking: +async def api_request_booking(data: BookingRequestData) -> BookingQuote: """Guest requests a stay. Check-then-hold is the lock: if available we - write a `held` booking with the canonical amount_sat and an expiry, then - (TODO) create the deposit invoice. The Nostr path calls the same steps.""" + write a `held` booking with the canonical amount_sat, create the deposit + invoice, and return the bolt11. The Nostr path calls the same steps.""" room = await crud.get_room(data.room_id) if not room or room.status != room.status.active: raise HTTPException(404, "Room not available") @@ -136,10 +140,36 @@ async def api_request_booking(data: BookingRequestData) -> Booking: ) await crud.create_booking(booking) - # TODO(payment): create_invoice(wallet_id=room.wallet, amount=deposit_sat, - # extra={"tag": "chatelet", "booking_id": booking.id}); store payment_hash; - # set status=awaiting_payment; DM the bolt11 to the guest (NIP-17). - return booking + # Invoice is denominated in sats (deposit_sat is already the canonical + # amount locked at quote time) so FX drift between now and payment can't + # change what the guest owes. tag+booking_id let tasks.on_invoice_paid + # match the settlement back to this booking. + try: + payment = await create_invoice( + wallet_id=room.wallet, + amount=booking.deposit_sat, + memo=( + f"Chatelet · {room.title} · " + f"{booking.check_in}→{booking.check_out} ({nights}n)" + ), + extra={"tag": "chatelet", "booking_id": booking.id}, + ) + except InvoiceError as exc: + # Free the dates immediately — a hold with no payable invoice is dead. + booking.status = BookingStatus.declined + await crud.update_booking(booking) + raise HTTPException(502, f"Could not create invoice: {exc.message}") from exc + + booking.payment_hash = payment.payment_hash + booking.status = BookingStatus.awaiting_payment + await crud.update_booking(booking) + + # TODO(#5): DM the bolt11 to the guest over NIP-17 for the Nostr path. + return BookingQuote( + booking=booking, + payment_request=payment.bolt11, + payment_hash=payment.payment_hash, + ) @chatelet_api_router.get("/api/v1/bookings/{booking_id}") @@ -174,11 +204,7 @@ async def api_create_block( async def _to_sats(amount: float, currency: str) -> int: """Fiat/currency -> sats. Canonical conversion happens HERE, once, at quote/hold time; the result is stored as amount_sat and never recomputed - downstream. - - TODO(fx): if currency in ('sat','sats') return int(amount); else call - lnbits.utils.exchange_rates.fiat_amount_as_satoshis(amount, currency). - """ + downstream.""" if currency.lower() in ("sat", "sats"): return int(amount) - raise NotImplementedError("wire lnbits fiat_amount_as_satoshis") + return await fiat_amount_as_satoshis(amount, currency)