Pencil in or book a big-group enquiry
/api/enquiries/{id}/convertCreates a reservation from the enquiry, links the two (book_group_enquiries.reservation_id, migration 0038), and moves the enquiry along.
Its own route rather than the client calling POST /api/reservations and then PATCHing the enquiry: those are two writes with a gap, and a failure in the gap leaves a booking nobody can reach from the thread it came from. The enquiry is claimed with where reservation_id is null, so two staff converting at once produces one booking and a 409; the partial unique index is the backstop, not the button being hidden.
Two stages, chosen by status. A big group is agreed over days of back-and-forth, and the thing a venue needs on message two is not a confirmed booking but the tables taken off the market while the conversation happens.
pendingpencils it in. It holds its tables against the exclusion constraint exactly like a confirmed sitting (0011 releases onlycancelledandno_show), sets the enquiry toopenrather thanwon, and posts NOTHING into the thread, because the guest has not been promised anything yet. Undo it with the DELETE below, or finish it with POST /confirm.confirmed(the default) is the one-shot, for a group that agreed everything in one reply. Sets the enquiry towonand posts the "Booked" line.
tableIds is ordered and may hold up to 20. The first becomes the reservation's own table_id; the rest are written to book_reservation_tables (0035) so the whole combination is inside the double-booking constraint. This is the normal case here, not an edge one: a party over the venue's online cap rarely fits on a single table, and a venue will hand a big group a whole room. Twenty rather than the allocator's four: that four bounds a combinatorial subset search on an anonymous request, while here a signed-in host names the tables and each one costs a single insert. Seat limits (staffSeatLimitEnforced, 0036) are measured across the whole combination, not the primary alone.
The guest is resolved into a client by email, so a returning one lands on the row they already have. partySize may be corrected on the way through, and is bounded by the same 500 the public enquiry form accepts, not by the 30 that bounds the public booking path. Otherwise an enquiry too big to book online would be too big to book at all.
Authorization
sessionCookie The dashboard's Supabase Auth session cookie, set at sign-in. Large sessions are split across
numbered chunks (…auth-token.0, .1), so treat this as a cookie family rather than one name.
Every request re-validates it against the Auth server (getUser()), never by decoding the cookie
locally: a JWT nothing has checked is not a credential. Tenancy is then read from the verified
app_metadata.company_id claim and enforced by row-level security; it is never read from request
input, on any route, ever.
role (admin / staff) is deliberately not in RLS. It gates specific actions in route code,
the operations marked admin below, so hiding a button in the UI is cosmetic only, and a route's
own check is the enforcement.
In: cookie
Path Parameters
Enquiry id.
Request Body
application/json
TypeScript Definitions
Use the request body type in TypeScript.
Response Body
application/json
application/json
application/json
application/json
application/json
application/json
application/json
curl -X POST "https://example.com/api/enquiries/string/convert" \ -H "Content-Type: application/json" \ -d '{ "startsAt": "2019-08-24T14:15:22Z", "turnMinutes": 5 }'{ "ok": true, "reservationId": "54c41ef9-5629-4a9c-bb0d-10f615966bd0", "status": "pending"}Close out a waitlist entry PATCH
Sets `status` to `converted`, `expired` or `cancelled`. **`notified` is deliberately NOT settable here**, and it is the only interesting rule on this route. That status is a claim about a message that left the building; setting it from a plain PATCH would let the list show an offer that was never sent, with the guest waiting at home for an email nobody dispatched. Only POST ./offer writes it, after the send. The three end states are terminal; a `converted` entry cannot be dragged back to `open` and offered again to a guest who is already booked. No minRole: clearing a waitlist is ordinary floor work.
Release a pencilled-in hold DELETE
Deletes the reservation this enquiry was pencilled in on and hands the enquiry back as one with no live booking, ready to be pencilled in somewhere else. The group went elsewhere, or the date moved. **Delete, not cancel, and that is the point of it existing.** Cancelling enqueues `reservation_cancelled`, which emails the guest that their booking is off, for a hold they were never told about. Deleting frees the tables silently, cascades `book_reservation_tables` (0035), and lets 0039's `on delete set null (reservation_id)` clear the link. Refuses anything a guest has been told about or has paid for: only a `pending` or already-`cancelled` reservation with no payment intent. A confirmed booking is cancelled from the Reservations screen, where the guest is notified and the deposit is resolved.