Foodops
All docs

Ordering · Updated 2026-08-21 · v60ad199

Online Ordering Platform

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

Plain-English summary

Foodops isn't just the back-of-house POS — it also ships with a customer-facing ordering website. Customers browse your menu at order.foodops.io/your-restaurant, add items to cart, pick delivery or pickup, and pay online. Online orders flow into the counter in real time via SignalR. Delivery itself is now managed from the counter (a Delivery order-type) backed by the sales API's delivery service — riders are assigned and tracked there. The separate Flutter rider/driver app (foodops.delivery.mobile) and its old backend (foodops.delivery.api) are dormant/legacy and not a launch surface — see Rider App.

When you'd use this

  • Enabling online orders for the first time (restaurant launch)
  • Explaining "how does a customer actually order?" to new staff or a voice agent
  • Debugging "my customer says they ordered but I didn't see it" — trace the pipeline from website to POS
  • Planning delivery zones, operating hours, minimum order amounts
  • Onboarding a delivery driver onto the mobile app

Summary

Foodops includes a customer-facing online ordering website where end-users can browse menus, place orders for delivery or pickup, track order status, and pay online. Built with React 19 and SignalR for real-time updates.

What changed since 2026-04-15 (NEW 2026-05)

The customer-facing ordering site (foodops.ordering.app) and its backend (foodops.ordering.api) saw substantial activity in this window — 25 substantive commits across both repos. This entry has the strongest real-signal of the FoodOps top-level entries.

Ordering app (foodops.ordering.app) — UX additions

  • Anonymous-friendly authentication flow (NEW) — guest customers can now browse the menu, build a cart, and reach checkout without an account; the AuthSheet only prompts for login when payment requires it (6687c8a 2026-04-20). Deep cart-persistence rewrite touched useCart, useCartSync, cartSlice, OutletValidator, TableValidator.
  • Name-collection step in auth — anonymous checkout now collects a customer name before order submission (new NameStep.tsx component, fdc82c3 2026-04-22). Drives delivery-address and group-cart attribution.
  • Group cart improvements — multi-customer ordering at one table is now smoother: server-side cart clearing after order submission (3c1e538), profile prefill cleanup in JoinGroupCartModal (ee910dd 2026-05-03), keyboard inset handling for iOS (f77f831), authentication gate on GroupCartButton (138190f). Group-cart hub endpoint now skips API key validation (b6d2989 2026-05-03 in api).
  • Order mode header — outgoing API requests now include an order-mode header so the menu API can filter items by session type (dine-in vs delivery vs pickup) — pairs with the api-side order mode handling for menu items and categories change (e9a8554 ordering.app + ae159b4 ordering.api).
  • Modifier group display — modifier groups now sort by display order (9150862 2026-04-30); UI updated for clearer hierarchy (touches EditCartItemDrawer, EditGroupCartItemDrawer, ItemDetailsDialog).
  • Product image aspect ratio — product cards now use a consistent aspect ratio container for layout stability (8b30951).
  • Delivery address handling for anonymous users — table context is cleared on outlet selection (bb3690d).
  • Outlet logo handling — height-constrained image URL utility and storage-sync improvements for the outlet logo in AppLayout/Header (0cc2a42, b0ed6f8).
  • Casing normalization — "Foodops" branding is now consistent across configuration and UI files (068257b 2026-04-15).

Ordering API (foodops.ordering.api) — backend evolution

  • SignalR notifications enhancement — order status and transfer-slip uploads now flow over richer SignalR events, paired with the counter.app 1337cbe subscription change so cashiers see online-ordering updates in real time (2b6ad34 2026-05-02).
  • Outlet retrieval with logo URL resolution — queries now include logo images, supporting the new outlet logo handling on the ordering app (f765452).
  • GroupCart OutletId validation — error handling improvements in CreateGroupCart (89c1a07).
  • Production deployment refactor — switched to Windows production deployment with revised SCP/SSH workflow (commits 2026-04-27); e1772da corrects the production DefaultConnection string.
  • OpenTelemetry / Uptrace tracing — observability improvements in staging (e321b50, b5682bb, 3e91073).

Implications for users

  • Customers can now order anonymously end-to-end, making the ordering site less friction-heavy for first-time visitors.
  • Group-cart (multiple devices ordering against one table simultaneously) is more reliable: real-time invalidation, server-side cart clearing on submit, and richer notifications.
  • Counter and online ordering are now bidirectionally SignalR-connected (counter.app 1337cbe subscribes to /orderHub; ordering.api 2b6ad34 enriches notifications).
  • Item availability per order mode (dine-in vs delivery vs pickup) is now enforced end-to-end — items not available for delivery no longer appear in the customer's delivery cart.

Verified 2026-05-04 via Playwright against live E2E environment (order.foodops.io/e2e-test-outlet): The outlet selection page, menu page, ItemDetailsDialog, CartSidebar, and AuthSheet (Phone + OTP steps) were all captured. Screenshots 141–148 below.

Foodops online ordering — outlet selection page (enter outlet code, Continue button, "Don't know your outlet code?" helper)

Foodops online ordering — menu page showing header "Welcome to E2E Test Outlet", category pills, search bar, view-mode toggle, and product cards

Foodops online ordering — ItemDetailsDialog for Coffee: price MVR 48.60 (Base price incl. tax), Allergies (Optional) field, quantity counter, Add to Order button

Foodops online ordering — CartSidebar with 1 item (Coffee), Takeout/Delivery order-type buttons, totals, Notes field, "Login to place order" CTA

Foodops online ordering — CartSidebar with Takeout order type selected (dark button)

Foodops online ordering — AuthSheet PhoneStep: "Log in to continue", "We'll send a one-time code to verify your number.", phone field, "Request OTP" button

Foodops online ordering — AuthSheet PhoneStep with +9607779999 filled in, "Request OTP" button enabled

Foodops online ordering — AuthSheet OtpStep: "Enter verification code", "Code sent to +9607779999", 6-digit OTP boxes, "resend in 35s" countdown, "Verify" button

GroupCartQRCodeModal, NameStep, and group-cart participant badging remain code-documented only (require a live table QR session and two authenticated devices).

Tech Stack

Component Technology
Frontend React 19, TypeScript, Vite
State management Redux Toolkit, TanStack Query
Styling Tailwind CSS
Real-time SignalR (order status updates, group cart sync, transfer slip uploads)
URL pattern order.foodops.io/{restaurant-slug}

Customer Features

Menu Discovery

  • Category-based navigation
  • Search by item name
  • Item details with description, modifiers, and photos
  • Favorites/wishlist (sign-in required — anonymous visitors cannot see or save favourites)

Ordering

  • Add items to cart with modifier selection (modifier groups sorted by display order as of 2026-04-30)
  • Anonymous-friendly checkout — browse, build cart, and proceed without an account; auth prompt only at payment-time (2026-04-20)
  • Name-collection step for anonymous orders (collected before submission, drives delivery + group-cart attribution)
  • Delivery or pickup selection
  • QR-code dine-in ordering (scan table QR, order from phone)
  • Group cart — multiple customers at one table can join the same cart and submit together; server-side cart clearing keeps the next session clean
  • Order-mode-aware menu filtering: items can be configured to appear only for dine-in, delivery, or pickup (enforced via X-Order-Mode API header)
  • Estimated preparation and delivery time

Order Tracking

  • Real-time status: Placed → Accepted → Preparing → Ready → (Enroute → Delivered | Picked Up | Served), driven by SignalR
  • Order history and re-ordering (sign-in required)
  • Push notifications on status changes

Payments

  • Cash on Delivery (COD)
  • Online card payment (via configured gateway)
  • Bank transfer

Customer Engagement

  • Customer profiles
  • Post-order CSAT (satisfaction survey) — fires after every successful order with cool-off period to avoid repeat prompts
  • Loyalty/rewards program integration

Walkthrough — customer's view

The walkthrough below traces the path a real customer takes through foodops.ordering.app (deployed at order.foodops.io for prod, dev-order.foodops.io for staging). Per-step references show the source-of-truth file. Confidence markers: [code: …] for direct matches in the codebase, [code-inferred] for derived behaviour, [UI-TODO] for things still awaiting visual capture.

1. Landing / outlet selection

Two entry surfaces exist:

  • Direct outlet URL: order.foodops.io/{outletCode} (e.g. /myrestaurant) — shared by the restaurant on social, flyers, etc. The OutletValidator route resolves the slug via GET /menu/outlet/reference/{code}, persists outlet to local storage, and forwards to /menu. [code: src/router.tsx:34], [code: src/components/outlet/OutletValidator.tsx:31]

  • QR/table URL: order.foodops.io/table/{tableId}/login — printed on table tents. The TableValidator resolves the table, sets dine-in context, and forwards to /table/{tableId}/menu. [code: src/router.tsx:46]

  • Bare root / redirects to /select-outlet [code: src/router.tsx:21], where the customer either:

    • Picks a recently-visited outlet from a history list (cards labelled with outlet name, code, last-visited timestamp). [code: src/features/auth/pages/OutletSelectionPage.tsx:178]
    • Taps "Add New Outlet" and types the code into a free-text field with placeholder e.g., myrestaurant, then hits "Continue". [code: src/features/auth/pages/OutletSelectionPage.tsx:222]
    • Helper text reads "Don't know your outlet code? Contact your restaurant." [code: src/features/auth/pages/OutletSelectionPage.tsx:272] Confirmed: the helper text appears below the Continue button. See screenshot 141.

    Outlet selection page — outlet code input with placeholder "e.g., myrestaurant" and helper text below Continue button

Picking an outlet wipes any prior cart (clearCartState) and any leftover table context, so the customer starts fresh. [code: src/features/auth/pages/OutletSelectionPage.tsx:50]

2. Menu browse (anonymous OK)

After outlet validation the customer lands on /menu (or /table/:tableId/menu). What they see:

  1. Header — outlet logo (height-constrained URL utility added 2026-04-30) and outlet name. [code: src/components/layout/Header.tsx:142] Confirmed: the header shows the FoodOps logo and "Welcome to / E2E Test Outlet". When no outlet logo image is set, the FoodOps icon-only mark is shown (the header dropped the full wordmark fallback for a smaller icon-only logo as of de66ad7, 2026-07). See screenshot 142.
  2. Sticky search bar with placeholder "Search for food items…" and a view-mode toggle (list / single-grid / multi-grid; mode persisted to localStorage.menuViewMode). [code: src/features/menu/pages/MenuPage.tsx:449] Confirmed: toggle buttons visible top-right of the search bar.
  3. Category tabs — auto-select via Intersection Observer as the customer scrolls. [code: src/features/menu/pages/MenuPage.tsx:155] CHANGED: category selectors are now compact text-only chips on every screen size — the emoji icon tile shown on desktop (🥤 Beverages, 🍔 Burgers, 🍰 Desserts, 🍟 Sides) has been removed, and mobile/desktop now share the same text-chip design. [code: src/components/menu/CategoryTabs.tsx]
  4. Product cards in the chosen layout, each tappable to open ItemDetailsDialog (modifiers, special instructions, allergies, qty). Modifier groups sort by configured display order (shipped 2026-04-30). [code: src/components/menu/ItemDetailsDialog.tsx] Confirmed: tapping Coffee opens the dialog with Allergies (Optional), Special Instructions (Optional) fields, quantity counter, and "Add to Order" button.

Menu page — header, category pills, search bar, view toggle, and product cards in multi-column grid layout ItemDetailsDialog for Coffee — price MVR 48.60 (Base price incl. tax), Allergies and Special Instructions fields, qty counter, Add to Order button 5. Live menu refresh — outgoing requests carry the X-Order-Mode: dine-in header when a tableId is present, otherwise pickup-delivery. The API uses this to filter items per the per-variant DineIn / Takeout / Delivery flags. [code: src/lib/api/client.ts:40], [code: src/store/menuApi.ts:35], [code: foodops.ordering.api: Models/Menu/OrderMode.cs:7] 6. Real-time menu updates — when staff edits items in the POS, a SignalR MenuUpdated event resets the menuApi cache and shows a toast. [code: src/features/menu/pages/MenuPage.tsx:101]

Anonymous users can do all of the above. The "Favorites" tab is gated — the useGetFavoritesQuery is skipped when not authenticated, so the heart icons are inert and the favourites page shows a LoginRequiredEmptyState. [code: src/features/menu/pages/MenuPage.tsx:39], [code: src/components/auth/LoginRequiredEmptyState.tsx]

  1. "Not accepting online orders" banner (NEW 2026-05-11, 9b3efa63) — the menu page fetches live outlet data and, when the outlet's IsAcceptingOnlineOrders flag is off, shows an amber info banner: "{outlet name} is not accepting online orders at this time." The same banner appears above the cart's checkout button. Dine-in (table-QR) sessions and group-cart submissions are exempt — the server treats those as in-person orders, so the banner is hidden for them. This is the customer-facing surface of the staff "Pause ordering" toggle. [code: src/features/menu/pages/MenuPage.tsx:50], [code: src/features/menu/pages/MenuPage.tsx:457], [code: src/components/cart/CartSidebar.tsx:548] [code: foodops.ordering.api: 0bc7bc18 — enforce online-ordering checks on order submission]

3. Cart (anonymous-friendly)

Tapping the cart icon opens a bottom-sheet drawer (CartSidebar). For an anonymous customer:

  • Cart items live in a Redux slice persisted to localStorage (key foodops_cart_snapshot). The useGetCartQuery server call is skipped. [code: src/components/cart/CartSidebar.tsx:240]
  • Totals are now server-computed even for anonymous carts (CHANGED since 2026-05-25): the cart POSTs its client-side items to POST /cart/totals/anonymous so the server returns the real tax + service-fee breakdown. The old behaviour (flat local subtotal, no tax) survives only as a brief fallback while that call is in flight or if it errors. [code: src/components/cart/CartSidebar.tsx:253], [code: src/components/cart/CartSidebar.tsx:274], [code: src/store/cartApi.ts:114]
  • Multi-tax (multi-GST) breakdown (NEW 2026-05-25, 9bfc125a): when the API returns a structured taxes array, the totals panel renders one row per component (e.g. CGST (2.50%), SGST (2.50%)); otherwise it falls back to a single Tax Total line. [code: src/components/cart/CartTotals.tsx:58], [code: src/utils/taxComponents.ts:18]
  • Order-type buttons render: Takeout / Delivery, plus Dine In when a tableId is present. [code: src/components/cart/CartSidebar.tsx:633]
  • Choosing Delivery opens AddressSelector, which itself is auth-gated; for anonymous users it shows a login prompt instead of an address list. [code: src/components/cart/CartSidebar.tsx:533]
  • Order-type / address gates are deferred for anonymous users so the checkout button stays tappable. [code: src/components/cart/CartSidebar.tsx:538]

For an authenticated customer the cart hits GET /cart and GET /cart/totals (the totals query carries the lower-cased order type as a query param so the server can apply tax + service fees correctly). [code: src/components/cart/CartSidebar.tsx:246]

The CTA at the bottom of the drawer changes label depending on auth + outlet config:

  • Anonymous: "Login to place order" [code: src/components/payment/PaymentButton.tsx:107]
  • Authenticated, outlet allows immediate payment: "Make Payment"
  • Authenticated, outlet requires staff acceptance first: "Place Order"
  • Dine-in with allowPaymentWithoutAcceptance=true: a paired "Place Order" + "Make Payment" layout [code: src/components/payment/PaymentButton.tsx:373]

4. Anonymous (guest) checkout — the new flow

Tapping "Login to place order" opens the AuthSheet bottom drawer. [code: src/components/auth/AuthSheet.tsx:127]

Step 1 — Phone

  • Title: "Log in to continue"
  • Description: "We'll send a one-time code to verify your number."
  • Single field: a tel input with placeholder "Enter your phone number", min length 7. [code: src/components/auth/PhoneStep.tsx:71]
  • Button: "Request OTP". Calls sendOtp({ phoneNumber, outletId }), which returns a prefill OTP in dev mode. The outletId was added 2026-05-24 (2eac734b) so the backend routes the SMS to the provider matching the outlet's country — Fast2SMS for India, MsgOwl for Maldives — which is what now makes Indian (and other non-Maldives) phone numbers usable for online ordering. [code: src/components/auth/PhoneStep.tsx:47]

Step 2 — OTP

  • Title: "Enter verification code"
  • Description: "Code sent to {phoneNumber}"
  • 6-digit OTP input (auto-prefilled in dev). [code: src/components/auth/AuthSheet.tsx:180]

Step 3 — Profile check

  • Title: "Signing you in…", a spinner while the app calls loadCustomerProfile(phone). If the customer has a name on file (and it's not the placeholder "N/A"), the sheet closes. [code: src/components/auth/AuthSheet.tsx:80]

Step 4 — Name (only if name missing) — the new step shipped 2026-04-22

  • Title: "What should we call you?"
  • Description: "Set your name so we can personalise your orders."
  • Body copy: "We'd like to know what to call you. This will be used on your orders." [code: src/components/auth/NameStep.tsx:56]
  • Single text field, placeholder "Your name", max 80 chars, min 2 chars. [code: src/components/auth/NameStep.tsx:68]
  • Button: "Save". POSTs to /customers/update (same endpoint as the profile page). [code: src/components/auth/NameStep.tsx:43]

After the sheet closes, the auth-success callback automatically resumes the original action — PaymentButton re-runs handleCheckout with fresh state and the order is placed without a second tap. [code: src/components/payment/PaymentButton.tsx:316]

Cart hand-off after login: useCartSync replays each anonymous cart item to POST /cart/add, then re-pulls the canonical server cart via GET /cart and replaces local state. The PaymentButton awaits this sync via awaitCartSync() before submitting, so the server cart is authoritative when the order request fires. [code: src/hooks/useCartSync.ts:39], [code: src/components/payment/PaymentButton.tsx:128]

What guest checkout requires: phone number (verified via OTP) + name. That's the minimum. Email is optional and only collected from the Profile page later. Delivery orders additionally require an address. Phone is sufficient for the customer to track the order afterwards by signing back in with the same number. [code-inferred]

5. Order mode selection

Order mode is decided by the URL/context, not a separate page:

  • Customer hits /table/{tableId}/menu (QR scan) → the cart defaults to dinein and the tableId is bound. [code: src/components/cart/CartSidebar.tsx:311] Outgoing menu requests carry X-Order-Mode: dine-in.
  • Customer hits /menu (no table) → menu requests carry X-Order-Mode: pickup-delivery; the cart drawer shows the Takeout / Delivery toggle. [code: src/lib/api/client.ts:40]

The API treats X-Order-Mode as advisory: OrderModeParser.Parse accepts dine-in/DineIn or pickup-delivery/PickupDelivery (case-insensitive), returns null for missing/unknown values (which skips channel filtering). MenuController passes the parsed value into every menu read endpoint (categories, products, search, top-selling). [code: foodops.ordering.api: Foodops.Ordering.Models/Menu/OrderMode.cs:21], [code: foodops.ordering.api: Foodops.Ordering.Api/Controllers/MenuController.cs:134]

Result: a customer scanning a table QR sees only items flagged dine-in; a customer on a delivery URL never sees dine-in-only items. The decision is enforced server-side, so out-of-channel items can't sneak in via the cart even after switching order types.

6. Group cart (multi-device, one shared cart)

Group cart is the second-shipped multi-device flow. It's exposed via the GroupCartButton component, which only renders when both outletId and tableId are present (i.e. dine-in QR sessions). [code: src/components/cart/CartSidebar.tsx:569]

Host flow:

  1. Authenticated customer opens the cart, taps the "Start Group Order" chip (Users icon, primary tint). [code: src/components/groupCart/GroupCartButton.tsx:194]
  2. If anonymous, the auth sheet opens first (group orders require an authenticated host on the API side). [code: src/components/groupCart/GroupCartButton.tsx:54]
  3. StartGroupCartModal opens with copy: "How it works — You'll get a QR code that others can scan to join your group. Everyone can add items, but only you can place the final order." Display name input is prefilled from the profile. [code: src/components/groupCart/StartGroupCartModal.tsx:111]
  4. Footer note: "The group order will expire after 2 hours of inactivity." [code: src/components/groupCart/StartGroupCartModal.tsx:174]
  5. On submit, the host gets a 6-character share code and a QR code modal opens automatically with three actions: Copy Link, Share (Web Share API when available), and a live participant counter. The QR encodes a deep link of the form order.foodops.io/join/{shareCode}. [code: src/components/groupCart/GroupCartQRCodeModal.tsx:36]

Joiner flow:

  1. Second customer scans the QR or pastes the link. The JoinGroupCartPage at /join/:shareCode calls GET /group-cart/info/{shareCode} to fetch outlet, table, host name, and participant count. [code: src/pages/JoinGroupCartPage.tsx:68]
  2. If the joiner is on the wrong outlet/table, the page realigns context (saves the group's outlet + tableId, clears any incompatible local cart). [code: src/pages/JoinGroupCartPage.tsx:111]
  3. Anonymous joiners get a non-dismissible auth sheet (the API requires a session for group joins). [code: src/pages/JoinGroupCartPage.tsx:147]
  4. After auth, JoinGroupCartModal shows outlet name, table name, host display name, and participant count, then asks for the joiner's display name (prefilled from profile if available, max 50 chars). Submit calls POST /group-cart/join. [code: src/components/groupCart/JoinGroupCartModal.tsx:194]
  5. The cart drawer now shows items grouped by participant with (You) / ★ Host badges and a per-participant subtotal. Anyone can add or remove their own items. [code: src/components/cart/CartSidebar.tsx:766]

Real-time sync via groupCartHub: A second SignalR connection at {api-origin}/groupCartHub (separate from the order hub) fires events CartUpdated, ParticipantJoined, ParticipantLeft, CartSubmitted, CartExpired. Each event triggers a refetch + cache invalidation; toasts surface joins/leaves. [code: src/services/groupCartSignalRService.ts:113], [code: src/components/cart/CartSidebar.tsx:118]. The groupCartHub endpoint was added to the API key validation skip list on 2026-05-03 so unauthenticated WebSocket upgrades reach the hub before the per-message session check (b6d2989).

Submit + cleanup: Only the host's CTA is enabled. On submit:

  • Group cart is converted to a sales order; participants get a CartSubmitted SignalR event with a "The host has placed the group order!" toast and their drawer closes. [code: src/components/cart/CartSidebar.tsx:135]
  • Server-side cart-clear runs against the host's personal cart (added 2026-04-30 in 3c1e538 to fix a bug where the host's items reappeared because their personal server cart still held them). [code: src/components/payment/PaymentButton.tsx:140]
  • All local storage keys (foodops_cart_snapshot, foodops_group_cart_id, groupCart) are wiped via clearAllCartsAndStorage. [code: src/components/payment/PaymentButton.tsx:150]

A participant leaving the group keeps the group alive; the host leaving ends it for everyone (warning copy in the confirmation dialog distinguishes the two). [code: src/components/groupCart/GroupCartButton.tsx:114]

7. Order tracking after submit

After a successful order submission:

  1. The cart drawer closes; if a CSAT question is configured for "ordering" and isn't in cool-off, a CSATSurveyDrawer opens for a rating + optional review. [code: src/components/cart/CartSidebar.tsx:428]
  2. After CSAT (or skip), the app navigates to /orders (or /table/{tableId}/orders for dine-in).
  3. OrdersPage lists orders split into Ongoing / Completed, sortable, searchable. Each card shows the per-order-type progress bar:
    • Delivery: Placed → Accepted → Preparing → Ready → Enroute → Delivered (6 steps)
    • Takeout: Placed → Accepted → Preparing → Ready → Picked Up (5 steps)
    • Dine-in: Placed → Accepted → Preparing → Ready → Served (5 steps) [code: src/features/orders/utils/orderUtils.ts:55]
  4. Live updatesuseSignalR hook subscribes to the /orderHub SignalR endpoint and joins the organisation group. Three event names map to status updates: OrderStatusUpdated, KitchenStatusUpdated, OrderUpdated (all funneled through one wrapped callback). The page mutates orders in place — no full refetch unless a totals-changing event fires. [code: src/services/signalRService.ts:134], [code: src/features/orders/pages/OrdersPage.tsx:72]
  5. Backend enrichment shipped 2026-05-02 (2b6ad34): order-status SignalR notifications now include richer payloads (transfer-slip uploads etc.). Pairs with the counter.app subscription change so cashiers see online-ordering updates the same way customers do.
  6. Connection lifecycle: the SignalR client uses automatic reconnect, attaches an online listener for offline recovery, and re-joins the group after reconnect. [code: src/services/signalRService.ts:96]

Anonymous customers can re-find their order list later by signing in with the same phone number — the orders are bound to the customer record, not to the device session. [code-inferred]

Restaurant Management

Incoming Orders

  • New orders trigger notification sound + alert on the Foodops POS
  • Staff reviews order: items, delivery/pickup, customer info, payment status
  • Accept with estimated prep time → customer notified
  • Reject with reason → customer notified, refund if pre-paid

Configuration

  • Operating hours (per day, with holiday overrides)
  • Delivery zones and fees
  • Minimum order amount per zone
  • Pause ordering temporarily (for rush periods)
  • Menu item availability toggle (out of stock)

Delivery App (Driver) — DORMANT/LEGACY

Warning: Not a launch surface. The standalone Flutter rider/driver app (foodops.delivery.mobile) and its old backend (foodops.delivery.api) are dormant/legacyfoodops.delivery.api's last commit was 2025-11-27, and the mobile app has no CI (manual build only). For the July-10 launch, delivery is handled inside the counter as a Delivery order-type, backed by the sales API delivery service (hubits-api-salesDeliveryController/DeliveryOrdersController/DeliveryService, rider/driver auth via DeliveryAuthorizeAttribute). See Rider App and Deliveries. The historical Flutter driver flow below is retained for reference only.

Platform Technology
Mobile (legacy) Flutter (iOS + Android) — dormant
Features Order dispatch, map navigation, delivery status updates, COD tracking

Driver Flow (legacy app — reference only)

  1. Receive delivery notification
  2. See restaurant address, customer address, order details
  3. Navigate to restaurant → pick up order → mark "Picked Up"
  4. Navigate to customer → deliver → mark "Delivered"
  5. If COD: record cash collection amount

Key concepts

  • Online ordering site — public-facing React app at order.foodops.io/{restaurant-slug}. No login required for guest checkout.
  • Restaurant slug — the URL-friendly identifier for each restaurant (e.g., kb-test-restaurant). Set in Settings, acts as the public URL path.
  • QR-code dine-in — printed table QR codes link straight to a table-attached ordering experience. Customer scans, orders, order shows on the POS and KDS tied to the correct table.
  • SignalR — the real-time transport that keeps status updates flowing between the ordering site, the counter, and the customer's tracking page.
  • COD (Cash on Delivery) — payment collected by the driver at the door. Reported separately from online payments for reconciliation.
  • Delivery zone — a geographic area with its own fee and minimum order amount. Customer address resolves to one zone at checkout.

Common questions

Q: How do customers find my online ordering page? A: Share the URL directly (order.foodops.io/your-slug), print it on flyers, add it to social media bios, or use the generated QR-code sheets. Foodops doesn't auto-distribute — discoverability is your marketing job.

Q: Does every menu item automatically appear online? A: Yes by default. Toggle individual items off via the Item edit form (Ordering Availability = off) if you want to keep something POS-only.

Q: Can I pause online orders during a rush without taking the site offline? A: Yes — the outlet's online-ordering toggle (IsAcceptingOnlineOrders) pauses new takeaway/delivery orders. While it's off, customers see an amber "{outlet} is not accepting online orders at this time" banner on the menu and cart, and the server rejects online order submission. Dine-in (table-QR) and group-cart orders are unaffected. Existing in-flight orders continue normally. (Customer-side banner shipped 2026-05-11.)

Q: What happens if a customer orders something out of stock? A: If the item's recipe ingredients are below threshold, the item shows as unavailable automatically. For items without recipes, staff can mark them out-of-stock manually via the Item edit form.

Q: How do customer-initiated orders appear at the POS? A: They arrive with a distinctive notification sound and appear on the Counter queue under the "New" tab. Staff accept with prep time (goes to customer), or reject (auto-refund if pre-paid).

Q: Can customers order from a table by scanning QR? A: Yes — generate per-table QR codes from Settings → Outlets → Floor Layout → QR Codes. Customer scans, menu loads pre-tied to that table, order appears on the POS with the table assigned.

Q: What payment methods does the ordering site support? A: Online card payment (requires configured gateway), Bank transfer, Cash on Delivery. Methods configurable per outlet in Payment Methods settings.

Q: Can customers see where their delivery driver is on a map? A: Live driver-location-on-a-map tracking depended on the standalone Flutter rider app, which is now dormant/legacy. For the July-10 launch, customers see order status (Placed → Accepted → Preparing → Ready → Enroute → Delivered) over SignalR; the live map is not a supported launch feature.

Edge cases and known issues

Customer complains order didn't arrive on POS

  • Symptom: Customer shows confirmation but no order appears in Foodops
  • Cause: Network hiccup during order handoff, or the accept-notification was dismissed before the order loaded
  • Workaround: Check /orders under the New tab and also under Closed if someone accidentally closed it. Contact support if order ID exists in customer view but not in POS — there's a recovery flow.

Online ordering out of hours

  • Symptom: Customer tries to order, site says "Restaurant closed"
  • Cause: Operating hours configuration
  • Workaround: Settings → Operating Hours → verify today's hours. Holiday overrides take precedence.

Related

  • Online Ordering Storefront + Online Ordering Setup — deep-dives split from this entry: the customer web app (payments, OTP routing, tracking links) and the merchant go-live checklist (toggle surfaces, menu publishing pipeline)

  • User Stories: Online Ordering — 20 scenarios

  • Pricing — all plans include ordering website

  • About Hubits — tech stack overview

  • POS Takeaway Order — how online orders integrate with the counter

  • Kitchen Display — online orders appear here once accepted

  • 2026-08-09 — codebase: foodops.ordering.app 4e8d3b2..2c35685 (commit subjects: DrawerBody layout refactor — identical to the range already logged 2026-08-06 from the same base 4e8d3b2, no new information there — plus 'implement useOtpInput hook', 'enhance order type handling with polling and service availability checks', and 'enhance payment modals to skip cart query for unauthenticated users'). The order-type-availability commit is the genuinely new thread intersecting this entry's §3/§5 Cart and CartSidebar walkthrough: CartSidebar now derives isDineInAvailable/isDeliveryAvailable/isTakeoutAvailable from three new outlet flags (treated as available unless explicitly false, so outlets without the flags set — i.e. all current behavior — are unaffected), and polls useGetOutletByIdQuery every 15s while the cart drawer is open instead of once on mount, so a service an admin disables mid-session (Settings → Outlets → Services, per the new code's own comment) disappears from the order-type picker live. Three concrete, not-yet-documented effects: (1) the §5 dine-in-default-on-table-QR behavior is now skipped when the outlet has dine-in switched off; (2) an order type the customer already had selected auto-clears (dispatch(setOrderType(null))) the moment its toggle flips off mid-session, instead of being submitted and rejected server-side; (3) isOrderingBlocked gains a third hasNoAvailableOrderTypes condition — if every service usable in the current session is off (group carts hinge on dine-in alone since they always submit as dine-in; everyone else needs at least one of takeout/delivery/table-dinein), ordering is blocked entirely. This is additive to, and distinct from, the existing IsAcceptingOnlineOrders pause-ordering gate the entry's Common Questions already documents ('Dine-in (table-QR) and group-cart orders are unaffected' by THAT toggle specifically) — it doesn't contradict that answer, but means dine-in/group-cart CAN now be blocked via this separate per-service Settings toggle, which is worth a caveat. Nothing in the entry's existing '§3 Order-type buttons render: Takeout / Delivery, plus Dine In when a tableId is present' line is contradicted (still the default/common case). Not UI-confirmed this pass (code-only diff; no live capture of a disabled-service order-type picker or the resulting blocked state). The useOtpInput hook and payment-modal-skip-cart-query commits show no hunks intersecting this entry's tracked glob in this diff and should be checked next pass. Recommend bumping verified_against_commit.haafai/foodops.ordering.app to 2c35685 once folded in. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-06 — codebase: foodops.ordering.app 4e8d3b2..0465553 ('feat: update Drawer components to improve layout and add DrawerBody for better content management'). Every bottom-sheet drawer this entry's walkthrough documents (AuthSheet §4, CartSidebar §3, EditCartItemDrawer, EditGroupCartItemDrawer, GuestNameSheet, GroupCartQRCodeModal §6, StartGroupCartModal §6, ItemDetailsDialog §2) drops its per-component max-h-[90vh]/max-h-[95vh] height cap in favor of a new shared DrawerBody wrapper (@/components/ui/drawer) that scrolls its own content (min-h-0 flex-1 overflow-y-auto overscroll-contain) while header/footer chrome stays fixed — a pure layout refactor; no field, button, step, or copy described anywhere in this entry changes. One functional removal worth flagging: AuthSheet.tsx's useKeyboardInset hook — the custom iOS-Safari visualViewport-based workaround this entry's changelog previously attributed to f77f831 ('keyboard inset handling for iOS') — is deleted outright, along with the --auth-sheet-keyboard-inset CSS var, the repositionInputs={false} override, and the inline bottom style that consumed it. The diff doesn't show a replacement mechanism specific to AuthSheet (DrawerBody's own scroll container, or vaul's now-un-overridden default repositionInputs, may or may not compensate), so whether the Phone/OTP/Name steps still clear the iOS keyboard is unconfirmed either way — not a contradiction of documented behavior (the entry never asserted the specific mechanism, only that the feature shipped historically), but worth a live iOS-Safari check on AuthSheet before the next mobile verification pass. Not UI-confirmed this pass (code-only diff). Recommend bumping verified_against_commit.haafai/foodops.ordering.app to 0465553 once folded in. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-06 — codebase: foodops.ordering.api 96a238e..93be673 (three commits, within tracked Foodops.Ordering.Api/** glob). OrderStatusController.SendOrderAcceptedSmsWithBankDetails — the SMS sent to a customer when staff accept an order requiring bank-transfer payment — now quotes the persisted order's actual balance (Outstanding if >0, else GrossTotal) and its real TransactionCurrency code instead of the raw OrderTotal/Currency fields off the caller's request payload; per the new code's own comment, a counter client with a stale or absent local total 'used to make this text "MVR 0.00"' — i.e. customers could previously receive a bank-transfer-payment-request SMS asking them to send zero. This is a correctness fix to a notification this entry already documents at a high level (Restaurant Management > Incoming Orders: 'Accept with estimated prep time → customer notified'), not a new flow, so nothing existing is contradicted — but the previous 'wrong amount' failure mode is worth a one-line caveat once convenient. Two adjacent changes in the same diff are out of scope for this customer-facing entry: (1) Program.cs gives trusted service callers (Sales.Api/Restaurant.App webhooks relaying status updates) a separate, much larger rate-limit bucket (2000/min vs 100/min) on the status-update endpoint so a busy multi-org estate doesn't get throttled into dropped SMS — pure backend/ops, no customer-visible behavior change; (2) six RazorLight invoice/payment/sales-order template-tag views (CompanyAddress.cshtml, CompanyLogo.cshtml) switch from EF-context injection (which per the new code's comment throws a NullReferenceException because RazorLight's cached engine has no service provider) to reading a pre-populated ViewBag.Organisation — a document-generation bug fix that belongs to invoicing/receipt-template territory, not this entry's ordering walkthrough. Not UI-confirmed this pass (code-only diff; no live capture of the corrected SMS text). Recommend bumping verified_against_commit.haafai/foodops.ordering.api to 93be673 once folded in. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-30 — codebase: foodops.ordering.api 7b91621..96a238e (5 commits; intersects this entry's tracked Foodops.Ordering.Api/Controllers + Foodops.Ordering.Core/Interfaces+Services glob). Two threads worth documenting, both extensions of already-logged material rather than contradictions. (1) The bank-transfer hard-rejection flow logged 2026-07-30 from the ordering.app side (paymentApi gaining rejected/rejectionReason fields) now has its API-side counterpart: UploadReceiptResultDto gains Rejected/RejectionReason, set when ITransferSlipValidationService's new derived IsNotATransferSlip property is true — per its doc comment, the ONE verdict that blocks payment outright (every other mismatch — amount, date, missing reference — stays warn-and-allow for counter staff to review manually). IOrderSubmissionService gains a matching TransferPaymentProcessResult.Rejected(reason) factory whose ErrorMessage now flows into PaymentController's transfer/process response message, incidentally fixing a real bug where the old code literally concatenated the raw TransferReceiptDocumentId GUID onto the customer-facing success message string. This is the concrete backend implementation of what was previously inferred only from the client diff — recommend bumping verified_against_commit.haafai/foodops.ordering.api to 96a238e (Foodops.Ordering.Core/Services/OrderSubmissionService.cs's actual order-creation-path handling of this was PATCH-UNAVAILABLE in this diff and should be spot-checked before folding the 07-30 payments note into the entry body). (2) CartService.CalculateTotalsForItemsAsync — the method backing the §3 Cart claim that 'the server returns the real tax + service-fee breakdown' for both anonymous (/cart/totals/anonymous) and authenticated (/cart/totals) carts — is corrected: percentage service fees (rate types 2/3/4) now compute off a new serviceableItemsTotal/serviceableTaxTotal that excludes line items whose ProductCategory has IsServiceFeeApplication=false, matching the filter SalesOrder.UpdateTotals already applies to the final bill. Previously the cart-totals endpoint included service-fee-exempt items in the fee base, so a restaurant with exempt categories configured would have shown customers a pre-checkout service fee higher than what the bill actually charged. Doesn't contradict the entry's existing 'real tax + service-fee breakdown' claim — it makes that claim more accurate — but worth a one-line mention once convenient. Out of scope for this entry: an OrderStatusController fix correcting a silent EF INNER-JOIN bug that dropped customer-less (walk-in/counter/POS) orders from the status-SMS phone lookup entirely (doesn't affect online-ordering customers, who always have a real CustomerId from OTP auth), and a new IOrderSubmissionService.EnsureKitchenTicketsAsync guarding against a paid-but-ticket-less order after a transient mid-transaction failure — reads as kitchen-display.md territory, not this entry's customer-facing walkthrough. Not UI-confirmed this pass (API-only diff). (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-30 — codebase: foodops.ordering.app 8c01414..4e8d3b2 ('feat: implement useSingleFlight hook to prevent duplicate submissions in payment processes'). New src/hooks/useSingleFlight.ts wraps an async handler so a second invocation is dropped synchronously (a ref flips same-tick) rather than relying on React state (isProcessing/isSubmittingOrder/isAcceptingOrder), which per the hook's own doc comment 'only disables the button on the next render — a double-tap can land inside that gap.' It's now applied to the four order-submission entry points this entry's walkthrough documents: PaymentButton.tsx's handleSubmitOrderWithoutPayment (§3/§4 'Place Order' without-payment path) and handleSaveAndAccept (§3 dine-in Save+Accept), CartSidebar.tsx's handleGuestNameSubmit (the no-login, pay-in-person dine-in settle flow), PaymentModal.tsx's handleSubmit (card/transfer payment on an existing order, the same surface as the 2026-07-30-logged transfer-rejection change), and CashPaymentModal.tsx's handleSubmit. Per the hook's comment, prior to this a rapid double-tap on any of these buttons 'used to mean two orders, two invoices, two kitchen tickets and two texts' — a real (if presumably rare) duplicate-order bug across every checkout path this entry walks through. The server has its own duplicate guard per the referenced ordering-api ADR 0003, so this is a client-side hardening layer on top of an existing backend safety net, not a new customer-visible flow or UI change — nothing in the entry's existing text describes single-tap-only submission guarantees, so nothing is contradicted. Not worth a dedicated walkthrough subsection (no visible behavior change on a single tap), but worth a one-line mention if the entry ever gets a 'reliability/edge-case' note on duplicate submissions. Recommend bumping verified_against_commit.haafai/foodops.ordering.app to 4e8d3b2 once folded in. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-30 — codebase: foodops.ordering.api 7b91621..1f7c4a2 (two commits, within tracked Foodops.Ordering.Api/;Foodops.Ordering.Core/ globs). This is the backend half of the transfer-slip hard-rejection flow whose frontend surface was logged in the prior 2026-07-30 note (ordering.app 079a642..8c01414) — it confirms that note's detail and adds two genuinely new, not-yet-documented blocking checks on ProcessExistingOrderTransferPaymentAsync (the existing-order retry/PaymentModal path): (1) TransferReceiptExistsAsync now rejects with 'A transfer slip is required to record a bank transfer payment.' if the submitted document id doesn't correspond to a real uploaded receipt — closing a gap where any well-formed GUID could previously zero an order's balance for free; (2) IsSlipReferenceAlreadyUsedAsync now rejects with 'This transfer slip has already been used to pay another order. Please upload the slip for this payment.' — a genuine bank reference number (now persisted to the Payment.ChequeNumber column specifically to make this queryable) can no longer be replayed to settle a second, unrelated order. A parallel IsSettleableSlipAsync helper applies the document-exists + not-a-transfer-slip checks (cache-only, no new vision-model call) to the new-order-submission path already described in the prior note, though the diff is truncated before showing whether the replay check applies there too — worth confirming next pass. Separately, OrderStatusController.cs fixes a bug where status-update SMS was silently suppressed for any order without a real Customer row (an EF INNER-JOIN-on-required-navigation issue) — but per the code's own comment this affects only counter/POS/walk-in orders (CustomerId == Guid.Empty); online-ordering orders always carry a real Customer record via the documented phone-OTP flow (§4), so this fix doesn't change any online-ordering-documented behavior and is out of scope for this entry (closer to a POS/Counter entry's territory). None of this contradicts the entry's existing Payments section (which just lists 'Bank transfer' as a method) — nothing existing is wrong, but the two new rejection reasons are genuine customer-facing failure messages worth a line in Payments/§4 or the out-of-stock-adjacent Common Questions once UI-confirmed (not captured this pass — code-only diff, no live capture of either rejection toast). Recommend bumping verified_against_commit.haafai/foodops.ordering.api to 1f7c4a2 once folded in. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-30 — codebase: foodops.ordering.app 079a642..8c01414 ('feat: enhance transfer payment process with upload validation and rejection handling'). Bank-transfer slip uploads now get a hard-rejection path distinct from the existing soft needsReview mismatch flow: paymentApi's upload-receipt response gains rejected/rejectionReason fields, set when the server determines the uploaded file isn't a transfer slip at all (as opposed to needsReview, where it looks like one but details don't match and the restaurant is asked to verify manually). Effects on the flows this entry documents but doesn't currently detail at the mechanics level: (1) On an already-placed order's payment retry (OrderDetailsPage), a hard rejection clears the file and shows the rejection reason immediately — no 'upload anyway' confirmation is offered (that's reserved for the softer needsReview case). (2) In PaymentModal, paying for an existing order via transfer now does upload→validate→finalize as two steps; a hard rejection clears the file and surfaces the reason without reporting success. (3) Most notably, when a customer submits a brand-new order with transfer as the payment method and the slip is hard-rejected, the order submission itself is NOT rolled back — the order is still created and the cart is cleared, but the toast changes to '{reason}. Your order was placed — open it from My Orders to upload the slip.' instead of the generic success toast. This means an order can now be placed with transfer selected but no accepted payment slip attached, recoverable only via the order's detail page. None of this is currently described in the entry's Payments section (which just lists 'Bank transfer' as a method) or the walkthrough, so nothing existing is contradicted — but the hard-reject-vs-needs-review distinction and the 'order placed unpaid, retry from My Orders' behavior are worth a short new subsection under Payments/Walkthrough plus a Common Questions entry once UI-confirmed (not captured this pass — code-only diff, no live capture of the rejection toast or the retry-from-order-details path). Recommend bumping verified_against_commit.haafai/foodops.ordering.app to 8c01414 once folded in. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-23 — codebase: foodops.ordering.api 690d322..7b91621 (two commits). (1) CustomersController/CustomerService bug fix directly touching this entry's documented NameStep flow (§4 Step 4 — 'POSTs to /customers/update (same endpoint as the profile page)'): UpdateCustomerAsync now accepts an optional applicationOrganisationId and, when supplied, scopes the customer lookup to that org before updating name/email; CustomersController.UpdateCustomer now resolves organisationId from the active outlet header and passes it through. Per the new code's own comment, 'Without this the update can land on a different org's record for the same phone, so the name never persists for this outlet and the app re-prompts on every login' — i.e. a customer with the same phone number registered under more than one organisation could previously save their name via NameStep, have it silently written to a different org's customer row, and see NameStep reappear on a later visit to this outlet's org despite having 'saved' it. This is a correctness fix, not a contradiction of the entry's existing NameStep description (endpoint and step ordering unchanged) — worth a short caveat in §4 Step 4 once convenient, noting the org-scoping now guarantees a saved name sticks for repeat visits to the same outlet. (2) New TimeZoneHelper.ToIanaTimeZone converts the organisation's stored (possibly Windows-format) time zone id to IANA format and is now surfaced as a new TimeZone field on OutletDto from both outlet-read paths (GetRestaurantOutletsByOrganizationAsync and the single-outlet lookup) — per its doc comment, this exists because 'browsers' Intl/toLocaleString only accept IANA ids, never Windows ids,' implying the ordering.app client will eventually use this to render order-status timestamps in the outlet's local time. Paired with this, OrderSubmissionService now stamps OrderDate/DeliveryDate/Note timestamps with DateTime.UtcNow instead of DateTime.Now (previously server-local, which could silently mis-record order times if the API server's local timezone didn't match the organisation's). Neither the new TimeZone field nor any consumer of it is visible in this diff (API-only; foodops.ordering.app untouched here), so no client-side timestamp-rendering behavior can yet be documented — worth checking foodops.ordering.app for a consumer of OutletDto.TimeZone next pass before adding anything to §7 Order tracking or the walkthrough's timestamp handling. No existing entry text is contradicted by either change. Recommend bumping verified_against_commit.haafai/foodops.ordering.api to 7b91621. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-23 — codebase: foodops.ordering.app 59a13c5..079a642 ('feat: add timeZone property to outlet-related components for improved time handling'). Outlet objects (via AuthSheet, LoginForm, AppLayout, OutletValidator, outletStorage.saveOutlet/loadOutlet) now carry a cached timeZone IANA id (e.g. Asia/Karachi) alongside the existing logo/description fields. A new getOrgTimeZone() helper in orderUtils.ts reads this from the cached outlet in sessionStorage, and formatDate()/formatTime() — used throughout the Order Tracking pages (§7) and now also AttachmentViewerModal for uploaded-file timestamps — now pass it as the explicit timeZone option to toLocaleDateString/toLocaleTimeString. Net effect: order-status timestamps and attachment upload times shown to the customer are now rendered in the restaurant organisation's local time zone (falling back to the browser's own local zone only when no org time zone is cached), rather than unconditionally in the customer's browser-local time zone as before. This entry's Order Tracking section (§7, 'OrdersPage lists orders...') makes no existing claim about which time zone timestamps are shown in, so nothing is contradicted — but it's a genuine, not-yet-documented user-facing behavior (a customer ordering from a different time zone than the restaurant will now see order times in the restaurant's zone, not their own) worth a line in §7 Order tracking and/or the Key concepts section once UI-confirmed. Not UI-confirmed this pass (code-only diff; no live capture of a cross-timezone order-time display). Recommend bumping verified_against_commit.haafai/foodops.ordering.app to 079a642 once folded in. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-21 — codebase: foodops.ordering.app e7625a2..59a13c5 (tracked glob src/**). Most of this range's commits (customization-signature cart-line merging and modifier-surcharge math, productId threaded through productSnapshot for accurate modifier-lookup on edit, full local edits — modifiers/allergies/special instructions — for anonymous cart items via updateItemOptimistic, the submit-time Out-of-Stock rejection flow (ADR 0002: unavailableVariantIds / red cart-line badge / cart banner), and hasActiveRegisterSession now gating isOrderingBlocked in CartSidebar) were already logged in the 2026-07-14, 2026-07-17 and 2026-07-20 verification passes — no new information there. The one genuinely new-to-log thread is menu-browse-time Out-of-Stock treatment on ProductCard / ProductCardGridMulti / ProductCardGridSingle: items where !product.isAvailable now render a red 'Out of stock' badge (new OutOfStockBadge component) over a grayscaled/dimmed product image, hide the Add-to-cart button entirely (previously it was rendered but merely disabled), and make the whole card non-clickable (onClick becomes undefined) so ItemDetailsDialog can no longer be opened for a sold-out item at all — a change from the prior behavior where out-of-stock cards stayed tappable with just a disabled Add button. This doesn't contradict the entry's general '(product cards) each tappable to open ItemDetailsDialog' line (still true for in-stock items, the overwhelming majority of browsing) but is a real, not-yet-documented exception worth a short addition to §2 Menu browse step 4 and the 'What happens if a customer orders something out of stock?' Common Question once UI-confirmed — not captured this pass (no screenshot of the badge/grayscale state). Recommend bumping verified_against_commit.haafai/foodops.ordering.app to 59a13c5 once folded in. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-21 — codebase: foodops.ordering.api 10edb52..690d322 (within this entry's tracked Foodops.Ordering.Core/** glob), commit 'feat: Update out-of-stock handling to flag items as unavailable instead of hiding them'. MenuService.cs changes ALL four menu-read paths — GetProductsAsync (category listing), GetGroupedProductsAsync, GetProductVariantsAsync, and SearchProductsAsync — from hiding Out of Stock items (the removed .Where(StockAvailability.IsOrderable) filter / the old RemoveOutOfStock helper, whose own doc comment said it 'prune[d] the already-materialised menu so a depleted item never reaches (nor can be added by) the customer') to a new MarkAvailability helper that keeps every item on the menu and instead sets IsAvailable = false on out-of-stock variants/products — per its doc comment, 'so the app shows them greyed-out and blocks ordering.' This reverses the previous pre-emptive-hide model across category browse, grouped listing, single-variant lookup, and search — not just the submission-time safety net already logged 2026-07-20 (cartSlice's unavailableVariantIds, which that note explicitly distinguished as 'reactive at submit time' versus the (then-hiding) menu-browse behavior). The existing Common-Questions answer ('If the item's recipe ingredients are below threshold, the item shows as unavailable automatically') isn't contradicted — if anything it now matches the code more precisely than it did against the prior hide-based implementation — but it doesn't capture the mechanism (item stays visible/greyed rather than disappearing) or that this now applies uniformly to search and category listings too. Worth expanding the Q&A and §2 Menu browse once the ordering.app client's actual greyed-out rendering is UI-confirmed (this diff is API-only; no ordering.app consumer of the flag verified this pass). Recommend bumping verified_against_commit.haafai/foodops.ordering.api to 690d322. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-21 — codebase: foodops.ordering.app e7625a2..9a1b9ea. The cartSlice unavailableVariantIds (ADR 0002 out-of-stock-at-submission), CartItem.tsx red 'Out of stock' line badge, and CartSidebar hasActiveRegisterSession -> isOutletClosed -> isOrderingBlocked gating in this diff are the same material already logged in the 2026-07-20 verification-history note — no new information there. What IS new: menu-browse-time out-of-stock treatment on ProductCard.tsx, ProductCardGridMulti.tsx and ProductCardGridSingle.tsx. Each now derives isOutOfStock = !product.isAvailable and, when true, renders a new OutOfStockBadge component, applies a grayscale/opacity filter to the product image, greys out the name/price text, and disables card-tap-to-open. CHANGED from prior behavior: the Add-to-cart button is now hidden entirely for out-of-stock items rather than rendered visible-but-disabled (previously disabled={!product.isAvailable || !product.isEnabled || isAdding}; now the whole button is wrapped in {!isOutOfStock && (...)} with only !product.isEnabled || isAdding gating it). This is the concrete UI behind the entry's existing Common-Questions line 'the item shows as unavailable automatically' — worth enriching that Q&A (and possibly the Menu Discovery bullets) with the badge/grayscale/hidden-button/no-click detail once UI-confirmed live. Also re-confirms (not new) productId now threading through productSnapshot on these same card components, matching the 2026-07-20 note. No existing entry text is contradicted by this diff. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-20 — codebase: foodops.ordering.app e7625a2..26cd36c. Two new user-facing threads, neither contradicting existing entry text. (1) Out-of-Stock-at-submission handling (ADR 0002): cartSlice gains a transient unavailableVariantIds field, populated when an order submission is rejected because some cart lines sold out between add-to-cart and checkout (result.unavailableItems in CartSidebar's submit handler). Affected lines get a red 'Out of stock' badge (CartItem.tsx) plus a red cart-level banner, and the flag auto-clears the moment the cart changes (qty edit, item add/remove, or a fresh server cart pull) so a retry re-checks cleanly. This is a race-condition safety net distinct from the existing 'item shows unavailable in the menu' Q&A (recipe-threshold/manual out-of-stock) — that's pre-emptive at menu-browse time; this is reactive at submit time. Worth a line in the out-of-stock Q&A and §3 Cart once UI-confirmed (not captured this pass). (2) hasActiveRegisterSession — flagged 2026-07-17 as API-only/unconsumed — now has a client consumer: CartSidebar derives isOutletClosed = outletData?.hasActiveRegisterSession === false and folds it into isOrderingBlocked, which per the new code's own comment blocks submission for EVERY order type including dine-in and group-cart (unlike the existing IsAcceptingOnlineOrders pause toggle, which the entry correctly documents as dine-in/group-cart-exempt). This is a separate, broader gate (register/POS session closed) layered on top of the documented online-ordering pause — doesn't contradict the existing Pause-ordering Q&A but is worth a caveat there once UI-confirmed. Separately (minor, not yet doc-worthy): anonymous cart item edits now apply full edits locally (modifiers/allergies/special instructions, not just quantity) via updateItemOptimistic, resolving a previously-code-commented limitation; and EditCartItemDrawer + ProductCard/FavoriteProductCard/grid variants now thread a real productId through productSnapshot so modifier-group lookups for cart edits resolve correctly even when the older variant-id-as-productId bug was in play. Recommend bumping verified_against_commit.haafai/foodops.ordering.app to 26cd36c. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-20 — codebase: haafai/hubits-api-sales 939e821..99e6847 (within this entry's tracked glob: Services/Counter/Delivery/**). Two changes intersect DeliveryService.cs, the backend the entry cites as now powering the counter-based Delivery order-type ('delivery is handled inside the counter... backed by the sales API's delivery service'). (1) Driver profile-photo upload now writes to S3 via the shared IBlobService instead of local wwwroot disk — per the new code's own comment, the old local write '404s from every other instance and is lost on redeploy,' meaning driver profile photos were silently broken/lost under any multi-instance deployment and are now durable. (2) HandoverCashAsync's pending-cash-handover lookup is now scoped by ApplicationOrganisationId (joined through SalesRegisterSession) instead of DeliveryRiderId alone — a tenant-isolation fix, since per the code's comment a single rider account can carry pending handovers under more than one organisation. Both are backend correctness/security fixes with no UI change; this entry currently makes no claims about driver profile-photo upload or cash-handover mechanics, so nothing existing is contradicted. Scope-wise, driver profile photos and rider cash handover read as closer to the Rider App entry's territory than to this Online Ordering entry's customer-facing walkthrough — worth cross-checking rider-app.md's coverage before deciding where (if anywhere) to document this. Not UI-confirmed this pass. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-20 — codebase: haafai/hubits-api-sales 939e821..8ee2cf4 (within this entry's tracked Services/Counter/Delivery/** glob). DeliveryService.cs's driver-profile-photo upload (used when onboarding/editing a rider on the counter's Delivery order-type) switched from writing the file to local wwwroot/uploads/profiles (relative URL, e.g. uploads/profiles/{fileName}) to uploading via the new IBlobService dependency to S3, storing the returned absolute URL. Per the new code's own comment, the old local write 404'd on any server instance other than the one that received the upload and was wiped on every redeploy — i.e. driver profile photos were silently broken in a multi-instance production deployment. This is a real bug fix (drivers can now reliably set/see a profile photo), but it's rider/driver-onboarding-specific plumbing inside the counter's delivery service, not the customer-facing ordering flow this entry documents — this entry explicitly defers driver/rider-side details to Rider App and Deliveries, neither of which is in scope for this pass. No claim in this entry (which never mentions driver profile photos) is contradicted, so no edits. Worth folding into rider-app.md / deliveries.md's driver-onboarding section next time that entry is verified. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-19 — codebase: haafai/hubits-api-notifications d76f3f7..eaf138f (within this entry's tracked hubits-api-notifications glob: EmailsController.cs + Services/). Replaces the SelectPdf HTML→PDF renderer (Services/Email/HtmlToPdfService.cs, deleted) with a hardened Chromium port (new ChromiumHtmlToPdfService.cs + ChromiumBrowserPool.cs) behind the same IHtmlToPdfService interface EmailService.BuildEntityPdfAttachmentAsync calls from EmailsController's Send endpoint. Per the new code's own doc comments, SelectPdf's .NET Core converter needed native browser binaries absent from the aspnet:10.0 runtime image and never started in the container, so EVERY emailed PDF attachment for the covered entity types (invoice, credit note, payment, quote, sales order) was silently dropped in production — BuildEntityPdfAttachmentAsync catches the render failure and sends the email without the attachment rather than failing the send. That's now fixed: the Chromium-backed renderer (capped at MaxConcurrentRenders=3 concurrent pages, 30s per-render timeout, periodic browser recycle) actually produces the PDF, so any email meant to carry one of these document types now includes it. This entry's source_repos glob tracks EmailsController.cs/Services/ for hubits-api-notifications as in-scope for Online Ordering, but the diff itself doesn't establish that an online-order confirmation/receipt email is among the entity types attached via this path — 'sales order' plausibly maps to an online order's backing record, but that needs confirmation against hubits-api-sales' email-trigger code before adding anything to this entry's walkthrough, §Payments, or Common Questions. Not UI-confirmed this pass. No existing entry text is contradicted — the entry currently makes no claims about emailed PDF receipts/attachments. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-17 — codebase: foodops.ordering.api e860cc1..10edb52. Two user-facing/security-relevant additions beyond the out-of-stock filtering (which contradicted the existing out-of-stock Q&A, see edits[]): (1) Cart modifier prices are now server-authoritative — CartService.cs's new ResolveModifierPricesFromCatalogAsync re-reads each modifier's price (and backfills name/group) from RestaurantModifierGroupItem by id before both the duplicate-line signature check and the stored cart item, neutralising price to 0 for any id that doesn't resolve to a real catalog option. This closes a price-tampering/omission gap in AddItemToCartAsync and UpdateCartItemAsync and complements the modifier-equality fix already logged in the 2026-07-13 verification note — worth a line in §3 Cart once convenient ('modifier prices are resolved server-side from the catalog, not trusted from the client'). (2) OutletDto (via GetRestaurantOutletsByOrganizationAsync, the outlet-switcher's sibling-outlet source per the 2026-07-10/07-11 notes) gained a new HasActiveRegisterSession field, computed by checking for an open SalesRegisterSession on the outlet — comment states this exists so the app can warn up-front that order submission needs an open till session, mirroring an existing submission-time check. This is API-only in this diff; no ordering.app usage confirmed yet, so not ready to document as a walkthrough behavior. Separately, the API key auth path was refactored (SkipApiKeyValidationAttribute removed, replaced by ApiKeyAuthenticationHandler + AuthSchemes with an explicit fallback-policy comment on MenuController) — this is an internal architecture change with no observed customer-facing effect (menu browsing still authenticates via the same X-API-Key under the new fallback policy); not entry-relevant unless a future pass documents API auth internals. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-14 — codebase: foodops.ordering.app 04776e0..e7625a2 (commit e7625a2, 'feat: enhance cart item management with customization signatures and modifier surcharges') changes the client-side local-cart merge logic in cartSlice.ts's addItemOptimistic reducer — the CLIENT-side counterpart to the API-side CartService.cs modifier-equality bug flagged in the 2026-07-13 note, but a separate repo/file and a materially bigger behavior change. Previously, addItemOptimistic matched an existing cart line by productVariantId alone, so adding the same base item twice with DIFFERENT modifier selections (or different allergies/special-instructions notes) would merge into a single line, summing quantities and recomputing totalPrice as quantity * unitPrice — silently dropping any modifier surcharge from the total. The new buildItemSignature() builds an order-independent key from productVariantId + a sorted modifierId:quantity list + trimmed allergies + trimmed special instructions, and lines now merge only when the full signature matches; a new perUnitModifierTotal() folds each modifier's price x quantity into the merged line's totalPrice via quantity * (unitPrice + perUnitModifierTotal(item)). The same signature-based merge and surcharge math is applied to updateItemQuantity. Net effect for the customer: adding the same product with different addon/modifier selections, different allergy notes, or different special instructions now correctly produces separate cart lines instead of silently merging and losing surcharge pricing; changing a merged line's quantity now correctly includes modifier surcharges in the total. This entry's §3 Cart section and the 'Add items to cart with modifier selection' bullet under Ordering don't yet describe this merge/split-line behavior — worth a short addition once UI-confirmed (add the same item twice with different modifiers, confirm two distinct lines with correct per-line totals). (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-13 — codebase: foodops.ordering.api 93d14db..e860cc1. Three of the four commits in this range (outlet-metadata cache disablement, OutletTypeId added to OutletDto, GetRestaurantOutletsByOrganizationAsync + organizationId param on GET /menu/outlets) are re-containments of changes already logged in the 2026-07-10 and 2026-07-11 verification-history notes — same code, no new information, no entry text affected. The genuinely NEW item is a cart-modifier-matching bug fix in CartService.cs's AreModifiersEqual: modifier-equality comparison (used to decide whether two cart lines should merge into one) previously ignored each modifier's Quantity and GroupName, so e.g. a cart line with '1x extra shot' and a separately-added '2x extra shots' selection would incorrectly be treated as identical and merged into a single line — undercounting the modifier quantity in the cart/order total. The fix builds an order-independent signature per modifier (Id|Name|Price|Currency|Quantity|GroupName) and compares signature sets, which also correctly handles duplicate modifier entries that the old one-directional 'every item in list1 has some match in list2' check did not. This is user-facing (affects what a customer sees in their cart line items and totals when adding the same base item with different modifier quantities) but is not yet reflected in this entry's §3 Cart section or the 'Add items to cart with modifier selection' bullet. Not yet UI-confirmed this pass — worth a live cart check (add same item twice with different modifier quantities, confirm two distinct lines) before adding a walkthrough note. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-11 — codebase: foodops.ordering.api 93d14db..465b53e is the API-side implementation backing the outlet-switcher client changes already noted 2026-07-10 (app 2865c43..04776e0), and closes the loop on that note's speculation. The new GetRestaurantOutletsByOrganizationAsync (MenuService.cs), wired through GET /menu/outlets?organizationId=, confirms the server DOES scope sibling-outlet results by both ApplicationOrganisationId == organizationId AND OutletTypeId == Restaurant (plus RecordStatusId == Active) — so the client-side outletTypeId filtering flagged in the 2026-07-10 app note is genuinely defensive/redundant, not compensating for a gap. OutletTypeId is now populated on OutletDto across every outlet-read path (GetOutletsByPortalAsync, GetOutletsByReferenceAsync, GetOutletsByOrganizationIdAsync, GetOutletByIdAsync, GetOutletByReferenceAsync, OptimizedMenuService), consistent with the client consuming it as a Retail(1)/Restaurant(2) discriminator. This diff also re-contains the outlet-metadata no-caching fix already logged at 93d14db..718a6ac (Cache-Control now no-store/no-cache/must-revalidate on GetOutlets/GetOutlet/GetOutletByReference) — same code, no new information there. No documented entry text is contradicted; the outlet-switcher feature itself remains code-only/UI-unconfirmed and still awaits the walkthrough addition flagged in the 2026-07-08/07-10 notes. Recommend bumping verified_against_commit.haafai/foodops.ordering.api to 465b53e. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-10 — codebase: foodops.ordering.app 2865c43..04776e0 refines the outlet-switcher work first flagged (code-only, undocumented) in the 2026-07-08 verification pass, and adds behavior not covered by that note. (1) NEW: the switcher now auto-opens the first time a customer lands on /menu having just come from the outlet-login/slug redirect (location.state.fromOutletLogin), but only if the organization has more than one switchable outlet — a hasAutoOpenedSwitcherRef guard fires it once per mount and then clears the nav-state flag so back/forward or refresh won't reopen it. (2) NEW: the sibling-outlet list is filtered to Restaurant-type outlets only via a new outletTypeId discriminator on OutletDto (1=Retail, 2=Restaurant; null/undefined passes through) — defensive filtering on the client even though the server is expected to already scope by org+type. (3) getOutlets (menuApi.ts) changed its query-arg shape from a bare outletReference string to { outletReference?, organizationId? }, enabling the org-scoped fetch the switcher needs; this is an internal API-contract change with no direct customer-visible surface beyond feeding (1)/(2). (4) The VariantSelectionDrawer immediate-hand-off change already logged 2026-07-08 (no more two-step 'select variant → tap Add Item') is now fully landed — the Button import, footer, and selectedVariantId state are entirely removed, confirming that prior note rather than superseding it. Remaining hunks (CategoryProductSection/MenuPage/ProductWithVariantsCard dark-mode and responsive-spacing tweaks) are cosmetic only and don't change documented behavior. None of this is UI-confirmed yet (code-only this pass) — worth a walkthrough addition under §2/§6 (outlet switcher: auto-open-on-login + Restaurant-only filtering) once a live capture is done, alongside the still-pending OutletSwitcherModal and VariantSelectionDrawer mentions from 2026-07-08. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-10 — codebase: foodops.ordering.api (93d14db..718a6ac) removed response caching on the outlet-metadata endpoints (GET /menu/outlets, GET /menu/outlet/{id}, GET /menu/outlet/reference/{reference}) — previously cached for 1 hour (Cache-Control: public, max-age=3600, s-maxage=7200), now no-store, no-cache, must-revalidate. This is a backend fix, not a documented behavior change: it closes a gap where outlet branding (logo), name, and the IsAcceptingOnlineOrders flag could take up to an hour to reach the customer-facing site after a staff-side change, which sat underneath the entry's existing 'fetches live outlet data' claim for the 'Not accepting online orders' banner (§2) and the outlet-switcher/logo behavior noted in the 2026-07-08 verification entry. No walkthrough text needs correction — the entry never asserted a specific cache TTL — but worth a one-line mention if a future pass adds server-cache-latency details to §2 or the Pause-ordering Q&A. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-08 — codebase: foodops.ordering.app's Header gained a new outlet-switcher surface not yet documented in this entry. When a customer's outlet belongs to an organization with more than one active outlet, the 'Welcome to {outlet}' text becomes a tappable button (chevron indicator) that opens a new OutletSwitcherModal (Drawer) listing sibling outlets by name/address/logo with a 'Current' badge on the active one. Selecting a different outlet clears the cart (clearCartState) and any table context, persists the new outlet via outletStorage.saveOutlet, and does a full page reload to /menu so all outlet-scoped data re-fetches — i.e. switching outlets is a destructive 'start fresh' action, same as picking a new outlet from the outlet-selection page. Separately, VariantSelectionDrawer (variant/size picker) dropped its two-step 'select variant, then tap Add Item' flow in favor of an immediate hand-off on tap — not previously documented in this entry, worth a one-line mention in the walkthrough if that drawer is added to the citation set. Neither change is UI-confirmed yet (code-only this pass). (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-XX — codebase: foodops.ordering.api gained two user-facing additions not yet reflected in this entry. (1) A new guest/walk-in order-submission surface — POST /order/guest-submit — lets a customer place a dine-in or takeaway order with only their name (no session token, OTP, or phone), settled in person as cash; the order links to the organisation's shared 'Walk-In Customer' record (mirroring the counter/POS walk-in flow) via new ICustomerService.GetOrCreateWalkInCustomerAsync. This is distinct from and simpler than the documented OTP-based anonymous-checkout flow (AuthSheet Phone→OTP→Name) — that flow is unaffected and still accurate as documented. Guest checkout via this new endpoint explicitly excludes delivery orders. Not yet confirmed whether foodops.ordering.app's UI calls this endpoint (diff is API-only); needs a frontend check before writing full walkthrough steps. (2) Customer order-details/order-list responses (GetCustomerOrderDetails, GetCustomerOrders) now populate a real DeliveryAddress (from the order's ShippingAddress.AddressLine1) for delivery orders — previously always hardcoded to an empty string. Worth adding to §7 Order tracking once UI-confirmed that the tracking page surfaces this field. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-04 — Guest (walk-in) checkout added to foodops.ordering.app: a new 'Checkout as guest' path lets anonymous customers on dine-in or takeaway orders (delivery excluded) submit an order with only a name — no phone number, no OTP, no login. The new GuestNameSheet component collects a name (min 2 chars) and calls the new useSubmitGuestOrderMutation against POST /order/guest-submit, sending cart items inline (there's no session-backed server cart for a guest). It's shown alongside the existing 'Login to place order' CTA per new showGuestCheckout gating (hidden for delivery, before an order type is picked, in group-cart mode, and when the outlet has paused online ordering) — CartSidebar.tsx. This is distinct from the existing anonymous phone+OTP+name checkout flow, which is unchanged. Separately, OrderDetailsPage gained a 'Payment Receipts' section showing uploaded transfer-slip attachments directly below the status card, plus timing fixes (deferred toast/confirmation dialog, phantom-click-through guard) around transfer-slip upload — these are UX/reliability fixes, not new documented behavior. (auto-applied by the truth pipeline; adversarially gated)

Note: Code + Playwright verified 2026-05-04 · codebase + live E2E Verified_by: codebase + playwright · Last verified: 2026-05-04 Glob curated to src/** and Foodops.Ordering.Api/**;Foodops.Ordering.Core/**. Playwright walk against order.foodops.io/e2e-test-outlet (E2E Test Outlet, set up outlet reference code 2026-05-04): (1) outlet selection page confirmed — helper text, input, Continue button match code; (2) menu page confirmed — header, category pills with emoji, search bar, view-mode toggle, product cards with abbreviation placeholders; (3) ItemDetailsDialog confirmed — Coffee dialog shows price "MVR 48.60 / Base price (Incl. tax)", Allergies (Optional) field, Special Instructions (Optional) field, quantity counter, Add to Order button; (4) CartSidebar confirmed — Takeout/Delivery toggle, item row, Items Total / Sub Total / Total rows, Notes field, "Login to place order" CTA; (5) AuthSheet PhoneStep confirmed — "Log in to continue" title, subtitle copy, phone field with placeholder "Enter your phone number", "Request OTP" button; (6) AuthSheet OtpStep confirmed — "Enter verification code" title, "Code sent to +9607779999" subtitle, 6 individual digit boxes, "resend in Xs" countdown, "Verify" button. OTP step blocked at SMS code — cannot proceed to NameStep without real SMS. GroupCartQRCodeModal, group-cart participant badging remain code-documented only. Screenshots 141–148 captured. Status remains needs-review pending NameStep and group-cart live verification.

Note: 2026-05-04 — Walkthrough rewrite (Track C2) Verified_by: codebase · Last verified: 2026-05-04 Replaced the thin "Customer Features" sub-bullets with a 7-section "Walkthrough — customer's view" derived from deep-read of foodops.ordering.app (router.tsx, OutletSelectionPage, OutletValidator, MenuPage, CartSidebar, AuthSheet / PhoneStep / OtpStep / NameStep, PaymentButton, useCartSync, GroupCartButton, StartGroupCartModal, JoinGroupCartModal, JoinGroupCartPage, GroupCartQRCodeModal, OrdersPage, signalRService, groupCartSignalRService, lib/api/client.ts) and foodops.ordering.api (BaseAPIController, MenuController, Models/Menu/OrderMode.cs). Each step carries a [code: …] reference to the source file; UI-level details (header layout, item-details dialog, view-mode toggle visuals) marked [UI-TODO] for live verification after auth.json refresh. New sections: outlet selection, menu browse, cart, anonymous checkout, order mode selection, group cart, order tracking. Confidence: 90%+ on logic and labels (lifted verbatim from JSX/copy strings), ~60% on visual placement (no live capture this pass).

Note: 2026-06-04 — Content re-verify (CHANGED) · codebase Verified_by: codebase · Last verified: 2026-06-04 Re-verified against foodops.ordering.app@55532a4 and foodops.ordering.api@fb9d0a2 (main). Real user-facing changes since the 2026-05-04 pass: (1) Anonymous carts now get server-computed totals via POST /cart/totals/anonymous — the old "flat local subtotal, no tax" is now only a loading fallback (9bfc125a/cdfd66fc); corrected §3 Cart. (2) Multi-tax / multi-GST breakdown — totals render per-component rows (CGST/SGST) when the API supplies a taxes array (9bfc125a); added to §3. (3) "Not accepting online orders" amber banner driven by outlet IsAcceptingOnlineOrders, dine-in/group-cart exempt, enforced server-side (9b3efa63/0bc7bc18); added to §2 + the Pause-ordering Q&A. (4) OTP SMS now routes by country (Fast2SMS India / MsgOwl Maldives) — sendOtp signature changed to sendOtp({ phoneNumber, outletId }), enabling non-Maldives phone numbers (2eac734b + ordering.api Fast2SMS commits); corrected §4 PhoneStep + citation. (5) Price formatting moved symbol→ISO-code mapping (74474e8) — no display change vs documented "MVR 48.60". (6) Full-menu single-call query (212bf328), group-cart polling tuning (19357b60), Sentry added-then-removed (net zero) — no behaviour change documented. §4 PhoneStep citation line numbers updated to current. NOT re-verified live (no Playwright this pass); banner + multi-tax rows marked UI-TODO.

  • 2026-06-29 — codebase: NEW-STACK re-verification for July-10 launch. Re-pinned to foodops.ordering.app@25f1e9a82e, foodops.ordering.api@80e07fdf7a, hubits-api-notifications@23fa38496e; added hubits-api-sales@19190b9a04 (the delivery service now lives here). DELIVERY/RIDER SURFACE CHANGE: the standalone Flutter rider app (foodops.delivery.mobile) and old backend (foodops.delivery.api, last commit 2025-11-27) are dormant/legacy and NOT a launch surface — delivery is now handled inside the counter as a Delivery order-type, backed by hubits-api-sales Services/Counter/Delivery/** (DeliveryController/DeliveryOrdersController/DeliverySectorsController/DeliveryService, rider auth via DeliveryAuthorizeAttribute). Marked the "Delivery App (Driver)" section dormant, corrected the Plain-English summary + the driver-map Q&A + the SignalR key concept. Customer-facing ordering walkthrough, anonymous checkout, group cart, order-mode filtering, and SignalR tracking all still match the ordering.app/api on main (no UI re-walk this pass — prior Playwright screenshots 141–148 retained). Online orders still flow into the counter via the ordering hub + foodops.api (staging, still in runtime path).

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.