diff --git a/src/modules/chatelet/index.ts b/src/modules/chatelet/index.ts index 60653c9..8980b69 100644 --- a/src/modules/chatelet/index.ts +++ b/src/modules/chatelet/index.ts @@ -8,7 +8,7 @@ export interface ChateletModuleConfig { /** * 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({ name: 'chatelet', @@ -52,3 +52,4 @@ export const chateletModule = createModulePlugin({ export default chateletModule export type { Room, AvailabilityResult } from './types/room' +export type { Booking, BookingQuote, BookingRequest, BookingStatus } from './types/booking' diff --git a/src/modules/chatelet/services/ChateletApiService.ts b/src/modules/chatelet/services/ChateletApiService.ts index 015252a..c468c47 100644 --- a/src/modules/chatelet/services/ChateletApiService.ts +++ b/src/modules/chatelet/services/ChateletApiService.ts @@ -1,4 +1,5 @@ import type { AvailabilityResult, Room } from '../types/room' +import type { BookingQuote, BookingRequest, PaymentStatus } from '../types/booking' export interface ChateletApiConfig { baseUrl: string @@ -9,8 +10,8 @@ export interface ChateletApiConfig { * HTTP client for the LNbits `chatelet` extension's guest surface. * * Plain config-wrapper (same pattern as events' TicketApiService) — not a - * BaseService. Slice 1 is read-only guest browsing over public endpoints; - * booking (POST /bookings) + status polling arrive in slice 2. + * BaseService. Guest browsing, availability and the booking request are all + * public endpoints; settlement is polled on the invoice via LNbits core. * * Transport note: this is HTTP-for-now. The extension also exposes a * 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 { + 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 { + return this.request(`/api/v1/payments/${paymentHash}`, { method: 'GET' }) + } + private async request(path: string, init: RequestInit = {}): Promise { const headers: Record = { accept: 'application/json', diff --git a/src/modules/chatelet/types/booking.ts b/src/modules/chatelet/types/booking.ts new file mode 100644 index 0000000..8a843a2 --- /dev/null +++ b/src/modules/chatelet/types/booking.ts @@ -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 +}