feat: card rail via LNbits fiat providers (Stripe), per operator
A guest may pay a fiat-priced room by card when its owner has opted in and
LNbits core has a fiat provider for that user — resolved through
settings.get_fiat_providers_for_user(owner), the seam lnbits#67's per-user
Stripe Connect credentials will plug into; chatelet stores no credentials.
Both rails now go through create_payment_request: Lightning unchanged
(sats, deposit_sat), card charges the same deposit share of the fiat price
in the room's currency with extra.checkout parameterising the hosted
Stripe page — success/cancel return to {frontend}/chatelet/{room}?checkout=…
&booking=<id>, customer_email, line item, metadata. frontend_url is
allow-listed against the instance's trusted origins (ported from events)
and resolved before the hold so a refused rail never leaves a dead hold.
Core settles the Stripe webhook onto the same invoice queue, so
tasks.on_invoice_paid confirms card bookings unchanged.
BookingRequestData gains payment_method / fiat_provider / frontend_url;
BookingQuote gains fiat_payment_request / fiat_provider / is_fiat and a
nullable payment_request. RPC chatelet_booking_request passes the fields
through. min_lnbits_version → 1.4.1 (events' floor for these APIs).
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
8557ce617e
commit
0dad30b648
11 changed files with 417 additions and 36 deletions
30
models.py
30
models.py
|
|
@ -20,8 +20,9 @@ Design notes carried into the field definitions:
|
|||
import json
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from urllib.parse import urlsplit
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from pydantic import BaseModel, Field, validator
|
||||
|
||||
|
||||
def _now() -> datetime:
|
||||
|
|
@ -167,6 +168,27 @@ class BookingRequestData(BaseModel):
|
|||
num_guests: int = 1
|
||||
guest_contact: str | None = None # optional email/phone/nostr note
|
||||
message: str | None = None # free-form note to the host
|
||||
# Rail the guest wants to pay with. "fiat" needs the room owner to accept
|
||||
# card AND LNbits core to have a provider for them (services checks).
|
||||
payment_method: str = "lightning"
|
||||
fiat_provider: str | None = None # e.g. "stripe"; defaults to the owner's first
|
||||
# Where the hosted checkout should send the guest back (the calling app);
|
||||
# origin must be one the instance trusts — see frontend.resolve_frontend_root.
|
||||
frontend_url: str | None = Field(default=None, max_length=512)
|
||||
|
||||
@validator("frontend_url")
|
||||
def validate_frontend_url(cls, v): # noqa: N805
|
||||
if v is None:
|
||||
return None
|
||||
v = v.strip()
|
||||
if not v:
|
||||
return None
|
||||
parts = urlsplit(v)
|
||||
if parts.scheme not in ("http", "https") or not parts.netloc:
|
||||
raise ValueError("frontend_url must be an absolute http(s) URL")
|
||||
if parts.query or parts.fragment or ".." in parts.path:
|
||||
raise ValueError("frontend_url must not contain a query, fragment or '..'")
|
||||
return v.rstrip("/")
|
||||
|
||||
|
||||
class Booking(BaseModel):
|
||||
|
|
@ -316,5 +338,9 @@ class BookingQuote(BaseModel):
|
|||
computes what they owe."""
|
||||
|
||||
booking: Booking
|
||||
payment_request: str
|
||||
payment_request: str | None # bolt11 — None on the card rail
|
||||
payment_hash: str
|
||||
# Card rail: the provider's hosted checkout URL to send the guest to.
|
||||
fiat_payment_request: str | None = None
|
||||
fiat_provider: str | None = None
|
||||
is_fiat: bool = False
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue