Plain-English summary
Getting your restaurant onto the customer ordering site is mostly per-outlet configuration: the outlet's reference code becomes the storefront URL, the outlet logo becomes the storefront branding, your existing menu is published to the ordering site automatically (with per-item channel flags deciding what shows for dine-in vs pickup vs delivery), delivery sectors set the delivery charges, and the payment methods you enable decide how customers can pay. The master switch is the outlet's Accept Online Orders toggle — flippable from the counter app or the admin backoffice; flipping it broadcasts over SignalR so every connected counter (and the customer-facing banner) reacts live without a refresh.
When you'd use this
- Launching online ordering for a new outlet (the go-live checklist below)
- Pausing orders during a kitchen rush and resuming after — without touching operating hours
- Working out why a customer "can't see" an item, the delivery option, or a payment method
- Re-branding: changing the storefront URL slug or logo
- Verifying a menu edit actually reached the customer site
Walkthrough — the go-live checklist
Code-grounded; UI labels quoted from source.
1. Storefront URL — the outlet reference code
The outlet form's reference code is the public slug: customers reach the storefront at order.foodops.io/{referenceCode}, which the ordering app resolves via GET /menu/outlet/reference/{code}. Set/changed in the outlet details form — see Outlets. Changing it changes the URL (old links/QRs stop resolving), so print materials after the code is final. [code: foodops.ordering.app: src/components/outlet/OutletValidator.tsx]
2. Branding
The outlet logo uploads on the same outlet form and renders in the storefront header (height-constrained); when no logo is set, the FoodOps wordmark shows instead. [code: foodops.ordering.app: src/components/layout/Header.tsx; see screenshot 142 in online-ordering.md]
3. Services and delivery sectors
- Enable the Delivery and/or Takeout services on the outlet (Services section) — these gate the order-type buttons customers see.
- Create delivery sectors (name + per-sector delivery charge) — backed by
api/settings/restaurant/delivery-sectorsCRUD, edited in the admin app's sector form dialog. The sector's charge lands on the customer's totals. See Deliveries and Outlets. [code: hubits-api-sales: RestaurantSettingsController.cs:749-863]
4. Payment methods
Per outlet, each storefront payment option is independently enabled: BML card gateway, Cash, Bank Transfer (slip upload). The storefront fetches the enabled set per outlet and hides the rest; with none enabled, checkout cannot complete. Also decide the per-outlet allow payment without acceptance flag — whether customers can pay before staff accept the order. [code: foodops.ordering.api: PaymentController.cs:57-141]
5. Menu publishing — automatic, with per-channel flags
There is no separate "publish" button: your POS menu is the storefront menu.
- Every item is online by default; per item-variant DineIn / Takeout / Delivery flags control which order modes it appears in (the storefront sends an
X-Order-Modeheader and the ordering API filters server-side). Turn all three off to keep an item POS-only. - Edit propagation pipeline: a menu change in the back-office POSTs to the ordering API's
api/menu/notify-update; that endpoint invalidates the ordering API's menu cache and broadcastsMenuUpdatedover SignalR to the organisation group — open customer storefronts drop their cache, refetch, and show a toast. The receiver is confirmed onfoodops.ordering.apiMenuUpdateController(routeapi/menu/notify-update,[SkipApiKeyValidation], "Receive menu update notifications from foodops and broadcast via SignalR"). The notifier previously lived in the monolith (MenuUpdateNotificationService); with the monolith retired it is emitted by the FoodOps menu backend — the exact new emitter is an open verification item. [code: foodops.ordering.api: Foodops.Ordering.Api/Controllers/MenuUpdateController.cs:15,39,76]
6. The master switch — Accept Online Orders
One per-outlet boolean (IsAcceptingOnlineOrders), one backing endpoint (POST outlets/{outletId}/toggle-online-orders on the sales API), two places to flip it (the legacy monolith Settings → Restaurant Outlets surface is retired at the July-10 launch):
| Surface | Where | Notes |
|---|---|---|
| Counter app | Session Details modal → Online Orders tab → "Accept Online Orders" switch ("When off, this outlet stops accepting orders from the ordering app.") | Online-only control — disabled while the counter itself is offline. [code: foodops.counter.app: SessionOnlineOrdersTab.tsx] |
| Admin backoffice | Outlet details → "Accept online orders" switch ("Allow customers to place orders through online channels.") | [code: foodops-admin-app: OutletDetailsSection.tsx] |
Flipping it broadcasts OutletOnlineOrdersToggled over SignalR, so other open counters update their switch live (the counter also re-syncs the modal via a window event). [code: hubits-api-sales: RestaurantSettingsController.cs; foodops.counter.app: SessionDetailsModal.tsx]
7. What customers see while paused
With the toggle off, the storefront shows the amber "{outlet} is not accepting online orders at this time" banner on the menu and cart, and the ordering API rejects submission server-side — the banner isn't just cosmetic. Dine-in (table-QR) and group-cart orders are exempt (treated as in-person), and in-flight orders continue normally. [code: foodops.ordering.api: OrderSubmissionService.cs; foodops.ordering.app: MenuPage.tsx, CartSidebar.tsx]
Key concepts
- Reference code = URL — the outlet's slug is the whole "domain setup"; no DNS or per-restaurant deployment.
- One toggle, three surfaces, one broadcast — counter, admin, and legacy settings all hit the same endpoint; SignalR (
OutletOnlineOrdersToggled) keeps every surface honest in real time. - Push-based menu publishing — back-office edit →
notify-updatePOST → cache invalidation →MenuUpdatedbroadcast → storefront refetch. No cron, no manual sync. - Channel flags, enforced server-side — DineIn/Takeout/Delivery per variant; the ordering API filters by
X-Order-Mode, so misconfigured clients can't leak items across channels. - Pause ≠ closed — the toggle is for rushes; operating hours are separate configuration. Toggle off still serves the menu (browse-only banner); out-of-hours messaging is its own behaviour.
Common questions
Q: What's the minimum to go live with online ordering? A: An outlet with a reference code, at least one published menu item flagged for Takeout or Delivery, at least one enabled payment method, the relevant service (Takeout/Delivery) on, and the Accept Online Orders toggle on. Sectors are only needed for delivery.
Q: Where do I find my storefront link to share?
A: order.foodops.io/{your outlet reference code}. The reference code is on the outlet details form — see Outlets.
Q: How do I pause orders during a rush? A: Fastest path is on the counter: Session Details → Online Orders tab → flip "Accept Online Orders" off. Customers immediately see the not-accepting banner and the server rejects new takeaway/delivery submissions; flip it back when the kitchen clears.
Q: Does pausing affect dine-in QR ordering at tables? A: No — table-QR and group-cart orders are treated as in-person and bypass the toggle.
Q: Do menu changes need a re-publish?
A: No. Edits propagate automatically: the back-office notifies the ordering API, which clears its cache and pushes MenuUpdated to open storefronts. If a change doesn't show, that pipeline is where to look (see Edge cases).
Q: How do I keep an item off the ordering site but on the POS? A: Turn off the variant's online channel flags (DineIn/Takeout/Delivery ordering availability) in the item edit form.
Q: Why can't customers see the Delivery button? A: Either the Delivery service is off for the outlet, or they're in a table-QR session (locked to dine-in). Check the outlet's Services section first.
Q: How do delivery charges get set? A: Per delivery sector — each sector carries a charge; the customer's totals include it. Configure sectors in the admin app; see Deliveries.
Q: Which payment options should I enable? A: Any subset of BML card / Cash / Bank Transfer per outlet. Cash needs no setup but routes through pickup counters or the rider cash loop; transfer requires staff to verify uploaded slips; BML requires your gateway credentials configured.
Q: What is "allow payment without acceptance"? A: A per-outlet flag deciding whether customers can pay immediately ("Make Payment") or must wait for staff to accept first ("Place Order"). [code: foodops.ordering.api: PaymentController.cs:141]
Q: I flipped the toggle on one counter — will other counters know?
A: Yes, live: the toggle broadcasts OutletOnlineOrdersToggled over SignalR and connected counters update without a refresh.
Q: Can I change the storefront URL later? A: Yes, by editing the reference code — but printed QRs and shared links with the old code stop working, so treat it as a breaking change.
Edge cases and known issues
- Toggle is online-only on the counter — the counter's Online Orders tab disables the switch while the device is offline ("Available online only."); you can't queue a pause for when connectivity returns. [code: SessionOnlineOrdersTab.tsx]
- Stale switch after modal re-open — the counter deliberately force-refreshes session details after toggling to bypass its cache; if the switch ever looks wrong, close/reopen the modal or check another surface (admin app) for the true state. [code: SessionOnlineOrdersTab.tsx]
- Menu edit not appearing on the storefront — the propagation chain has three failure points: the menu backend's
notify-updatePOST to the ordering API (best-effort — logs failures, doesn't retry), the ordering API's cache invalidation, and the storefront's SignalR connection. A customer hard-refresh bypasses the last hop; the first two need log checks. - Operating hours are separate — the toggle does not implement "Currently Closed" out-of-hours behaviour; hours configuration was not re-verified in this pass against current code.
- No staging environment for the admin app — admin-side toggles act on production data directly (admin app deploys main → prod with no staging env).
Related
Online Ordering Platform — the umbrella entry (customer walkthrough, recent changes, troubleshooting)
Online Ordering Storefront — what customers experience once you're live
Outlets — outlet form (reference code, logo, services, QR codes) and the toggle's settings-page home
Deliveries — sectors, the delivery queue, rider dispatch
Kitchen Display — where accepted online orders fire KOTs
User Stories: Online Ordering — US-ORD-009/010/011/019 cover this entry's scope
2026-07-30 — codebase: foodops.ordering.api (7b91621..96a238e) hardens the Bank Transfer upload flow cited in section 4/Common questions, in the file this entry already cites (PaymentController.cs). UploadTransferReceipt's response DTO gains two fields: Rejected (true when the AI-extraction verdict flags the upload as not a transfer slip at all) and a customer-facing RejectionReason ("That file doesn't look like a bank transfer slip. Please upload the receipt from your banking app."). This is a stronger tier than the already-documented NeedsReview path — NeedsReview lets the customer confirm through to staff review, Rejected means the file can never settle the order and the customer must pick a different one; the doc comment confirms transfer/process still re-checks this server-side regardless, so it's UX-only, not a new security boundary. Separately, transfer/process's failure response now surfaces result.ErrorMessage (e.g. a specific rejection reason) instead of a generic "Failed to process transfer payment" string. Neither hunk contradicts the entry's documented claim that "transfer requires staff to verify uploaded slips" — it just adds an earlier, more specific rejection message for slips that are obviously invalid. Worth a line in Common questions (bank transfer) once screenshots exist; not urgent enough to edit existing prose. The OrderSubmissionService.cs changes in this same commit range (duplicate-submission guard) were not evaluated — diff unavailable (PATCH-UNAVAILABLE) — flagged as an open verification item, not assessed here. (auto-applied by the truth pipeline; adversarially gated)
2026-07-30 — codebase: foodops.ordering.api (7b91621..1f7c4a2) hardens the Bank Transfer payment path in files this entry cites for the Payment methods section (PaymentController.cs, OrderSubmissionService.cs), without changing the documented enablement/config surface. ProcessExistingOrderTransferPayment now hard-rejects a transfer payment in two new cases previously only warn-but-allowed: (1) the uploaded document's AI slip-verdict says it isn't a transfer slip at all (blank/unrelated image) — now returns TransferPaymentProcessResult.Rejected with a customer-facing message instead of proceeding; and (2) the slip's bank reference number (now persisted to a queryable ChequeNumber column rather than only prose Remarks) is checked against every other transfer payment in the org, rejecting the payment if that same slip already settled a different order (replay/reuse prevention). UploadTransferReceipt's response DTO also gains Rejected/RejectionReason fields so the customer sees the 'not a transfer slip' message immediately on upload, before attempting to pay. This is additive user-facing behavior on the Bank Transfer flow — new rejection reasons at upload/pay time — not a change to which payment methods merchants can enable, the per-outlet 'allow payment without acceptance' flag, or the general Cash/BML flows described in section 4. Worth a line in Edge cases/Common questions once corroborated with a live storefront screenshot; not verified against the UI this pass. (auto-applied by the truth pipeline; adversarially gated)
2026-07-27 — codebase: hubits-api-sales (c3662fc..bf7ac10) fixes a cache-path bug in RestaurantSettingsController.GetDeliverySector, in the file this entry cites for delivery-sector config (section 3). BuildDeliverySectorByIdAsync previously read CallContext.ApplicationOrganisationId directly inside the HybridCache.GetOrCreateAsync factory delegate; because the AsyncLocal CallContext doesn't reliably flow into that factory, a cache MISS could resolve org as Guid.Empty and incorrectly 404 a valid sector. The fix captures orgId from CallContext in the caller and passes it in as a parameter (matching every other Build*Async helper). This is a reliability/correctness fix for sector lookups under caching, not a change to the documented CRUD behavior, charge semantics, or admin-app sector form workflow — no edit needed to the entry's claims. Worth noting in case intermittent 'delivery sector not found' reports predate this fix. Unrelated commits in this same range (per-org rate limiting/admission control, a stampede-protected order-details micro-cache, and Phase-1 tenancy/org-resolution changes) touch other controllers/services not cited by this entry's surface and were not evaluated here. (auto-applied by the truth pipeline; adversarially gated)
2026-07-19 — codebase: foodops.ordering.api (e860cc1..10edb52) adds two user-facing behaviors not covered by this entry, both in files it already cites. (1) OrderSubmissionService.SubmitOrderAsync now runs a server-side Out-of-Stock guard (ADR 0002) right before the accept-online-orders check: every cart line (guest or authenticated) is re-checked against live Available Stock (CurrentStockLevel - CommittedStockLevel, respecting AllowNegativeStock), and if any item can no longer be fulfilled the whole order is rejected with a named list of unavailable items (
OrderSubmissionResult.CreateOutOfStock) instead of silently succeeding — closing the gap where a stale/cached menu lets a sold-out item sit in an open cart. This is a new order-rejection reason alongside the already-documented 'outlet not accepting online orders' rejection and belongs next to it in Edge cases/Common questions. (2) PaymentController's payment-processing endpoints (cash/process,bml/initiate,transfer/upload-receipt,transfer/process) now require[CustomerAuthorize]plus a newIsOrderOwnedBySessionAsyncphone-match check before acting on an order id — closing a 'pay/settle any order id' IDOR. This is backend auth-hardening, not a change to which payment methods merchants enable or how, so it doesn't contradict the documented Payment methods section. Separately, MenuUpdateController's anonymous-access marker was refactored from the custom[SkipApiKeyValidation]attribute to standard ASP.NET[AllowAnonymous](part of a broader removal of the custom API-key-validation middleware); an inline TODO confirms the endpoint is still intentionally anonymous for foodops's server-to-server notify-update call, so the entry's described menu-publishing/broadcast behavior is unaffected — no edit needed, since per the verification rule method behavior (not the attribute name) governs, and behavior is unchanged. No existing documented claim in this entry (toggle, payment-method enablement, menu-publishing pipeline, channel flags) is contradicted by this diff. (auto-applied by the truth pipeline; adversarially gated)2026-07-04 — codebase: foodops.ordering.api (80e07fd..87682c4) adds a new guest/walk-in order path not covered by this entry. A new
POST order/guest-submitendpoint onPaymentControllerlets a customer submit an order with only a typed name (no session token, OTP, or phone) — the order links to the organisation's shared Walk-In Customer, the guest name is folded into order notes (Guest: {name}), and no payment is processed (hardcodedPaymentMethod = "cash", settled in person). Guest checkout is restricted to dine-in/takeaway — delivery is explicitly rejected server-side.OrderSubmissionServicenow branches on a newIsGuestflag: guest orders skip session validation entirely and use client-supplied inline items, which are re-priced server-side from the DB (same source as the anonymous cart-totals endpoint) to prevent price tampering, rather than loading a session-keyed server cart. This is additive to, not a contradiction of, the documented Payment methods and Menu publishing behavior — the existing BML/Cash/BankTransfer enablement and per-channel flags govern the normal registered-customer flow; the guest path is a separate, unauthenticated walk-in flow. Worth a new subsection (or a sibling entry) once the client-side entry point (counter kiosk? dine-in table flow?) is identified — not yet located in this pass. Flagging as an open verification item rather than editing existing sections, since no currently-documented claim is contradicted. (auto-applied by the truth pipeline; adversarially gated)2026-06-11 — codebase: entry seeded against
foodops.counter.app@1f77fc6,foodops-admin-app@bcde336,hubits-api-sales@20fc7ca,haafai.app.foodops@a8ea20b,foodops.ordering.api@d44572f(all prod branches). Written because the umbrella entry's "Restaurant Management → Configuration" section was thin and partly ungrounded; this entry adds the code-verified go-live chain: reference-code URL resolution, the three toggle surfaces +OutletOnlineOrdersToggledSignalR broadcast + counter window-event re-sync, the menu publishing pipeline (MenuUpdateNotificationService→api/menu/notify-update→ cache invalidation +MenuUpdated), per-channel variant flags enforced viaX-Order-Mode, sector-charge config endpoints, and per-outlet payment-method/allow-payment-without-acceptanceflags. Operating-hours configuration NOT re-verified this pass. Confidencemedium; all three toggle surfaces pending live screenshots.2026-06-29 — codebase: NEW-STACK re-verification for July-10 launch (legacy
haafai.app.foodopsmonolith fully retired). Dropped the monolith source_repos glob and the legacy Settings → Restaurant Outlets toggle surface — Accept Online Orders now has two flip surfaces (counterSessionOnlineOrdersTab+ adminOutletDetailsSection), backed byhubits-api-sales RestaurantSettingsController(confirmed present on main). Re-pinned counter32669a9f3a, adminf6ffd0b19d, sales19190b9a04, ordering.api80e07fdf7a. Menu-publishing pipeline: receiver confirmed onfoodops.ordering.api MenuUpdateController(api/menu/notify-update,[SkipApiKeyValidation], broadcastsMenuUpdated); the notifier moved off the retired monolith — exact new emitter flagged as an OPEN verification item (not located in hubits-api-inventory/sales or foodops.api by filename this pass). Channel flags, sector-charge config, payment-method enablement unchanged.
Additional verification events are appended here by the weekly KB sync and by manual re-verification passes. See _meta/failure-log.md for the global failure index.