feat: wire FX + deposit invoice into the booking hold (#3) #7
2 changed files with 50 additions and 13 deletions
11
models.py
11
models.py
|
|
@ -206,3 +206,14 @@ class AvailabilityResult(BaseModel):
|
||||||
quote_sat: int | None = None
|
quote_sat: int | None = None
|
||||||
quote_fiat: float | None = None
|
quote_fiat: float | None = None
|
||||||
currency: str | 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
|
||||||
|
|
|
||||||
52
views_api.py
52
views_api.py
|
|
@ -14,14 +14,18 @@ from datetime import datetime, timedelta, timezone
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
from lnbits.core.models import WalletTypeInfo
|
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.decorators import require_admin_key, require_invoice_key
|
||||||
|
from lnbits.exceptions import InvoiceError
|
||||||
from lnbits.helpers import urlsafe_short_hash
|
from lnbits.helpers import urlsafe_short_hash
|
||||||
|
from lnbits.utils.exchange_rates import fiat_amount_as_satoshis
|
||||||
|
|
||||||
from . import crud
|
from . import crud
|
||||||
from .models import (
|
from .models import (
|
||||||
AvailabilityQuery,
|
AvailabilityQuery,
|
||||||
AvailabilityResult,
|
AvailabilityResult,
|
||||||
Booking,
|
Booking,
|
||||||
|
BookingQuote,
|
||||||
BookingRequestData,
|
BookingRequestData,
|
||||||
BookingStatus,
|
BookingStatus,
|
||||||
CreateBlockData,
|
CreateBlockData,
|
||||||
|
|
@ -93,10 +97,10 @@ async def api_check_availability(q: AvailabilityQuery) -> AvailabilityResult:
|
||||||
|
|
||||||
|
|
||||||
@chatelet_api_router.post("/api/v1/bookings", status_code=201)
|
@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
|
"""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
|
write a `held` booking with the canonical amount_sat, create the deposit
|
||||||
(TODO) create the deposit invoice. The Nostr path calls the same steps."""
|
invoice, and return the bolt11. The Nostr path calls the same steps."""
|
||||||
room = await crud.get_room(data.room_id)
|
room = await crud.get_room(data.room_id)
|
||||||
if not room or room.status != room.status.active:
|
if not room or room.status != room.status.active:
|
||||||
raise HTTPException(404, "Room not available")
|
raise HTTPException(404, "Room not available")
|
||||||
|
|
@ -136,10 +140,36 @@ async def api_request_booking(data: BookingRequestData) -> Booking:
|
||||||
)
|
)
|
||||||
await crud.create_booking(booking)
|
await crud.create_booking(booking)
|
||||||
|
|
||||||
# TODO(payment): create_invoice(wallet_id=room.wallet, amount=deposit_sat,
|
# Invoice is denominated in sats (deposit_sat is already the canonical
|
||||||
# extra={"tag": "chatelet", "booking_id": booking.id}); store payment_hash;
|
# amount locked at quote time) so FX drift between now and payment can't
|
||||||
# set status=awaiting_payment; DM the bolt11 to the guest (NIP-17).
|
# change what the guest owes. tag+booking_id let tasks.on_invoice_paid
|
||||||
return booking
|
# 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}")
|
@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:
|
async def _to_sats(amount: float, currency: str) -> int:
|
||||||
"""Fiat/currency -> sats. Canonical conversion happens HERE, once, at
|
"""Fiat/currency -> sats. Canonical conversion happens HERE, once, at
|
||||||
quote/hold time; the result is stored as amount_sat and never recomputed
|
quote/hold time; the result is stored as amount_sat and never recomputed
|
||||||
downstream.
|
downstream."""
|
||||||
|
|
||||||
TODO(fx): if currency in ('sat','sats') return int(amount); else call
|
|
||||||
lnbits.utils.exchange_rates.fiat_amount_as_satoshis(amount, currency).
|
|
||||||
"""
|
|
||||||
if currency.lower() in ("sat", "sats"):
|
if currency.lower() in ("sat", "sats"):
|
||||||
return int(amount)
|
return int(amount)
|
||||||
raise NotImplementedError("wire lnbits fiat_amount_as_satoshis")
|
return await fiat_amount_as_satoshis(amount, currency)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue