Commit graph

10 commits

Author SHA1 Message Date
0dad30b648 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>
2026-09-16 12:25:31 +02:00
8b816d83b0 feat(api): a guest's own bookings on both doors
GET /api/v1/bookings/mine (LNbits account auth; identity = the account's
Nostr pubkey, the same value the booking request carried) and RPC twin
chatelet_booking_list_mine (scoped by the signed sender_pubkey). Rows come
back newest check-in first via the m003 guest index, as guest_booking_dict:
the guest's own contact and counts, minus the Lightning/Nostr plumbing.
Declared ahead of /bookings/{booking_id} so 'mine' is not read as an id.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 12:17:39 +02:00
9ab52f2c69 feat: per-operator settings — house rules + card acceptance (multi-tenant)
Chatelet is multi-tenant: any LNbits user can host rooms. What an operator
decides for all their rooms now lives in chatelet.operator_settings, keyed
by user id and created lazily (m003, which also indexes bookings by guest):
check-in/out times, cancellation policy, and accept_fiat.

Guests see it: the public room view (both doors) gains house_rules and
payment_methods, and the kind:30402 listing carries payment_methods,
checkin_time and checkout_time tags so a generic Nostr client can render
the right pay buttons and rules without our RPC. The check-in DM reads the
room owner's rules instead of the instance row.

Card is offered only when the operator opted in, the room is fiat-priced,
and LNbits core has a fiat provider for that user — resolved through
settings.get_fiat_providers_for_user(owner), the one seam lnbits#67's
per-user Stripe credentials will plug into; chatelet never sees creds.

Operator endpoints: GET/PUT /api/v1/operator (admin key → wallet user) and
RPC twins chatelet_operator_get/update (AUTH_WALLET); saving re-publishes
the owner's active listings. Admin UI moves the house-rule inputs into a
per-operator card with the card toggle and a provider hint. The old
house-rule columns on settings stay for old rows but are no longer read.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 12:16:14 +02:00
feaaef93e3 feat(api): keyless unavailable-ranges feed for the guest calendar
GET /api/v1/public/rooms/{id}/unavailable?start=&end= and its RPC twin
chatelet_room_unavailable return the nights a guest cannot book over a
window (default today → +365 d, capped at 400 d): occupying bookings —
live holds included, so the feed always agrees with POST /availability —
and manual blocks, clipped, sorted and coalesced into anonymous half-open
spans. Adjacent spans merge on purpose so a guest cannot tell where one
occupant's dates end and the next begin, nor a block from a stay.

is_available now uses the OCCUPYING_STATUSES constant it was inlining.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-16 11:45:51 +02:00
94542ad0f9 feat(api): keyless GET /api/v1/public/bookings/{id} for guests
The guest cannot subscribe to the operator's wallet and the existing
booking read needs a wallet invoice key, so a client had no way to wait
for awaiting_payment -> confirmed over HTTP short of polling the invoice
on LNbits core. Add the HTTP twin of the RPC door's chatelet_booking_get:
the 10-char booking id from the quote is the capability, and the response
is public_booking_dict — lifecycle, dates and money only, with the guest's
pubkey/contact and the Lightning/Nostr plumbing stripped.

Closes #18

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-15 23:43:21 +02:00
0631edf8a2 feat: public guest room discovery endpoints (slice 1 for webapp #141)
GET /public/rooms + /public/rooms/{id} — no auth, active rooms only,
operator-private fields stripped. The webapp guest UI needs these because
the existing GET /rooms is admin-scoped; availability + booking POST are
already public. Reuses the public_room_dict strip.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VUQCfdqiLSsFS2jcGnaFD
2026-07-20 01:39:39 +02:00
f715d728ef feat: operator admin REST endpoints (rooms/blocks/bookings/settings)
Adds the HTTP surface the admin UI needs, all admin-key + ownership-scoped:
- rooms: list (filtered to caller's wallet), update (PUT), delete, publish /
  unpublish (with ownership checks; publish/unpublish flip status + sync the
  relay listing).
- per-room: GET bookings, GET blocks.
- blocks: create (ownership-checked) + delete.
- settings: GET + PUT (merges only the editable fields).

_owned_room centralizes the 404/403 ownership guard.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VUQCfdqiLSsFS2jcGnaFD
2026-07-20 00:44:10 +02:00
5cfc9b893d refactor: extract booking flow into services.py
Move availability quoting + the check-then-hold + invoice orchestration out
of views_api.py into a transport-agnostic services.py, with typed errors
(NotFound/Unavailable/ValueError/BookingError). views_api becomes a thin
HTTP door that maps those to status codes. No behavior change — this is so
the incoming nostr-transport door can share one booking flow instead of
duplicating it.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VUQCfdqiLSsFS2jcGnaFD
2026-07-19 01:30:13 +02:00
4df137190f feat: wire FX + deposit invoice into the booking hold (#3)
Booking requests now complete end-to-end over HTTP:

- _to_sats() converts fiat->sats via fiat_amount_as_satoshis (sat/sats
  pass through). This is the single conversion point; the result is the
  canonical amount_sat and is not recomputed downstream.
- api_request_booking creates a sats-denominated deposit invoice (locked
  amount, immune to FX drift before payment), tagged {tag:chatelet,
  booking_id} so tasks.on_invoice_paid matches the settle. On InvoiceError
  the hold is released (status=declined) so dead holds don't block dates.
- New BookingQuote response returns the held booking + bolt11 + hash.

Settlement (tasks.on_invoice_paid: awaiting_payment -> confirmed, dates
hard-blocked, reservation republished) was already in place and now fires
on real payments. Testable on FakeWallet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VUQCfdqiLSsFS2jcGnaFD
2026-07-19 00:26:14 +02:00
d5df9ab662 feat: REST API + operator admin route
views_api.py exposes the booking flow over HTTP — availability check,
guest booking request (check-then-hold with canonical amount_sat), room
publish, blocks. REST and Nostr are two doors into the same crud flow;
FX + invoice creation marked TODO. views.py serves the operator page.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VUQCfdqiLSsFS2jcGnaFD
2026-07-19 00:16:57 +02:00