Skip to content

Restaurant Ordering

In development

Restaurant ordering is being built in stages and is off by default (restaurant-ordering-enabled). This page describes how it works; Running a restaurant or takeaway is the step-by-step guide for shop owners, and the storefront menu is described under Bistro Theme. The design is in the repository at docs/implementation-plan/restaurant-ordering.md.

A restaurant or takeaway takes orders on its own site for collection or local delivery, during the hours its kitchen takes orders. Staff accept each order in the Badger app (setting a preparation time) or reject it, and the customer is charged only once the kitchen accepts.

Turning it on

Setting Key Default What it does
Take restaurant orders restaurant-ordering-enabled off Menu items can be added to baskets, and collection or local-delivery orders from a location that takes food orders go to its kitchen.
Allergen notice restaurant-allergen-notice "Food allergies or intolerances? Please ask us before you order." The allergen line the menu and order show (UK food information law).

Both are under Settings › Restaurant ordering. Each location that cooks then needs ordering switched on in its ordering settings:

Field Default Meaning
enabled off The location takes food orders.
orderingHours the opening hours When orders are taken, if different. A period that closes before it opens (18:00-02:00) runs past midnight.
lastOrdersMinutes 15 Orders stop this long before each period closes.
scheduleAheadDays 0 How many days ahead a customer may order for. With 0, a time later today can be chosen while the kitchen is open; with more, customers can also order while it is closed.
slotMinutes 15 The steps between the times offered.
paused, pausedUntil off "We're slammed": no new orders until resumed (or until pausedUntil).
defaultPrepMinutes 20 The usual preparation time.
busyThresholdOrders, extraPrepMinutesPerOrder 3, 5 Each order in preparation beyond the threshold adds the extra minutes to the estimate.
acceptTimeoutMinutes 8 An order not accepted within this is rejected and refunded automatically; the kitchen is reminded at half the time.

Times are in the location's time zone (else the shop's, see Dates and time zones), and keep their wall-clock hours across clock changes.

A dish or drink is a product with the menuItem product type. Sizes are variants. Its dietary flags (dietary: VEGETARIAN, VEGAN, GLUTEN_FREE, DAIRY_FREE, NUT_FREE, HALAL, KOSHER, SPICY_MILD, SPICY_MEDIUM, SPICY_HOT) and allergens (allergens: the 14 UK FIR allergens, e.g. MILK, CEREALS_CONTAINING_GLUTEN, PEANUTS) are product attributes, as lists or comma-separated codes.

One place to order

The menu page is the one place a dish is added to an order. A dish's product page is its browse view (photos, description, allergens): where it would show the add-to-basket button it shows Order this instead (Nova and Brock themes), a link to the menu with that dish's options sheet open (/p/<menu page>?item=<dish>, plus &size= for the size picked on the product page; MenuItemLinks). It is offered for every enabled menu item on a shop taking restaurant orders that has a menu page, with choices or without (a can of cola too), and the product page of anything else (a gift card, merch) keeps its normal buy box. The sheet and the item page link back with More about this dish.

Adding a menu item to the basket is refused while the shop isn't taking food orders: ordering off, every kitchen closed (unless customers may order ahead), or paused.

Modifier groups

Choices such as "Choose a sauce" or "Extras" are modifier groups, shared between items and attached to a product by id (modifierGroupIds, in display order). A group has a name, the fewest and most choices (minSelections, maxSelections, 0 for no limit), how many of one option may be chosen (maxPerOption: 1 for a checkbox, more for a stepper) and its options, each with a price difference in pence (priceDelta, may be 0), whether it is available ("out of halloumi"), and its own dietary flags and allergens.

When a customer adds an item they send their choices and an optional note ("no onions", up to 200 characters). The choices are checked against the item's groups (required groups answered, limits kept, options available) and copied onto the basket line with their names and prices, so later edits to a group don't change a basket. Two adds of the same item become one line only when the choices and note are the same. The line's price is (item price + its choices) × quantity, and promotions apply to that. The line also records the allergens of the item as ordered, its own plus those of the options chosen.

  • Storefront form (POST /basket): modifiers values groupId:optionId or groupId:optionId:quantity (repeat a value to choose an option twice), and note.
  • REST (PUT /v1/basket, POST /v1/basket): modifiers ([{groupId, optionId, quantity}]) and note on each item; a choice that doesn't fit is a 400 ModifierSelectionError.

Collection and local delivery

A basket with a menu item is a food order. It can only be collected from a kitchen ("Collect from High Street", one option per location taking food orders) or delivered by the restaurant itself; the shop's other delivery and collection options aren't offered for it. Anything else in the basket goes with the food. A food order is never split between collection and delivery, even on a shop with collect some, deliver the rest on: it comes from one kitchen at one time. A basket without a menu item can have local delivery too, from a shop's own location (see Delivering locally from a shop), but a food order is only ever delivered by a kitchen.

Merch. A T-shirt or a bottle of sauce sold by the restaurant is retail stock, not a menu item. Before the customer picks a kitchen it can be had up to what can be posted and what click-and-collect stores hold. Once a kitchen is chosen only that kitchen's stock counts, because the item goes out with the food. So with Multiple stock locations on, merch sold alongside food needs its stock recorded at each kitchen that sells it. A basket of merch alone is an ordinary retail basket: posted, collected from a click-and-collect store, or delivered by a shop's own local delivery option whose location holds it. A kitchen's delivery isn't offered to it, so it never goes to a kitchen's ticket queue.

With Multiple stock locations on, the menu and product pages show what the best kitchen can make before one is chosen, and only the chosen kitchen's stock after, as the basket does.

Local delivery is a delivery option with method LOCAL_DELIVERY, bound to the location that cooks and delivers (collectionLocationCode):

Field Meaning
price The delivery charge.
minimumOrderValue The smallest basket delivered, in pence, counted on the items before discounts and delivery. Below it checkout says "Add £4.50 more for delivery".
parameters.travelMinutes How long the drive takes, added to the preparation time for the estimate.
allowedPostCodes, restrictedPostCodes Optional extra postcode rules for this option, on top of the location's service area.

Where it delivers is the location's service area: allowedPostcodePrefixes and blockedPostcodePrefixes per country ({"GB": ["LS1", "LS2", "LS6 1"]}), and a radiusKm. UK postcodes are matched on whole parts, in any case and with or without the space: LS1 covers LS1 6HD but not LS10 1AA, LS covers every LS district, LS6 1 covers that sector. No geocoding data is bundled, so the radius is checked only for an address that has a map position; a zone drawn only by a radius doesn't deliver to addresses without one. A service area with no rules at all delivers anywhere.

When an address isn't delivered to, checkout says why: "We don't deliver to LS10 1 yet, collection is available", "Add £4.50 more for delivery", or asks for the full postcode when the zone is drawn by sector. A delivery chosen earlier is checked again when the order is placed, in case the address or basket changed. Changing the address to one the chosen delivery doesn't reach drops that delivery (and its time slot) straight away and takes its charge out of the total, so the customer sees the options that do suit the new address. Both checkouts do this: the step-by-step checkout reopens the delivery step, and the single-page checkout says the option isn't available for this address any more and asks them to choose again (it waits until the address has a postcode and country, so a half-typed postcode doesn't drop it). Changing the address back offers delivery again. Until a delivery option is chosen the order summary shows "Choose a delivery option" rather than a delivery charge.

Estimates and the time

While a kitchen is open, each option shows how long it takes: "Delivering to LS1 6 in about 35-45 min" (preparation now plus travelMinutes, rounded up to five minutes, with ten minutes' spread) and "Ready in about 20 min" for collection. The basket page shows the same ("Delivery in about 35-45 min · Collection ready in about 20 min").

Checkout asks when the order is wanted: as soon as possible while the kitchen is open, or one of the times it offers (every slotMinutes, today and up to scheduleAheadDays ahead). The latest time offered in each opening period is last orders (the close less lastOrdersMinutes: a kitchen that closes at 23:00 with 15 minutes' last orders offers 22:45 at the latest), including periods that run past midnight. A delivery is only offered times it can arrive by. When the kitchen is closed and customers can't order ahead, checkout says when it opens again and the order can't be placed. In the step-by-step checkout (sites without unified checkout) the choice appears under the delivery step once a method is chosen (deliveryChooser, field requestedTime: ASAP or one of the offered times).

Postcode check

GET /api/public/v1/local-delivery/check?postcode=LS1%206HD answers "Do we deliver to you?" for the storefront (see the API reference, tag Restaurant Delivery). When a restaurant delivers there, the reply also makes that restaurant the shopper's own store (see below) and names it (storeName, storeSlug); the postcode itself is never kept.

On the menu page (the orderingStatus banner, and Bistro's header strip) a successful check collapses the checker to one line, "Clatter Shoreditch delivers to N1 7 · Change": no postcode box, the branch is now the shopper's store, and the banner's opening hours switch to it without a reload. Only Change opens the box again (and only that tap moves focus into it; nothing takes focus when the page opens). The browser remembers the delivery area (the district, never the full postcode, and the restaurant's name) in localStorage, so the line stays on later visits for as long as that restaurant is still the shopper's store. A postcode that isn't delivered to keeps the box open with the message, so they can try another or choose Collection.

On the checkout and order confirmation pages Bistro's header strip keeps the open / closed line but drops the Collection / Delivery switch: the checkout's own delivery options choose how the order arrives.

The shopper's own restaurant

A shop with several restaurants remembers which one the shopper chose, in the same bdgr_store cookie as click and collect's My store, and starts from it:

  • the checkout puts their restaurant first in the collection list and the collection card shows it;
  • the booking form (/book and the tableBooking block when it isn't tied to one restaurant) opens on it, unless the link names another (?location=);
  • the menu's status banner says whether it is open and how long orders take there, while it is taking orders (when it is closed or paused the shop's status says who is);
  • the delivery postcode check sets it to the restaurant that delivers to the postcode.

A link can choose a restaurant with ?store=<store page address> on any storefront page (/p/menu?store=leeds, /book?store=leeds); MyStoreHintInterceptor remembers it and the page that opens already uses it. An address that isn't one of the shop's store pages is ignored.

The checkout's collection stores

The step-by-step checkout (Nova and Brock) lists a few collection stores as cards beside the delivery options. From four stores up they fold into one: the shopper's own restaurant, else the nearest to the delivery address already entered (same postcode district, then area), else the first by name, with Change store (search by name, town or postcode) to open the list (CollectionStorePicker, deliveryChooser/store-picker.js). Without JavaScript every store shows as a card. The step's heading says what it offers: Delivery or collection, Collection when that is all (or what was chosen), Collection and delivery for a split order, and the theme's own "Delivery" for delivery alone.

The kitchen

A restaurant order (collection or local delivery from a location that takes food orders) goes to that location's kitchen when placed. Its card payment is only authorised then. It moves through these states:

State Means Order pipeline
PLACED Waiting to be accepted (submitted; payment authorised)
ACCEPTED Being prepared; the customer has a promised time payment captured
READY Ready to collect or to go out packOrder
OUT_FOR_DELIVERY With a driver dispatchOrder
COMPLETED Delivered or collected DELIVERED / dispatchOrder
REJECTED Turned down, or not accepted in time cancelOrder (hold released, customer emailed)
CANCELLED Cancelled some other way

Stock

With Multiple stock locations on, a restaurant order is served from its kitchen's stock, the same way a table is. Kitchens neither post orders nor offer click and collect, so the stock a retail shopper sees would count nothing at all. Before the customer picks where it comes from, a dish can go in the basket up to the most any kitchen taking orders has (or what can be posted, if that is more). Once they pick, only that kitchen's stock counts, and the basket is checked against it. The basket and mini basket mark a dish out of stock by the same figures. At payment the stock is taken there: a collection at the store it is collected from, a local delivery at the location it is cooked at, or at the main location if that one keeps no stock of its own. With one stock location, everything comes from the main location as before.

An order placed after last orders, while paused, or for a time no longer offered is refused at checkout with a message, and its card hold is released. Restaurant collections don't use the click-and-collect arrival flow. The kitchen API for the Badger app (tag Kitchen) and the customer's tracking page are described in the API reference.

In the admin

  • Catalogue › Menu choices lists the modifier groups with a Mark sold out / Back on switch per option, and edits a group's question, selection rules and options (price in pounds, dietary flags, allergens). It shows how many menu items use each group.
  • A product with the Menu item type gets a Menu item card: dietary flags (one heat level at most; the hottest ticked is kept), allergens, and the choice groups it offers, in order. The card owns the dietary and allergens attributes and modifierGroupIds; while the product isn't a menu item the card is hidden and its values are left alone.
  • The location form's Takes food orders tick (under What happens here, shown when the shop has restaurant ordering on, or the location already takes food orders) sets ordering.enabled and shows the Restaurant ordering card. The Hours card's Food orders taken sets the ordering periods (up to two a day; During opening hours keeps none, so the opening hours apply). The card edits the rest of ordering, and shows the live status with Pause new orders and Resume. A pause is never changed by saving the form. The Local delivery area card beside it sets the delivery area (UK postcode prefixes to deliver to and not to, and a radius); it also shows for a location that is not a kitchen when a local delivery option goes out from it.
  • A delivery option's How it gets there card sets the method Local delivery, the location it goes out from, the minimum order and the time on the road.
  • Orders › Kitchen (ADMIN, SUPERADMIN) is the web stand-in for the app, laid out for a tablet on the pass: lanes New, Preparing, Ready and Out for delivery (Board.preparing()/ready()/out()), and the day's finished orders folded under Completed today. Tickets are packed for a narrow lane: the number, delivery or collection and a red flag when anything on it carries an allergen on one row; ASAP or the requested time and the live clock on the next; then the dishes (quantity, name, size, choices, the customer's note) with their allergens as icons, then one big button for the next move, any other move a small button beside it (just its icon on a narrow ticket). Accept is a grid of preparation-time chips. A live ticket shows no price (the order page and Completed today do). The allergen icons (admin/allergens.html: a sprite of the 14 UK FIR allergens drawn once on the board, and chips/glyphs fragments any admin page can reuse, from AllergenMark lists) are solid red tiles that say their name on hover, on keyboard focus and on a tap (admin-ui.js); a name that isn't one of the 14 is kept and drawn as a warning sign with the name written out. Card.allergens(line) and TableCard.allergens(line) turn the lines' FIR names into marks (Allergen.parseName), and allergens() gathers a ticket's for its compact tile. admin-kitchen.js runs the clocks (time waiting and the auto-reject countdown on new orders, "late" against the promise on cooking and out-for-delivery ones, and on a ready ticket how long it has been waiting ("Ready 4 min" to be collected, "Waiting 4 min" for a driver) counted from when it became ready (Card.readySince(), the kitchen history, else readyAt) rather than down to the promise; all from epoch-ms data-* attributes corrected by the server's clock), redraws the board every 15 seconds (not while a turn-down reason is being typed; Completed today stays open or shut), chimes when a new order arrives with a sound switch remembered on the device, and shows the count in the tab title. Moves go through KitchenService: Accept, Turn down (reason), Ready, Out for delivery, Delivered, Collected. Styles are in admin-kitchen.css (bx tokens). A compact view (default at 8+ live orders, Board.liveCount(), or the Compact switch, remembered per browser) shows tiles (Card.itemCount(), summary(), hasNote(), the allergen icons) that open the whole ticket in a drawer, where accept and turn down live; the safe next move stays on the tile. Board.allDay() adds up dishes in New and Preparing; the last moved order (flash kitchenMoved) can be recalled; keys V, R, J/K, Enter. Each button carries a key made when the board was drawn, so a double click acts once.
  • The admin order page shows each line's choices and note, and a Kitchen card with the state and its times.
  • Platform upgrade 2026.10-restaurant switches restaurant ordering on and the main location's kitchen for a shop that opts in, adds the menu page layout (MenuStereotypes.ensure), and lists the steps it can't do (hours, address, giving a page the menu layout). The tracking link needs nothing per site: the order confirmation and email show it for every food order.

The confirmation repeats the time chosen

When the customer chose a time at checkout rather than as soon as possible, the order confirmation page (Nova, Brock and Bootstrap) and the confirmation email repeat it: "Collection at 16:00 today", "Delivery for 16:00 tomorrow", or "Collection at 16:00 on Sat 17 Oct" for a later day. The wording is built in Java by RequestedTimeWording in the kitchen's time zone (the location's, else the shop's) and passed as requestedTimeText (on the page model by OrderSummaryExtension, in the email model by SendOrderConfirmationEmail); the templates only show it. An ASAP order adds nothing.

MCP

modifierGroups / manageModifierGroups (with setAvailable, the sold-out switch), the menu object on manageProducts (dietary, allergens, modifierGroupIds), ordering, deliveryArea, pause and resume on manageLocations, localDelivery on manageDeliveryOptions, and orders kitchen (orders by kitchen state, with choices, notes and allergens). See MCP & OAuth2.

Moving a site

Site data transfer carries modifier groups (catalogue/modifier-groups.json) and remaps each product's modifierGroupIds; locations carry their ordering settings and delivery area, never a pause. See Site Data Transfer.

Demo

The ember demo site (Ember & Oak, a pizza and curry takeaway) has a full menu with choices, allergens, ordering hours, a delivery area and local delivery. See Demo Sites.