The guest picks what, when and how long
Each product card shows its online price. The price comes from the desk settings: the day-pass price for day passes and coworking days, and the hourly rate for each room. For rooms, start times that clash with a booking already in place are greyed out. Only bookable times are offered: a room booking must end by closing (10 PM), so durations that would run past it are disabled; today's start times respect each room's notice period; and once today can't be booked any more (after closing, or inside the notice), the earliest date moves to tomorrow — a day pass bought after closing is for tomorrow. The server checks the same rules.
The request is saved with its price
A BookingRequest is stored as PENDING with a reference like SP-7K3Q9M, the quoted amount and an unguessable publicToken. For a room, the server checks the slot again under a per-room lock, so two guests can't take the same hour. An unpaid priced room holds its slot for the ticket hold time (default 120 minutes).
Standard bookings confirm themselves
With Confirm bookings instantly on (Desk → Settings, on by default), a day pass, a coworking day or any of the three rooms is saved as CONFIRMED the moment the guest sends it. Nobody has to approve it first: rooms were already checked for clashes, and the guest pays at the front desk on arrival. The guest lands straight on "Your booking is confirmed" with the amount to bring. The team gets a "booking confirmed itself" email, and the booking shows under Today's arrivals and on the desk. Below the confirmation the guest's page offers Pay now online — skip the desk: the same Bankak transfer as below, optional. If they send a transaction number, the booking stays confirmed and the transfer waits for the team like any other; once matched, the guest gets "Payment received" and nothing is owed at the desk. If the team can't find it, the booking still stands — the guest is told why, can send the right number, or pays at the desk. The guest can still cancel from their page until the booking ends (until they've sent a transfer), and the team is told when they do. Private offices and dedicated desks always wait for the team. Turn the setting off to confirm every booking by hand; everything below then applies.
The guest pays on their own page
/booking/<token> shows the amount and how it was worked out (for example SDG 10,000 / hour · 2 hours), Space's Bankak account and the reference to write in the transfer note. The guest submits the transaction number, which can't be reused. Products without a price (dedicated desks, private offices, or anything with no rate set) show "we'll confirm the price with you" instead. While a request waits on the team, the page refreshes itself every 30 seconds, so the price or the confirmation appears without a reload.
Staff check the transfer
Staff learn about it three ways: the Bookings item in the admin menu shows how many requests need action, the to-do queue refreshes itself every minute, and — once RESEND_API_KEY is set — every active admin and cashier gets an email for each new request and each transfer to check, with a link to the booking. The transfer appears under Online payments to check on the desk and on the booking's page. Money received — confirm sets the booking to CONFIRMED and writes the charge and the Bankak payment to the ledger in one transaction. Not received rejects it with a reason the guest can see — a pending request is cancelled, an instantly confirmed booking stays confirmed. The booking's admin page lists every payment under Payments (Bankak or cash, transaction number, who took it, when) and shows the price with its rate × length; a booking with no price shows what today's rates would charge.
The guest arrives
A paid day pass appears under Expected today with a one-tap prepaid check-in. It adds no second charge. An unpaid one goes through the normal check-in, which charges.
Where the logic lives
| File | Role |
|---|---|
lib/desk/bookings.ts | Quote, clash check, create, transfer, verify, check-in |
site/booking/pricing.ts | Price calculation shared by the form and the server |
site/booking/config.ts | Products, durations, notice, time window |
site/booking/rules.ts | Booking rules |
site/booking/actions.ts | Create, room availability, transfer (honeypot + rate limit) |
site/booking/guest-content.tsx | The guest's page: pay, checking, confirmed, cancelled |
admin/desk/arrivals/ | Payments to check, expected today, the automation note |
Test it end to end
Use a real phone number and your own email. Book a room for a time nobody holds, because the clash check will refuse a slot that's taken.
- Instant (setting on). On
/book, choose a meeting room, a time and your details. The summary says "Booked instantly" and the button says Book now. After you send it, you land on "Your booking is confirmed", with the amount to pay at the desk. In the admin it shows as Confirmed, and its history reads Request made → Confirmed (instant). - Cancel. On the same page, open Cancel booking and confirm. The page says it was cancelled, and the room's time is free again on
/book. - Clash. Book the same room and time twice. The second attempt is refused on the time field.
- Manual (setting off). Turn off Confirm bookings instantly in Desk → Settings and book again. The page shows "pay to confirm" or "we'll confirm the price". Leave it open. In a second browser, sign in as staff, open the booking (it's in the queue, and the menu badge went up) and press Confirm booking. Within 30 seconds the guest's page switches to "confirmed" by itself.
- Pay ahead. Repeat step 1, then send any transaction number from Pay now online. The guest page says "We're checking your transfer"; in the admin the booking is still Confirmed, with Transfer to check in Actions. Money received — confirm → the transfer appears under Payments, Billing says Paid, and the guest page shows the receipt. Repeat with Not received → still Confirmed, and the guest can send another number.
- Desk. For a day pass booked today, the booking appears under Expected today on the front desk. Check in and charge takes the payment.
- Email (needs
RESEND_API_KEY). In steps 1 and 4, the guest gets "received" or "confirmed" and the team gets "new" or "confirmed itself". In step 2, the team gets "a guest cancelled".
Turn the setting back on when you're done.
On their own, every hour (Cloudflare cron → cf/worker.js → /api/cron, src/lib/desk/booking-routines.ts): confirmed bookings that are over become Visited; requests nobody confirmed before their time ended become cancelled, the guest is told "Request expired" and the history says so; and each confirmed booking gets one reminder in the 24 hours before it, once its confirmation is 12 hours old — never between 22:00 and 07:00. The desk's daily routines (office invoices, forgotten check-outs) ride the same tick.
Set the day-pass price, the room rates and the Bankak account in Desk → Settings. Until they're set, everything shows "quoted by Space" and still works as a request — and the bookings dashboard says so at the top, with a link to the settings. pnpm prices:seed fills any empty price with a starting point (day pass 20,000 SDG a day; meeting room 10,000, training room 20,000, event space 30,000 SDG an hour) for the team to adjust — it never overwrites a price that's already set.