Hospitality engine

Pencil in or book a big-group enquiry

post/api/enquiries/{id}/convert

Creates 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.

  • pending pencils it in. It holds its tables against the exclusion constraint exactly like a confirmed sitting (0011 releases only cancelled and no_show), sets the enquiry to open rather than won, 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 to won and 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
sb-qxrvgfkjyvbngipqvslu-auth-token<token>

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

id*string

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"}