Plain-English summary
The storefront is what your customers see: a mobile-first web app (React PWA) at order.foodops.io/{outlet-code} where they browse your live menu, build a cart, choose pickup or delivery, verify their phone with an OTP code, pay (card via BML, cash, or bank transfer with a receipt photo), and track the order in real time. No app install and no account password — a phone number is the identity. The merchant-side pipeline (how orders land on the POS, how the site is enabled and fed) is covered in Online Ordering Platform and Online Ordering Setup; this entry is the customer's-eye deep-dive.
When you'd use this
- Walking a customer through "how do I order from my phone?" (voice/chat agent script)
- Debugging a stuck checkout — which step gates on what (auth, address, payment method)
- Explaining why an item shows for dine-in but not for delivery
- Answering payment questions: what "Bank Transfer" means, when cash is collected, why the card page is BML-branded
- Helping a customer re-find an order from a tracking link
The storefront at a glance
| Aspect | Detail |
|---|---|
| URL | order.foodops.io/{outletCode} (prod), dev-order.foodops.io (staging) |
| Table QR | order.foodops.io/table/{tableId}/login — dine-in, pre-tied to the table |
| Tracking link | order.foodops.io/orders/{orderId} — works even when logged out (OTP gate) |
| Frontend | React 19 + Vite PWA (foodops.ordering.app@main) |
| Backend | Blazor-hosted ASP.NET API at order-api.foodops.io (foodops.ordering.api@main, IIS) |
| Identity | Phone + OTP for delivery/login checkout; no passwords ever. Dine-in and takeaway also support anonymous guest checkout (name only, no phone/OTP) via a "Checkout as guest" option |
| Real-time | SignalR /orderHub (status) + /groupCartHub (group carts) |
Walkthrough — customer's view
The umbrella entry's walkthrough covers outlet selection, menu browse, cart, anonymous checkout, order modes, group cart, and tracking step-by-step with screenshots 141–148. This section documents the layers that entry does not: availability display, the OTP plumbing, payments, and the tracking-link flow.
1. Entry and menu availability
- Three entry points: direct outlet URL (resolved via
GET /menu/outlet/reference/{code}), table QR, or the bare-root outlet picker. [code: src/router.tsx; src/components/outlet/OutletValidator.tsx] - Every menu request carries
X-Order-Mode(dine-inwhen a table is bound, elsepickup-delivery); the API filters categories/products/search by the item-variant's per-channel flags, so a delivery customer never sees dine-in-only items. The header is parsed case-insensitively and unknown values skip filtering. [code: src/lib/api/client.ts; foodops.ordering.api: Foodops.Ordering.Models/Menu/OrderMode.cs, MenuController.cs] - Staff menu edits propagate live: the FoodOps menu backend POSTs
api/menu/notify-updateto the ordering API, which invalidates its cache and broadcastsMenuUpdatedto the organisation's SignalR group — the storefront resets its menu cache and shows a toast. (Receiver confirmed on the ordering API; the notifier moved off the now-retired monolith — see Online Ordering Setup for the open emitter-location note.) [code: foodops.ordering.api: MenuUpdateController.cs; src/features/menu/pages/MenuPage.tsx] - When the outlet's
IsAcceptingOnlineOrdersflag is off, an amber "{outlet} is not accepting online orders at this time" banner shows on the menu and above the cart CTA, and the server rejects submission. Dine-in (table QR) and group-cart orders are exempt. [code: src/features/menu/pages/MenuPage.tsx; foodops.ordering.api: OrderSubmissionService.cs]
2. Cart and modifier rules
- Anonymous carts live in
localStorageand still get server-computed totals (tax + service fees) viaPOST /cart/totals/anonymous; authenticated carts are server-side (GET /cart,GET /cart/totals). Multi-tax (e.g. CGST/SGST) renders one row per component. [code: src/components/cart/CartSidebar.tsx; src/components/cart/CartTotals.tsx] - Modifier groups enforce min/max per item and, since 2026-06-08, a maximum total quantity across the group (
MaximumGroupQuantitySelection) — validated client-side in the item dialog and re-validated server-side at submission. [code: app5bd675f; api2a9e64bOrderSubmissionService] - Back-button behaviour: drawers/dialogs (cart, item details, auth sheet) register overlay history entries, so the phone's back gesture closes the overlay instead of leaving the site (shipped 2026-06-04). [code: src/hooks/useOverlayHistory.ts,
b0b419a]
3. Checkout — anonymous until payment, then OTP
A guest can browse, build a cart, and pick an order type with no account. For delivery orders (or if they choose to sign in), the auth sheet appears at the CTA ("Login to place order"). For dine-in and takeaway, anonymous carts also show a "Checkout as guest" option: it opens a name-only sheet (min 2 chars, no phone/OTP) and submits the cart items inline via POST /order/guest-submit. On success the cart clears and an order reference is shown, but the customer is not sent to the trackable /orders list (that requires login) and the CSAT survey is skipped for guest orders.
- Phone step — enter phone, "Request OTP". The request carries the
outletIdso the backend picks the SMS provider for the outlet's country. [code: src/components/auth/PhoneStep.tsx] - OTP routing (server) —
SmsServiceFactorymatches the outlet's organisation country name:India → Fast2SMS; everything else (incl. Maldives) → MsgOwl (MsgOwl is the default/fallback, also used when country resolution fails). [code: foodops.ordering.api: Foodops.Ordering.Core/Services/SmsServiceFactory.cs:36-43] - OTP step — 6-digit code, resend countdown.
- Name step — only if the customer record has no name; minimum 2 chars. That's the entire registration: phone + name.
- Cart hand-off — the local cart is replayed to the server (
POST /cart/addper item) and the order submits without a second tap. [code: src/hooks/useCartSync.ts; src/components/payment/PaymentButton.tsx]
4. Pickup vs delivery
- The cart drawer shows Takeout / Delivery buttons (plus Dine In when a table is bound). [code: src/components/cart/CartSidebar.tsx]
- Delivery requires a saved address — the
AddressSelectoris auth-gated, and addresses are managed from the Profile page. Delivery sectors (zones with per-sector charges) are configured by the merchant; see Deliveries. [code: src/components/cart/AddressSelector.tsx; src/features/profile/components/AddressManagement.tsx] - Takeout (pickup): no address; the customer collects at the outlet when the tracker reaches "Ready"/"Picked Up".
5. Payment options
The payment-method sheet offers up to three options, each individually enabled per outlet (GET api/payment/methods/outlet/{outletId}/enabled): [code: src/components/payment/ChoosePaymentMethodModal.tsx; foodops.ordering.api: PaymentController.cs]
| Method | Label/copy in app | How it works |
|---|---|---|
| BML card | "Bank of Maldives (BML)" — "Pay securely with BML gateway" | POST api/payment/bml/initiate redirects to the BML hosted gateway; order confirms on gateway success. [code: BmlPaymentService.cs] |
| Cash | "Cash" — "Pay cash on pickup/delivery" | POST api/payment/cash/process records a cash-due order; cash is collected at pickup or by the rider (then flows through the rider cash loop — see Rider App). |
| Bank transfer | "Bank Transfer" — "Transfer and upload receipt" | Customer transfers, then photographs/uploads the slip (POST api/payment/transfer/upload-receipt → blob storage → transfer/process); staff verify the slip on the counter. AI slip check (RE-CONFIRMED present on the new stack, 2026-06-29): at upload the slip is validated by an AI vision check (Google Gemini, model gemini-2.5-flash) comparing amount/currency/date to the order (±0.01 amount tolerance, ±1 day date tolerance; no reference-number comparison). If the AI check flags a soft mismatch (amount/currency/date off from the order) the customer sees a warn-but-allow dialog — title "Slip may not be valid", body "⚠ Our system detected that the uploaded file might not be a valid transfer slip. You can still submit it — the restaurant will verify it manually" (Upload Anyway / Choose Another) — and staff get a "{Order}: transfer slip needs review" toast on the counter. As of 2026-07-30, a distinct hard-rejection path exists (uploadResult.rejected) for files the server refuses outright as not being a transfer slip at all: there is no "Upload Anyway" option — the app clears the picked file and shows an error toast asking the customer to choose a different file, and (on the pay-after-order-submit path) if this happens after the order was already placed, the order is kept and the customer is told to retry the slip from the order page rather than losing the order. It's non-blocking + inert when Gemini:ApiKey is unset (returns WasValidated=false); manual staff verification remains the backstop. [code: foodops.ordering.api Foodops.Ordering.Api/Services/TransferSlipValidationService.cs:18-31,58-63 (Gemini) + Foodops.Ordering.Core/Services/TransferSlipVerdictBuilder.cs:23-59 (amount/currency/date compare); foodops.ordering.app src/features/orders/pages/OrderDetailsPage.tsx:757-764 (warn-but-allow ConfirmationDialog); foodops.counter.app src/hooks/useSignalR.ts:188-201 (TransferSlipUploaded → "transfer slip needs review" toast) @ main] |
If no method is enabled the sheet says so and checkout cannot complete. Whether the customer can pay before staff accept the order is the per-outlet allow-payment-without-acceptance flag — it decides whether the CTA reads "Make Payment", "Place Order", or shows both. [code: PaymentController.cs:141; src/components/payment/PaymentButton.tsx]
6. Tracking — including the logged-out tracking link
- After submit the customer lands on
/orders: Ongoing/Completed lists with a per-order-type progress bar (Delivery: Placed → Accepted → Preparing → Ready → Enroute → Delivered; Takeout ends at Picked Up; Dine-in at Served), updated live over SignalR. [code: src/features/orders/utils/orderUtils.ts; src/services/signalRService.ts] - Tracking link (
/orders/{orderId}): theOrderAccessPagehandles visitors who aren't logged in — it shows the phone+OTP form, and after verification loads the order, restores the outlet/table context from the order itself, and renders the order details. This is how a customer on a new device or cleared browser re-finds their order: same phone number, same order. [code: src/features/orders/pages/OrderAccessPage.tsx] - Order ratings: a CSAT drawer can fire after submission (with a cool-off so repeat customers aren't re-prompted every order). [code: src/components/survey/CSATSurveyDrawer.tsx]
Key concepts
- Phone is the account — no email/password ever; OTP-verified phone + a display name is the full customer identity. Orders bind to the customer record, not the device.
- Per-channel availability — each item variant carries DineIn/Takeout/Delivery flags; the
X-Order-Modeheader makes the server enforce them on every menu read, so out-of-channel items can't be added even by URL tricks. - Country-routed OTP — SMS provider is chosen by the outlet's country (India → Fast2SMS, otherwise MsgOwl), which is what makes non-Maldives phone numbers usable.
- Server-authoritative cart — anonymous carts are local but totals are server-computed; on login the local cart is replayed to the server, which then owns it.
- Payment-before-acceptance flag — per outlet: either customers pay immediately, or they "Place Order" and pay after staff accept.
- Tracking link ≠ session —
/orders/{orderId}works from any device; OTP with the ordering phone number unlocks it.
Common questions
Q: Does the customer need to install an app? A: No — it's a web app (PWA). It can be "installed" to the home screen from the browser prompt, but ordering works straight from the link or QR scan.
Q: Does the customer need an account or password? A: No password, ever. They can browse and build a cart anonymously; at checkout they verify their phone with an SMS code and give a name. That's the whole sign-up.
Q: My customer in India isn't receiving the OTP — why does my Maldives test number work? A: OTP SMS routes by the outlet's country (India → Fast2SMS, all other countries → MsgOwl). If a provider-side issue affects one route, the other is unaffected — check the corresponding provider account/balance.
Q: What payment methods can customers use? A: Up to three, each toggleable per outlet: BML card gateway, cash (on pickup/delivery), and bank transfer with a receipt-photo upload. If you see fewer on the site, the missing ones are disabled for that outlet.
Q: What does "Bank Transfer" actually do? A: The customer transfers to your bank account and uploads a photo of the transfer slip in the same flow. The slip lands on the order for staff to verify before treating it as paid.
Q: Why does one customer see "Make Payment" and another "Place Order"? A: The per-outlet "allow payment without acceptance" setting. Off = customers place the order and pay after staff accept; on = they can pay immediately.
Q: Customer says an item is "missing" from the delivery menu but it's on the dine-in menu. A: That's per-channel availability working as configured — the variant's Delivery flag is off. Edit the item's ordering availability to include delivery.
Q: Can a customer track an order from a different phone or after clearing their browser?
A: Yes — open the tracking link /orders/{orderId} (or the site) and verify with the same phone number used to order; the order list/details come back.
Q: How fast do menu edits show up on the storefront?
A: Near-instant — the POS edit triggers a notification to the ordering API, which broadcasts MenuUpdated; open storefronts drop their cache and refetch (customers see a refresh toast).
Q: What happens if the restaurant pauses online ordering mid-browse? A: An amber "not accepting online orders" banner appears on the menu and cart, and the server rejects submission. Dine-in table-QR sessions and group carts keep working (they're treated as in-person).
Q: Can customers order from a table QR and also pick delivery? A: No — a table-QR session is locked to dine-in for that table. For delivery they'd use the outlet's normal URL (picking an outlet clears table context).
Q: Is there a minimum order or delivery fee shown? A: Delivery charges come from the merchant's delivery sectors and land in the server-computed totals. (Per-sector minimum-order enforcement on the storefront is not code-verified in this pass — see Edge cases.)
Edge cases and known issues
- OTP delivery is the hardest dependency — checkout cannot complete without the SMS arriving. Provider/balance issues on Fast2SMS or MsgOwl block all new-customer checkouts for outlets routed to that provider.
- Anonymous totals are a fallback race — while
POST /cart/totals/anonymousis in flight (or on error) the cart briefly shows a flat local subtotal without tax; the server figure replaces it. [code: CartSidebar.tsx] - Favorites and re-order need sign-in — anonymous visitors see inert heart icons and a login-required empty state. [code: src/components/auth/LoginRequiredEmptyState.tsx]
- Minimum-order-per-zone: user story US-ORD-EDGE-007 describes a "Minimum order MVR 50 for this area" gate; no implementing storefront code was identified at
09e5e61in this pass — treat as not-verified rather than confirmed behaviour. - Order cancellation from the storefront (US-ORD-004/EDGE-005): customers can self-cancel an online order from the storefront while it is still "New" (not yet accepted by the restaurant) — a Cancel button on the order card / order details opens a confirmation dialog, calls cancel, and shows an "Order cancelled" toast. Once the restaurant accepts the order (its status leaves "New"), the self-cancel control disappears and cancelling reverts to a call-the-restaurant flow.
[code: foodops.ordering.app/src/features/orders/components/{OrderCard,OrderDetailsModal}.tsx — canCancelOrder / handleCancelOrder @ b63bce5] - Visual layout of the payment sheet, transfer-slip upload, and OrderAccessPage OTP form are code-documented only.
Related
Online Ordering Platform — the umbrella entry: customer walkthrough with screenshots, merchant pipeline, recent-change log
Online Ordering Setup — merchant go-live: URL/branding, the accept-orders toggle, menu publishing, payment-method enablement
Deliveries — what happens after a delivery order is accepted: queue, sectors, rider dispatch
Rider App — the rider who brings the order and collects the cash
Takeouts — pickup orders on the counter side
User Stories: Online Ordering — 20 scenarios incl. edge cases
2026-08-09 — codebase:
foodops.ordering.app4e8d3b2..2c35685 adds per-service order-type enablement to the storefront's order-type picker, not currently documented in this entry's section 4 ('Pickup vs delivery') or section 1 ('Entry and menu availability').CartSidebar.tsxnow reads three new outlet-data fields —isDineInEnabled/isDeliveryEnabled/isTakeoutEnabled(per the added comment, 'mirroring the admin app's Settings → Outlets → Services toggles') — and only an explicitfalsedisables an order type (undefined, e.g. mid-flight query or an API build predating the fields, is treated as available, so the picker can't flicker or strand a customer). The Takeout/Delivery/Dine-in buttons this entry's section 4 describes are now filtered toavailableOrderTypesbuilt from these flags (dine-in additionally requires a bound table, matching the entry's existing 'Dine In when a table is bound' claim); a useEffect drops an already-selected order type if its service is toggled off mid-session, and a newhasNoAvailableOrderTypes(all relevant services off — for group carts, dine-in alone, since 'Group carts always submit as dine-in') is folded intoisOrderingBlockedalongside the already-documentedisOutletClosed/isOnlineOrderingDisabled. To catch a mid-session admin toggle, the outlet query now polls every 15s while the cart drawer is open (pollingInterval: isOpen ? 15000 : 0), where the outlet fetch previously only ran on mount/focus. This doesn't contradict the entry's documented button set or blocking logic (both already described as conditional/per-outlet) — it's a new gating dimension worth folding into section 4 and a Common-questions entry next time that section is touched (e.g. 'why don't I see a Delivery button for this outlet' → the outlet has that service disabled in admin, separate from the online-ordering-paused banner). Nofoodops.ordering.apidiff was supplied this pass to confirm theOutletDtoactually serves these three new fields or how the admin app's Settings → Outlets → Services toggles map to them — flag for a follow-up pass before citing the admin-side control by name. Also in this diff, not touching documented behavior:AuthSheet.tsxdrops its customuseKeyboardInset/repositionInputs={false}iOS-keyboard workaround in favor of a new sharedDrawerBodyprimitive (commit: 'update Drawer components to improve layout and add DrawerBody for better content management'), and several other drawers (EditCartItemDrawer,EditGroupCartItemDrawer,GuestNameSheet,GroupCartQRCodeModal,StartGroupCartModal,ItemDetailsDialog) lose their fixedmax-h-[..vh]caps in favor of flex/overflow-y-autobodies — layout/scrolling internals only, no change to any documented step, label, or endpoint. The commit subjects also mention auseOtpInputhook and payment-modal changes to skip the cart query for unauthenticated users, but no diff hunks intersecting this entry's cited surface were supplied for either this pass — flag for a follow-up pass before touching OTP-step or payment-modal claims. Re-pinfoodops.ordering.appto2c35685on next full verification pass; nofoodops.ordering.apidiff supplied this pass, pine3fca93unaffected. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)2026-08-09 — codebase: foodops.ordering.api 96a238e..bf076ac. The 'fix(status-update): quote the persisted order total in the accepted SMS' commit in this range is the same change already logged in the 2026-08-06 verification-history entry (pinned e3fca93) — the Include(o => o.TransactionCurrency) and amountDue/currencyCode derivation match verbatim; no new note needed there, the pin can safely advance past it. Genuinely new in this range: 'feat: Implement outlet service availability checks and update caching strategy' introduces a per-outlet, per-order-type enable/disable gate (admin → Settings → Outlets → Services) not currently documented in section 1 or section 4 ('Pickup vs delivery'). A new OutletOrderTypeAvailability.ForOutletAsync (Foodops.Ordering.Core/Services/OutletOrderTypeAvailability.cs) derives Dine-In/Delivery/Takeout enabled state from the outlet's RestaurantTable rows (Delivery/Takeout: enabled if any non-deleted table of that order type exists; Dine-In: default-on, disabled only by a deleted IsSystemTable 'tombstone') — the same derivation sales-api's admin toggle reader uses, so admin and the storefront can't disagree. OrderSubmissionService.SubmitOrderAsync now enforces this server-side, authoritatively, ALONGSIDE (not replacing) the already-documented IsAcceptingOnlineOrders 'not accepting online orders' gate: submitting a disabled order type is rejected with 'This outlet is not accepting {delivery|dine-in|takeaway} orders at this time.' — and unlike the online-orders flag, there is no dine-in exemption on this new check, since disabling a service means the outlet doesn't run it at all (vs. the online-orders flag, which only governs online intake). OutletDto also gains IsDineInEnabled/IsDeliveryEnabled/IsTakeoutEnabled fields (MenuService.cs), and the previously 5-minute-cached order-types-availability endpoint (RestaurantController.GetOrderTypesAvailability) is now no-store so a merchant's Services toggle takes effect immediately instead of up to 5-10 minutes later. Separately, RestaurantService's three IsXAvailableAsync methods were refactored onto the same OutletOrderTypeAvailability reader, fixing a previously org-scoped-instead-of-outlet-scoped bug (enabling takeout at one outlet previously reported it available at every outlet in the org) — not previously documented, so nothing contradicted. No foodops.ordering.app diff was supplied this pass to confirm whether/how the storefront's order-type picker (section 4) already reflects these three new flags or what copy it shows on a rejected submission — a code comment in OrderSubmissionService claims 'The customer app hides disabled services from its order-type picker' but that's unconfirmed without app-side evidence. Worth folding into sections 1/4 and a new Common-questions entry next time those sections are touched ('why can't I order delivery from this outlet even though the site is otherwise accepting orders'). Re-pin foodops.ordering.api to bf076ac on next full verification pass; no foodops.ordering.app diff supplied this pass. (auto-applied by the truth pipeline; adversarially gated)
2026-08-06 — codebase:
foodops.ordering.api96a238e..e3fca93 ("fix(status-update): quote the persisted order total in the accepted SMS") is a bug fix to a customer-facing SMS this entry does not currently document:OrderStatusController.SendOrderAcceptedSmsWithBankDetailstexts the customer the outlet's bank-transfer account details on order acceptance, quoting an amount due. Previously that SMS quotedrequest.OrderTotal/request.Currencyverbatim from the status-update caller's payload; per the added comment, a counter client with a stale or absent local total could make the SMS read 'MVR 0.00'. It now derives the amount from the persisted order itself —order.Outstandingif >0, else falling back toorder.GrossTotal(Outstanding is 0 on a fresh unpaid ordering-app order) — and the currency from the order's newly-IncludedTransactionCurrency.Code, falling back to the request's currency, then 'MVR'. This SMS pathway sits alongside — and is distinct from — this entry's documented bank-transfer flow (section 5: customer transfers, uploads a slip, staff verify); it appears to be the mechanism that tells the customer where to transfer, which isn't described anywhere in this entry today. No diff evidence was supplied this pass to confirm exactly when this SMS fires (e.g. gated to bank-transfer-method + unpaid orders on acceptance) or how/whether it reaches guest-checkout orders; worth a follow-up pass to pull the surrounding method and fold a 'bank-details SMS' line into section 5 and Common questions. Re-pinfoodops.ordering.apitoe3fca93on next full verification pass; nofoodops.ordering.appdiff supplied this pass. (auto-applied by the truth pipeline; adversarially gated)2026-07-30 — codebase:
foodops.ordering.api7b91621..96a238e. Five changes intersect this entry's tracked surface, none contradicting documented claims. (1) Duplicate order-submission guard (new): a newOrderSubmissionLock(Foodops.Ordering.Core/Services/OrderSubmissionLock.cs) takes a SQL Serversp_getapplocksession-scoped exclusive lock, keyed byfo:ordersubmit:{organizationId}:{sha256(identity)}, before the cart is read for order submission — per its own doc comment, this exists specifically so two overlapping submissions from the same customer/cart cannot both turn into separate orders (docs/adr/0003). On failure to acquire (a genuine concurrent duplicate) the caller must treat the second submission as a probable duplicate and not create an order; on any unexpected error acquiring the lock it fails open (proceeds unlocked) rather than blocking a legitimate order; on the in-memory test provider it's a no-op. This is new server-side protection for the 'Cart hand-off... order submits without a second tap' flow documented in section 3 step 5 — worth a line in 'Edge cases' next time that section is touched (rapid double-tap/network-retry on submit is now guarded server-side). Nofoodops.ordering.appdiff supplied this pass to confirm what the storefront shows the customer if a submission is rejected as a duplicate — flag for follow-up. (2) Transfer-slip hard-rejection DTO fields land server-side:PaymentController.UploadTransferReceiptnow returnsRejected = verdict?.IsNotATransferSlip == trueplus a cannedRejectionReasononUploadReceiptResultDto— this is the first backend evidence of the exactRejected/RejectionReasonshape this entry's payment table already describes (dated 2026-07-30) as the app-side 'hard-rejection path' keyed onuploadResult.rejected. Confirms rather than changes the documented behavior; no app diff supplied this pass to confirm the app renders this specificRejectionReasonstring verbatim (vs. its own hardcoded copy) — worth confirming next full pass. Same controller:ProcessTransferPayment's failure message now surfacesresult.ErrorMessagewhen present instead of always the generic 'Failed to process transfer payment' — error-copy only, not previously quoted verbatim in this entry. (3) Service fee base now excludes fee-exempt categories (CartService.CalculateTotalsForItemsAsync,GroupCartService.CalculateTotalsAsync): percentage service fees are now computed against aserviceableItemsTotal/serviceableTaxTotalthat excludes line items whoseProductCategory.IsServiceFeeApplicationis false, instead of the full cart total — per the added comment, this matches how the in-person bill already excludes those categories. This is a quiet correctness fix to the 'server-computed totals (tax + service fees)' claim in section 2 — the computed fee may now be lower for carts containing fee-exempt items — but the entry doesn't document category-level fee-exemption mechanics today, so nothing is contradicted. (4) Menu image priority reordering (MenuService.GetCategoriesByOutletAsync): product-cardImageUrlnow prefers the product-level default image over any variant image (previously variant images were tried first); variant images remain a fallback. Not previously documented in this entry — cosmetic/display-only, low priority. (5) Out of this entry's customer-facing scope:OrderStatusController.UpdateOrderStatus's SMS phone-number lookup was rewritten to stop an implicit INNER JOIN on the requiredCustomernavigation from silently dropping status SMS for counter/POS/walk-in orders (CustomerId == Guid.Empty) — those orders aren't reached via the storefront's OTP-verified Customer flow this entry documents, so no edit applies, but flag for a future POS/counter-facing entry. Re-pinfoodops.ordering.apito96a238eon next full verification pass; nofoodops.ordering.appdiff supplied this pass, app pin unaffected. (auto-applied by the truth pipeline; adversarially gated)2026-07-30 — codebase:
foodops.ordering.app8c01414..4e8d3b2 ("feat: implement useSingleFlight hook to prevent duplicate submissions in payment processes") adds a client-side double-submission guard, not currently described in this entry. A newuseSingleFlighthook (src/hooks/useSingleFlight.ts) wraps an async handler in auseRefflag that flips synchronously on the same tick as the click/tap, dropping (resolving toundefined, not throwing) any second invocation while the first is still in flight — closing a gap the existing React-state disabled-button guards (isProcessing/isSubmittingOrder/isSubmittingGuest) leave open for one render cycle, per the hook's own comment: 'two overlapping submissions used to mean two orders, two invoices, two kitchen tickets and two texts.' It's now applied to every order-submission path this entry documents: the BML/transfer/card submit inPaymentModal.handleSubmit, cash submit inCashPaymentModal.handleSubmit, the guest-checkout name-sheet submit inCartSidebar.handleGuestNameSubmit(section 3's 'Checkout as guest' flow), and bothPaymentButton.handleSubmitOrderWithoutPayment(the pay-after-acceptance 'Place Order' path from section 5) andhandleSaveAndAccept(dine-in save-and-accept). This is purely a client-side race-condition hardening — no change to any documented button label, dialog copy, endpoint, or flow order — but is worth a short line in 'Edge cases and known issues' next time that section is touched, noting that double-tapping any submit/pay button is now a no-op rather than a risk of duplicate orders. The related commit in this diff range, 'feat: enhance transfer payment process with upload validation and rejection handling,' supplied no hunks intersecting this entry's cited surface this pass — flag for a follow-up pass with that diff before touching the transfer-slip hard-rejection claims already documented in section 5. Re-pinfoodops.ordering.appto4e8d3b2on next full verification pass. (auto-applied by the truth pipeline; adversarially gated)2026-07-30 — codebase:
foodops.ordering.api7b91621..1f7c4a2 hardens bank-transfer settlement, extending (not contradicting) this entry's documented 'hard-rejection path' and 'no reference-number comparison' claims. Two new server-side refusals inOrderSubmissionService.ProcessExistingOrderTransferPaymentAsync: (1) a transfer payment can no longer be applied against a made-up/empty document id —TransferReceiptExistsAsyncmust confirm the uploaded receipt document really exists first, elseTransferPaymentProcessResult.Rejected("A transfer slip is required to record a bank transfer payment."); (2) the same slip can no longer settle two orders — the bank's own reference number (now persisted queryably viaChequeNumber = slipVerdict?.ReferenceNumber, previously only embedded in the Remarks note text) is checked via a newIsSlipReferenceAlreadyUsedAsync, rejecting with "This transfer slip has already been used to pay another order." if reused. Storing the reference for replay-detection is a different comparison from the documented verdict-builder amount/currency/date match against the order's own figures — the 'no reference-number comparison' claim aboutTransferSlipVerdictBuilder's soft-mismatch logic is untouched and still accurate. Also,PaymentController.UploadTransferReceipt's response DTO gains explicitRejected/RejectionReasonfields (RejectionReason= "That file doesn't look like a bank transfer slip. Please upload the receipt from your banking app." whenIsNotATransferSlip), and the transfer-process failure message now surfacesresult.ErrorMessageinstead of a generic string — plumbing that lets these new customer-facing rejection reasons reach the app. Nofoodops.ordering.appdiff was supplied this pass, so it's unconfirmed whether the storefront's existing hard-rejection UI (clear file + error toast; keep-order-and-retry-from-order-page for the pay-after-submit case) already generically surfaces these two new reasons or needs new copy/branches — flag for a follow-up pass with app-side evidence before editing section 5's payment table. Separately and lower-priority, unrelated to this entry's documented storefront surface:OrderStatusController.UpdateOrderStatusfixed a bug where its authoritative-order lookup used an implicit INNER JOIN on the requiredCustomernavigation, returning null (and silently suppressing the customer status-update SMS) for any counter/POS/walk-in order with no Customer row; now resolves the order and customer via separate queries plus a blank-string-safeCoalescehelper. This concerns order-status SMS delivery, which this entry doesn't currently document (SignalR-based in-app tracking only) — likely belongs in whichever entry documents that notification path, not here. (auto-applied by the truth pipeline; adversarially gated)2026-07-23 — codebase:
foodops.ordering.api690d322..7b91621. Three changes, none contradicting documented claims. (1) Customer-update org scoping bug fix:CustomersController.UpdateCustomernow resolves the active outlet's organisation and threads it intoCustomerService.UpdateCustomerAsync, which (when supplied) filters the customer lookup toApplicationOrganisationId == applicationOrganisationIdbefore matching by phone. Per the added comments, customers are partitioned one row per phone+org, and the prior unscopedFirstOrDefaultcould update a different org's record for the same phone — silently failing to persist the name for the active outlet's org, so the storefront's 'Name step' (documented in section 3 step 4 as 'only if the customer record has no name') would keep re-prompting on every login for phone numbers shared across organisations. This is a fix that makes that already-documented one-time-name-capture behavior reliable in multi-org phone-reuse scenarios — not a change to the flow itself, so nothing to edit. (2)OutletDto.TimeZoneadded, populated via a newTimeZoneHelper.ToIanaTimeZone(organisation?.TimeZone)in bothGetRestaurantOutletsByOrganizationAsyncand the sibling outlet-lookup projection — converts the organisation's Windows time zone ID to IANA format. Backend-only plumbing; nofoodops.ordering.appdiff supplied this pass to confirm the storefront consumes it (e.g. for displaying/formatting order times in the outlet's local zone) — flag for a follow-up pass before documenting any timezone-display behavior. (3)OrderSubmissionService.SubmitOrderAsyncnow stampsOrderDate/DeliveryDate/note timestamps withDateTime.UtcNowinstead ofDateTime.Now— an internal correctness fix (server-local-time bug) with no customer-visible change to the documented order-tracking timeline (Placed → Accepted → … per section 6). Re-pinfoodops.ordering.apito7b91621on next full verification pass; nofoodops.ordering.appdiff supplied this pass. (auto-applied by the truth pipeline; adversarially gated)2026-07-23 — codebase:
foodops.ordering.app59a13c5..079a642 ("feat: add timeZone property to outlet-related components for improved time handling") changes how order timestamps are displayed to the customer, not currently described in this entry. The cached outlet object (localStorage/sessionStoragecurrentOutlet, populated at login/outlet-resolution inAuthSheet.tsx,LoginForm.tsx,AppLayout.tsx,OutletValidator.tsx, and typed intypes/menu.tsas a newOutletDto.timeZoneIANA id e.g.Asia/Karachi) now carries the organisation's time zone.orderUtils.tsadds agetOrgTimeZone()reader and threads it intoformatDate/formatTime'stoLocaleDateString/toLocaleTimeStringcalls via a newtimeZoneoption — order timestamps (order list, order details, theAttachmentViewerModalupload date, which also switched from a rawtoLocaleDateString()call to the sharedformatDatehelper) now render in the restaurant outlet's local time zone instead of the customer's browser/device time zone, falling back to the browser's local zone only when no outlet time zone is cached. This is a genuine display-behavior change worth a line in section 6 ('Tracking') or a Common-questions entry next time that section is touched (e.g. 'why does my order time look different than my phone's clock' — answer: times are shown in the restaurant's local time zone, not the customer's device time zone). No existing documented claim in this entry addresses timestamp time-zone handling, so nothing is contradicted. Re-pinfoodops.ordering.appto079a642on next full verification pass; nofoodops.ordering.apidiff supplied this pass, pin690d322unaffected. (auto-applied by the truth pipeline; adversarially gated)2026-07-21 — codebase:
foodops.ordering.api10edb52..690d322 ("feat: Update out-of-stock handling to flag items as unavailable instead of hiding them") changes out-of-stock handling inMenuService.csacrossGetProductsAsync,GetGroupedProductsAsync,GetProductVariantsAsync, andSearchProductsAsync: the hard.Where(StockAvailability.IsOrderable)query filters and the list-pruningRemoveOutOfStockhelper — which previously excluded a stock-tracked, depleted variant/product entirely from every menu/search response — are removed and replaced by a renamedMarkAvailabilityhelper, invoked after materialisation, that flags out-of-stock variants/products withIsAvailable = falseinstead of dropping them. Per the updated doc comments, 'Out of Stock items stay on the menu but are flagged un-orderable' and are 'shown greyed-out' rather than hidden; a product is now available if any of its variants is. This is a genuine behavior change from the menu-listing exclusion previously described only in this entry's verification-history notes (never promoted into the 'Entry and menu availability' main-body section, so nothing there is contradicted): out-of-stock items now stay browsable in menu browse, category listings, and search — just non-addable — instead of disappearing outright. This dovetails with the app-side note logged earlier today (ProductCard/ProductCardGridMulti/ProductCardGridSinglecomputingisOutOfStock = !product.isAvailableand rendering a greyed-outOutOfStockBadgewith disabled card click): that frontend logic now has a live backend-suppliedIsAvailable=falsesignal to act on for stock depletion, not just per-channel availability. Worth folding into 'Entry and menu availability' and a new Common-questions entry next time that section is touched, describing the new stay-visible-but-greyed-out treatment alongside the existing per-channel (DineIn/Takeout/Delivery) filtering. Re-pinfoodops.ordering.apito690d322on next full verification pass;foodops.ordering.apppin (9a1b9ea) unaffected — no additional app diff supplied this pass beyond the already-logged 2026-07-21 note. (auto-applied by the truth pipeline; adversarially gated)2026-07-21 — codebase:
foodops.ordering.appe7625a2..9a1b9ea. Nearly all hunks in this range are already fully logged in prior notes: the cart-line Out-of-Stock flow (unavailableVariantIds,CartItem.tsxbadge,CartSidebar.tsxbanner/toast/guest-submit branching, register-closedisOutletClosed/isOrderingBlocked) was logged 2026-07-17; the anonymous-cart full-edit (modifiers/allergies/instructions) parity fix and theEditCartItemDrawer.tsxproductId-resolution +productSnapshot.productIdbackfill acrossProductCard/ProductCardGridMulti/ProductCardGridSingle/FavoriteProductCardwere logged 2026-07-17/07-20 respectively; and the conditionalstate.outletIdassignment plusdeliveryTimereset incartSlice.tswere logged 2026-07-20 as non-contradicting bug fixes. Genuinely new in this range, not previously captured anywhere in this entry:ProductCard.tsx,ProductCardGridMulti.tsx, andProductCardGridSingle.tsxnow surface Out-of-Stock (ADR 0002) directly on the menu grid/list, not just in the cart. Each imports a newOutOfStockBadgecomponent and computesisOutOfStock = !product.isAvailable; when true the whole card'sonClick(product-details view) is disabled entirely (previously only the Add button was disabled via the existing!product.isAvailablecheck on the Button'sdisabledprop — the card itself was always clickable), the product image is grayscaled/dimmed, the name/price text is greyed, and the Add-to-cart button is no longer just disabled but removed from the DOM entirely. This is additive, customer-visible menu-browsing behavior this entry's 'Entry and menu availability' section doesn't currently describe (that section only covers per-channel DineIn/Takeout/Delivery filtering, not stock-based display treatment) — worth a short addition there and a Common-questions entry ('why can't I tap this out-of-stock item / add it') next time that section is touched. No contradiction to any documented claim. Re-pinfoodops.ordering.appto9a1b9eaon next full verification pass; nofoodops.ordering.apidiff supplied this pass, pin10edb52unaffected. (auto-applied by the truth pipeline; adversarially gated)2026-07-20 — codebase:
foodops.ordering.appe7625a2..26cd36c supersedes the already-pinned 45a6ca7. Nearly all hunks in this range are already fully logged: the Out-of-StockunavailableVariantIds/badges/banner and register-closedisOrderingBlockedgating (2026-07-17 note), the anonymous-cart full-edit (modifiers/allergies/instructions) parity fix inCartItem.tsx/cartSlice.ts(2026-07-17 note), and theEditCartItemDrawer.tsxproductId-resolution +productSnapshot.productIdbackfill acrossProductCard/ProductCardGridMulti/ProductCardGridSingle/FavoriteProductCard(2026-07-20 note) — no new claim there. Genuinely new in this range, not previously logged:updateCartStateincartSlice.tsnow only overwritesstate.outletIdwhen the server cart response actually supplies one, instead of unconditionally assigningcart.outletId(which is alwaysundefinedonCustomerCartDtoresponses) — per the added comment this previously risked silently nulling out the client-resolved outlet on every cart load, which could short-circuit the outlet-change-detection logic (clearCartIfOutletChanged) that the 2026-07-08/07-10 outlet-switcher notes already document as clearing cart/table state on switch. This is a bug fix that makes that already-documented switching behavior reliable — not a change to it — so nothing to edit. Also minor:clearCartStatenow resets adeliveryTimefield alongsidepickupTime/orderNotes, not previously mentioned. Commit subjects for this range also include an order-service Authorization-header refactor and an outlet-switch confirmation-toast/session-storage change, but no diff hunks intersecting this entry's documented surface were supplied for either, so neither is confirmed to touch customer-visible behavior this pass — flag for a follow-up pass with app-side evidence before editing outlet-switch claims. Re-pinfoodops.ordering.appto26cd36con next full verification pass; nofoodops.ordering.apidiff supplied this pass, pin10edb52unaffected. (auto-applied by the truth pipeline; adversarially gated)2026-07-20 — codebase:
foodops.ordering.appe7625a2..45a6ca7 supersedes the already-pinned 2f21ef8. The cartSlice/CartItem/CartSidebar hunks in this diff (Out-of-StockunavailableVariantIdsstate, red 'Out of stock' badges, the 'Some items are no longer available' banner, and the register-closedisOrderingBlockedgating) are the same changes already fully logged in the 2026-07-17 verification note — no new claim there, nothing to re-document. Genuinely new in this range:EditCartItemDrawer.tsxnow resolves the correct catalogproductIdbefore fetching modifier groups for a cart line. Per its own added comment, older/anonymous carts stored the variant id in theproductIdfield, soGET /menu/products/{productId}/modifierscould silently query with the wrong id and return no groups when a customer tried to edit modifiers on such a line; the drawer now detects that case (productIdmissing or=== productVariantId) and callsGET /menu/product-variant/{id}to resolve the true product id first, and also now threadscartItem.currencyinto the modifier objects it saves. In support,ProductCard,ProductCardGridMulti,ProductCardGridSingle, andFavoriteProductCardall now populateproductId: product.productIdin the localproductSnapshotused for optimistic anonymous-cart adds, so newly-added lines carry the right id going forward. This is a bug-fix precursor to — not a contradiction of — the entry's 2026-07-17-logged claim that anonymous-cart edits now support full modifier/allergy/instruction edits: before this fix, that capability could silently fail to surface a line's modifier choices for items added via these product cards. Nothing here touches OTP, payment, or tracking-link claims. Re-pinfoodops.ordering.appto45a6ca7on next full verification pass; thefoodops.ordering.apipin (10edb52) is unaffected — no API diff was supplied this pass. (auto-applied by the truth pipeline; adversarially gated)2026-07-17 — codebase:
foodops.ordering.appe7625a2..2f21ef8 supplies the app-side evidence for two items the 2026-07-17 API-side note (e860cc1..10edb52) had flagged backend-only, plus a minor cart-editing symmetry fix — none contradict documented claims. (1) Out-of-Stock (ADR 0002) now surfaces in the cart/checkout UI:cartSlice.tsadds a transientunavailableVariantIdsarray (cleared on every cart mutation and on server cart load) set via a newsetUnavailableVariantIdsaction;CartSidebar.tsx's guest-submit handler branches onresult.unavailableItemsfrom the submit response, flags the rejected lines, closes the guest-name sheet, and toasts 'Some items are no longer available: {names}. Please remove them and try again.'; matching lines inCartItem.tsxget a red-tinted row + 'Out of stock' badge, and the sidebar shows a red 'Some items are no longer available... sold out while in your cart' banner. This is the frontend counterpart to the stock-availability filtering already documented as backend-only (menu-listing exclusion) in section 1 — it's a new gate at submission time, additive to the existing per-channel (DineIn/Takeout/Delivery) availability flags, not a replacement. (2) Register/POS-session gating reaches the storefront:CartSidebar.tsxnow readsoutletData?.hasActiveRegisterSessionand ORsisOutletClosedinto a newisOrderingBlockedthat gates the 'Checkout as guest' CTA (previously gated only by theIsAcceptingOnlineOrders-derivedisOnlineOrderingDisabled, which this entry documents in section 1 as exempting dine-in/group-cart). Per the dev's own comment this register-closed state rejects submission for every order type including dine-in and group carts — a separate condition from, and not a contradiction of, the documented online-ordering-flag exemption. This confirms the backendHasActiveRegisterSessionfield (flagged 2026-07-17 as backend-only, unconfirmed on the storefront) is now consumed app-side, at least for the guest-checkout CTA. (3) Lower-priority: a removed code comment shows anonymous-cart line edits were previously quantity-only ('Modifier edits are only persisted through the quantity for now');updateItemOptimisticnow accepts optionalmodifiers/allergies/specialInstructionsand the edit-save handler passes them through for anonymous carts too (recomputingtotalPriceviaperUnitModifierTotal), making full-edit capability symmetric between anonymous and authenticated carts — not previously documented as a limitation, so nothing to correct. Re-pinfoodops.ordering.appto2f21ef8on next full verification pass; nofoodops.ordering.apidiff was supplied this pass to confirm theunavailableItems/guest-submit response shape server-side. (auto-applied by the truth pipeline; adversarially gated)2026-07-17 — codebase:
foodops.ordering.apie860cc1..10edb52 adds several backend behaviors not yet reflected in this entry, none contradicting documented claims. (1) Server-authoritative modifier pricing:CartService.AddItemToCartAsync/UpdateCartItemAsyncnow call a newResolveModifierPricesFromCatalogAsync, which re-resolves every submitted modifier's price (and backfills name/group) fromRestaurantModifierGroupItemby id before it's stored or used for the duplicate-line match — a client-submitted price is never trusted, and an unresolvable modifier id is neutralised to price 0. This extends, and doesn't contradict, this entry's 'Server-authoritative cart' key concept and '2. Cart and modifier rules' section (previously documented as totals-only server-authority). (2) Out-of-Stock menu filtering (ADR 0002):GetProductsAsync,GetGroupedProductsAsync,GetProductVariantsAsync,SearchProductsAsync(MenuService) andGetTopSellingItemsAsync(OptimizedMenuService) now all exclude variants that are stock-tracked, disallow negative stock, and have zero available stock (CurrentStockLevel - CommittedStockLevel <= 0) — a new filter dimension alongside the already-documented per-channel (DineIn/Takeout/Delivery) availability flags in '1. Entry and menu availability'. (3) Register-session visibility:GetRestaurantOutletsByOrganizationAsync'sOutletDtogains a newHasActiveRegisterSessionfield (mirrors the open-register-session checkOrderSubmissionServicealready enforces at submission) — comment states intent is to let the app warn up-front, but nofoodops.ordering.appdiff was supplied this pass to confirm the storefront actually surfaces or gates on it yet; treat as backend-only until confirmed. (4) WhatsApp OTP channel:AuthController.RequestOtpnow accepts aChannelfield (OtpChannelParser.Parse(request.Channel)) and callsGenerateOtpAsync(phone, outletId, channel), returning a distinct 'OTP sent successfully via WhatsApp' message whenchannel == OtpChannel.WhatsApp— a second delivery channel alongside the SMS/country-routed provider path this entry documents in section 3 step 2 and the 'Country-routed OTP' key concept. No app-side diff supplied to confirm the storefront UI exposes a channel choice; flag for a follow-up pass before editing the OTP-flow claims. (5) Auth hardening (internal, non-contradicting): the legacy[SkipApiKeyValidation]/Foodops.Ordering.Api.Attributesscheme was replaced by a policy-basedFoodops.Ordering.Api.Authenticationframework —CartControllerandCustomersControllernow carry class-level[CustomerAuthorize](withtotals/anonymousexplicitly[AllowAnonymous], matching this entry's documented anonymous-totals-preview behavior),AuthControlleris[AllowAnonymous], andCustomersControlleradded phone/customer ownership checks (IsOwnPhone/IsOwnCustomerAsync, returningForbid()on mismatch) on profile and address endpoints. This confirms rather than changes the entry's documented session/ownership model. Re-pinfoodops.ordering.apito10edb52on next full verification pass; prioritize an app-side diff to resolve (3) and (4) before editing user-facing claims. (auto-applied by the truth pipeline; adversarially gated)2026-07-14 — codebase:
foodops.ordering.app04776e0..e7625a2 (feat: enhance cart item management with customization signatures and modifier surcharges) changes local cart-line merge semantics insrc/store/slices/cartSlice.ts, not previously documented in this entry's '2. Cart and modifier rules' section. PreviouslyaddItemOptimisticmerged any two adds of the sameproductVariantIdinto one line regardless of modifiers/allergies/notes, and both that merge path andupdateQuantityrecomputedtotalPriceasquantity * unitPriceonly — dropping any modifier surcharge from the line total whenever a line was merged or its quantity changed via the stepper. A newbuildItemSignature(item)builds a canonical, order-independent key (productVariantId::sorted 'id:quantity' modifier pairs::trimmed allergies::trimmed notes), and merging now requires an exact signature match — so the same product added twice with different modifier selections, allergy notes, or special instructions now stays on two separate cart lines instead of collapsing into one. A newperUnitModifierTotal(item)helper is folded intototalPriceon both the merge path andupdateQuantity, so a merged/quantity-changed line's total now correctly reflects its modifier surcharges instead of silently dropping them. This is a client-side (ReduxcartSlice) mechanic, distinct from the backendCartService.AreModifiersEqualcart-line-matching fix already logged 2026-07-13 forfoodops.ordering.api— different repos/layers but the same functional area (accurate per-line modifier identity in the cart); worth cross-referencing next time this section is touched. No contradiction to any documented min/max/MaximumGroupQuantitySelectionclaim — nothing to edit. Re-pinfoodops.ordering.apptoe7625a2on next full verification pass. (auto-applied by the truth pipeline; adversarially gated)2026-07-13 — codebase:
foodops.ordering.api93d14db..e860cc1 (extends the already-pinned 465b53e). Most of this diff is the outlet-cache-disable fix (718a6ac, logged 2026-07-10) and the GetOutletsorganizationId/OutletTypeIdfix (465b53e, logged 2026-07-11) reaching a newer commit unchanged — already covered, no new note needed there. The one genuinely new commit is 'fix: improve modifier comparison logic to handle quantity and group name for accurate cart item matching' (CartService.cs AreModifiersEqual), not previously documented. Cart-line modifier-equality previously compared only Id/Name/Price/Currency via a one-directional 'every modifier in list1 has some match in list2' check — ignoring each modifier'sQuantityandGroupName, and susceptible to false-positive matches with duplicate entries. It now builds a canonical per-modifier signature (Id|Name|Price|Currency|Quantity|GroupName), sorts both modifier lists' signatures, and requires an exact orderedSequenceEqual. Practical effect on this entry's '2. Cart and modifier rules' section: two cart lines that previously could incorrectly merge into one aggregated line despite differing modifier quantity or originating group (e.g. '1x extra shot' vs '2x extra shots', or the same modifier chosen from two distinct modifier groups) now correctly stay as separate cart lines. This is a bug fix to existing, previously-undocumented cart-merge mechanics — it doesn't contradict the entry's documented min/max/MaximumGroupQuantitySelectiongroup-validation claims, so no edit is required, but it's worth folding into the cart section next time that area is touched. Re-pinfoodops.ordering.apitoe860cc1on next full verification pass. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)2026-07-11 — codebase:
foodops.ordering.api93d14db..465b53e (extends the already-pinned 718a6ac) adds the server-side counterpart to the outlet-switcher Restaurant-only filtering logged 2026-07-08/2026-07-10. PreviouslyGET api/menu/outlets(MenuController.GetOutlets) accepted only anoutletReferencequery param and delegated toGetOutletsByReferenceAsync, whoseOutletDtoprojection did not populateOutletTypeIdat all (several other projections —GetOutletByIdAsync,GetOutletByReferenceAsync, andOptimizedMenuService.GetOutletsByReferenceAsync— had the same gap). Since the 07-10 note already describes the storefront's client-side filter (outletTypeId == null || outletTypeId === 2, treating unset as pass-through) and the app sending anorganizationIdparam the API did not yet accept, the org-scoped switcher call would previously have received outlets withOutletTypeIdunset and fallen through the client-side filter unfiltered. This commit closes that gap:GetOutletsnow acceptsorganizationId, routes it to a newGetRestaurantOutletsByOrganizationAsyncthat filters server-side toOutletTypeId == Restaurant(active outlets only), and backfillsOutletTypeIdinto every other outlet DTO projection so the client-side filter has real data to act on everywhere. No contradiction to any main-body claim or to the 07-08/07-10 verification-history notes — this is the backend catching up to make that already-documented client behavior actually effective end-to-end. Also re-confirms (no new behavior) the outlet-metadata cache removal already logged 2026-07-10 on the same three endpoints. Re-pinfoodops.ordering.apito465b53eon next full verification pass; no app-side diff was supplied this pass to confirm the frontend now consumes the neworganizationId-scoped response shape end-to-end. (auto-applied by the truth pipeline; adversarially gated)2026-07-10 — codebase:
foodops.ordering.app2865c43..04776e0 refines the in-header outlet switcher already logged 2026-07-08 with two user-facing details not previously captured (no contradiction to that note or any main-body claim). (1) Restaurant-only filtering:Header.tsxnow discriminates sibling outlets byoutletTypeId(RESTAURANT_OUTLET_TYPE = 2; pertypes/menu.ts, 1 = Retail, 2 = Restaurant) — the switcher list is filtered toorganizationId === currentOrgId && (outletTypeId == null || outletTypeId === 2). So an organisation that also runs Shops/Retail outlets will NOT see those in the FoodOps ordering switcher, only sibling restaurant outlets (outlets with no type set still pass through, defensively). (2) Auto-open on login redirect: landing on/menuwith router statefromOutletLogin: true(straight off the outlet-login/slug redirect) now auto-opens the 'Switch Outlet' drawer once per mount when the org has 2+ eligible outlets (canSwitchOutlet), then clears the navigation state so a refresh or back/forward doesn't reopen it — previously (per the 07-08 note) the switcher only opened via tapping the header 'Welcome to {outlet}' control. Supporting plumbing:getOutlets(menuApi.ts) now takes{outletReference?, organizationId?}to fetch outlets scoped to the current org for the switcher's list, rather than only a string-reference lookup. Remaining hunks in this diff (Drawer footer removal onVariantSelectionDrawer.tsx, header logo icon-only variant, dark-mode text/sticky-bar spacing tweaks) are cosmetic/already covered by the 07-08 event — no new documentation needed there. (auto-applied by the truth pipeline; adversarially gated)2026-07-10 — codebase:
foodops.ordering.api93d14db..718a6ac (fix: disable caching for outlet metadata to ensure immediate updates on branding and configuration changes) removes the previous 1-hour HTTP ResponseCache (Cache-Control: public, max-age=3600, s-maxage=7200) from the three outlet-lookup endpoints —GET api/menu/outlets,GET api/menu/outlet/{outletId}, andGET api/menu/outlet/reference/{reference}(the last is the one this entry cites in 'Entry and menu availability' as the outlet-URL resolution call). All three now respondCache-Control: no-store, no-cache, must-revalidate(ResponseCache(Duration = 0, Location = None, NoStore = true)). Practical effect: outlet-level metadata — name, logo, and theIsAcceptingOnlineOrdersflag that gates this entry's amber 'not accepting online orders' banner — previously could be served stale from a browser/shared cache for up to 1–2 hours after a merchant-side change; it now reflects immediately on next request. This is a purely additive fix (removes staleness, doesn't change any documented UI behavior, banner copy, or endpoint semantics) — no contradiction to existing claims, nothing to edit. Re-pinfoodops.ordering.apito718a6acon next full verification pass. (auto-applied by the truth pipeline; adversarially gated)2026-07-08 — codebase:
foodops.ordering.app(Header.tsx, new OutletSwitcherModal.tsx) added an in-header outlet switcher, not covered by this entry's 'Entry and menu availability' section. When the outlet name/logo query resolves 2+ outlets sharing the current outlet'sorganizationId, the 'Welcome to {outlet}' text becomes a tappable control (now shown on mobile too, not desktop-only as before) that opens a drawer ('Switch Outlet') listing sibling outlets with logo/address, a green 'Current' badge on the active one, and tap-to-switch on the rest. Switching is a hard reset for the previous outlet's session: it clears cart state and any bound table id (outletStorage.clearTableId(),setCartTableId(null)) before persisting the new outlet and doing a full navigation to/menu— consistent with, and reinforcing, this entry's existing 'table-QR session is locked to dine-in' / 'picking an outlet clears table context' claims, so it does not contradict them. This is an additional entry point alongside the three already documented (outlet URL, table QR, bare-root picker) for customers who are already browsing one outlet in a multi-outlet organisation. Cosmetic-only in the same diff: header logo shrunk (35px→28px height, 'full' wordmark→icon-only 'menu' variant on fallback), and category headings/VariantSelectionDrawer got minor dark-mode/interaction tweaks (variant picker now adds an item immediately on tap instead of select-then-'Add Item') — none rise to documented-behavior changes for this entry. (auto-applied by the truth pipeline; adversarially gated)2026-07-04 — codebase:
foodops.ordering.api@93d14dbadds a guest/walk-in order-submission path not covered by this entry.POST order/guest-submit(PaymentController.cs) requires NO session token, phone, or OTP — only a customer name (min 2 chars, guarded via ModelState/length check) — and is dine-in/takeaway only (explicitBadRequestwhenorderType == "delivery"). Payment is hardcoded tocash('settled in person; no payment processed' per comment) and cart items arrive inline and are server re-priced (RepriceGuestItemsAsync). The order links to a shared per-organisation 'Walk-In Customer' record (CustomerService.GetOrCreateWalkInCustomerAsync, mirroring the counter/POS walk-in account, created viaAccountHelper.CreatePosCustomerAccountwith a race-safe re-query) rather than a phone-verified customer; the guest's name is recorded in order notes asGuest: {name}(pipe-joined with any typed instructions), andOrderSubmissionService.SubmitOrderAsyncnow skips session validation entirely whenrequest.IsGuest. This is an alternate path alongside — not a replacement for — the OTP-based checkout this entry documents in section 3 and the 'Phone is the account' key concept; nofoodops.ordering.app(order.foodops.io storefront) diff was supplied in this pass, so it is NOT yet confirmed whether/how guests trigger this from the customer-facing UI (vs. e.g. a counter/staff-side walk-in entry point). Flag for a follow-up pass with app-side evidence before editing the OTP-mandatory claims. Separately and lower-priority:OrderController.cs(GetCustomerOrderDetailsandGetCustomerOrders) now populatesDeliveryAddressfrom the order'sShippingAddress.AddressLine1for delivery orders — previously hardcoded to""with a 'TODO' comment — a plumbing fix that doesn't contradict any documented tracking behavior in this entry. (auto-applied by the truth pipeline; adversarially gated)2026-07-XX — codebase: foodops.ordering.app added a name-only guest checkout path for anonymous dine-in/takeaway carts (
GuestNameSheet+POST /order/guest-submit, gated byshowGuestCheckout= anonymous && !groupMode && orderType in {dinein, takeout} && cart non-empty && ordering enabled). No phone or OTP is collected; cart items and the guest's name are submitted inline (GuestOrderSubmissionRequest), and on success the cart clears and an order reference is shown, but the guest is not routed to the trackable/orders/{id}page (login-gated) and the CSAT drawer is skipped. Delivery and group-cart checkout are unaffected and still require phone+OTP. Separately, OrderDetailsPage.tsx hardened the transfer-slip warn-but-allow flow with deferred toast/dialog timing and a ~600ms phantom-click-through guard on 'Upload Anyway' — implementation-only, no change to the documented dialog copy or button labels. (auto-applied by the truth pipeline; adversarially gated)2026-06-11 — codebase: entry seeded against
foodops.ordering.app@09e5e61+foodops.ordering.api@d44572f(both main). Scope split from the umbrella online-ordering.md: this entry owns the customer-facing depth the umbrella lacked — payment options (BML initiate / cash process / transfer receipt-upload endpoints + per-outlet enablement + allow-payment-without-acceptance), the logged-out tracking-link flow (OrderAccessPage), exact OTP provider routing (SmsServiceFactory: India→Fast2SMS by country name, MsgOwl default), and the two changes newer than the umbrella's 2026-06-04 pass (modifierMaximumGroupQuantitySelectionvalidation5bd675f/2a9e64b; overlay back-button historyb0b419a). Minimum-order-per-zone and customer self-cancel verified NOT FOUND in storefront code. Confidencemedium; payment sheet / transfer upload / order-access visuals pending Playwright.2026-06-17 — codebase (drift triage): corrected the "no self-cancel" edge case.
foodops.ordering.app@b63bce5now ships customer self-cancel (commit3ad4207: Cancel button +ConfirmationDialog, gated bycanCancelOrder= order still "New"/pre-acceptance,orderService.cancelOrder+ "Order cancelled" toast) plus cancelled-order status display. Re-pinned ordering.app ee910dd→b63bce5. (foodops.ordering.apialso gained SignalR order-cancellation notifications + a net9→net10 bump in the same window — not separately re-verified this pass.)2026-06-29 — codebase: NEW-STACK re-verification for July-10 launch. Re-pinned
foodops.ordering.app@25f1e9a82e+foodops.ordering.api@80e07fdf7a. AI transfer-slip validation RE-CONFIRMED present and live across all three repos:TransferSlipValidationService.cs:18-31,58-63uses Google Gemini (gemini-2.5-flash), inert whenGemini:ApiKeyunset;TransferSlipVerdictBuilder.cs:23-59compares amount/currency/date only (CORRECTED the prior "amount/currency/date/reference" claim — no reference comparison exists); storefront warn-but-allowConfirmationDialogatOrderDetailsPage.tsx:757-764with verbatim copy ("Slip may not be valid" / "Upload Anyway" / "Choose Another"); counter staff "transfer slip needs review" toast atuseSignalR.ts:188-201. Re-pointed the "POS/monolith notifies the ordering API" menu-publish line off the retired monolith. OTP country routing, payment options, tracking-link flow, self-cancel all still match main.
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.