feat(chatelet): book a room and pay by Lightning (slice 2) #154

Merged
padreug merged 4 commits from feat/chatelet-booking into dev 2026-09-15 21:52:08 +00:00
3 changed files with 100 additions and 3 deletions
Showing only changes of commit 3e55decfad - Show all commits

feat(chatelet): booking wire types + requestBooking / payment-status client

Mirror the extension's BookingRequestData / Booking / BookingQuote
models and add the two calls slice 2 needs: POST /bookings (hold +
invoice) and an anonymous settlement poll on LNbits core's
GET /api/v1/payments/{hash}. Chatelet's own booking read needs a
wallet key, so the invoice is what the guest can watch until a public
booking-status endpoint lands on the extension.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Padreug 2026-09-15 23:10:59 +02:00

View file

@ -8,7 +8,7 @@ export interface ChateletModuleConfig {
/** /**
* Chatelet module — guest booking UI for the LNbits `chatelet` room-rentals * Chatelet module — guest booking UI for the LNbits `chatelet` room-rentals
* extension. Slice 1: read-only discovery + availability over HTTP. * extension: discovery, availability and Lightning-paid booking over HTTP.
*/ */
export const chateletModule = createModulePlugin({ export const chateletModule = createModulePlugin({
name: 'chatelet', name: 'chatelet',
@ -52,3 +52,4 @@ export const chateletModule = createModulePlugin({
export default chateletModule export default chateletModule
export type { Room, AvailabilityResult } from './types/room' export type { Room, AvailabilityResult } from './types/room'
export type { Booking, BookingQuote, BookingRequest, BookingStatus } from './types/booking'

View file

@ -1,4 +1,5 @@
import type { AvailabilityResult, Room } from '../types/room' import type { AvailabilityResult, Room } from '../types/room'
import type { BookingQuote, BookingRequest, PaymentStatus } from '../types/booking'
export interface ChateletApiConfig { export interface ChateletApiConfig {
baseUrl: string baseUrl: string
@ -9,8 +10,8 @@ export interface ChateletApiConfig {
* HTTP client for the LNbits `chatelet` extension's guest surface. * HTTP client for the LNbits `chatelet` extension's guest surface.
* *
* Plain config-wrapper (same pattern as events' TicketApiService) — not a * Plain config-wrapper (same pattern as events' TicketApiService) — not a
* BaseService. Slice 1 is read-only guest browsing over public endpoints; * BaseService. Guest browsing, availability and the booking request are all
* booking (POST /bookings) + status polling arrive in slice 2. * public endpoints; settlement is polled on the invoice via LNbits core.
* *
* Transport note: this is HTTP-for-now. The extension also exposes a * Transport note: this is HTTP-for-now. The extension also exposes a
* kind-21000 Nostr-RPC surface (chatelet_room_list / _availability / …), but * kind-21000 Nostr-RPC surface (chatelet_room_list / _availability / …), but
@ -41,6 +42,30 @@ export class ChateletApiService {
}) })
} }
/**
* Hold the dates and get the invoice that confirms them. The backend
* quotes the amount (canonical `booking.deposit_sat`) — the guest never
* recomputes what they owe. 502 = invoice creation failed server-side.
*/
async requestBooking(data: BookingRequest): Promise<BookingQuote> {
return this.request('/chatelet/api/v1/bookings', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(data),
})
}
/**
* Anonymous settlement check on LNbits core. Chatelet's own
* `GET /bookings/{id}` needs a wallet key, so until a public booking
* status endpoint lands the guest polls the invoice instead — the
* extension's paid-invoice listener is what flips the booking to
* `confirmed`.
*/
async getPaymentStatus(paymentHash: string): Promise<PaymentStatus> {
return this.request(`/api/v1/payments/${paymentHash}`, { method: 'GET' })
}
private async request(path: string, init: RequestInit = {}): Promise<any> { private async request(path: string, init: RequestInit = {}): Promise<any> {
const headers: Record<string, string> = { const headers: Record<string, string> = {
accept: 'application/json', accept: 'application/json',

View file

@ -0,0 +1,71 @@
/**
* Booking wire types — mirror `models.py` in the LNbits `chatelet`
* extension (BookingRequestData / Booking / BookingQuote). Field names are
* kept as the backend spells them so the JSON maps 1:1.
*/
export type BookingStatus =
| 'held'
| 'awaiting_payment'
| 'confirmed'
| 'declined'
| 'cancelled'
| 'expired'
| (string & {})
/** What a guest sends to `POST /chatelet/api/v1/bookings`. */
export interface BookingRequest {
room_id: string
/** Hex pubkey of the guest — the identity the check-in DM goes to. */
guest_pubkey: string
/** YYYY-MM-DD, inclusive. */
check_in: string
/** YYYY-MM-DD, exclusive. */
check_out: string
num_guests: number
guest_contact?: string
message?: string
}
export interface Booking {
id: string
room_id: string
guest_pubkey: string
guest_contact: string | null
check_in: string
check_out: string
nights: number
num_guests: number
/** Snapshot of the room's price currency. */
currency: string
/** nights × room price, display only. */
price_fiat: number
/** Canonical total due, in sats. */
amount_sat: number
/** Portion invoiced now (== amount_sat at 100 % prepay). */
deposit_sat: number
status: BookingStatus
payment_hash: string | null
/** Hold expiry while held / awaiting_payment; null once confirmed. */
expires_at: string | null
created_at: string
updated_at: string
}
/** Response to a booking request: the held booking + the bolt11 that confirms it. */
export interface BookingQuote {
booking: Booking
payment_request: string
payment_hash: string
}
/**
* Anonymous read of LNbits core `GET /api/v1/payments/{hash}` — the guest
* cannot read their booking over HTTP yet (that endpoint needs a wallet
* key), so settlement is polled on the invoice instead. `status` is only
* present on a failed payment.
*/
export interface PaymentStatus {
paid: boolean
status?: string
}