Foodops
All docs

Guides · Updated 2026-08-23 · vd59e731

Online Ordering Setup (Merchant Go-Live)

Audio narration for this article is generated with the weekly docs refresh.

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-sectors CRUD, 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-Mode header 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 broadcasts MenuUpdated over SignalR to the organisation group — open customer storefronts drop their cache, refetch, and show a toast. The receiver is confirmed on foodops.ordering.api MenuUpdateController (route api/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-update POST → cache invalidation → MenuUpdated broadcast → 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-update POST 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 new IsOrderOwnedBySessionAsync phone-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-submit endpoint on PaymentController lets 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 (hardcoded PaymentMethod = "cash", settled in person). Guest checkout is restricted to dine-in/takeaway — delivery is explicitly rejected server-side. OrderSubmissionService now branches on a new IsGuest flag: 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 + OutletOnlineOrdersToggled SignalR broadcast + counter window-event re-sync, the menu publishing pipeline (MenuUpdateNotificationServiceapi/menu/notify-update → cache invalidation + MenuUpdated), per-channel variant flags enforced via X-Order-Mode, sector-charge config endpoints, and per-outlet payment-method/allow-payment-without-acceptance flags. Operating-hours configuration NOT re-verified this pass. Confidence medium; all three toggle surfaces pending live screenshots.

  • 2026-06-29 — codebase: NEW-STACK re-verification for July-10 launch (legacy haafai.app.foodops monolith fully retired). Dropped the monolith source_repos glob and the legacy Settings → Restaurant Outlets toggle surface — Accept Online Orders now has two flip surfaces (counter SessionOnlineOrdersTab + admin OutletDetailsSection), backed by hubits-api-sales RestaurantSettingsController (confirmed present on main). Re-pinned counter 32669a9f3a, admin f6ffd0b19d, sales 19190b9a04, ordering.api 80e07fdf7a. Menu-publishing pipeline: receiver confirmed on foodops.ordering.api MenuUpdateController (api/menu/notify-update, [SkipApiKeyValidation], broadcasts MenuUpdated); 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.

Go beyond POS.
Choose the total solution.

Focus on creating the best experience for your guests while we handle the rest.