Foodops
All docs

Back Office · Updated 2026-08-22 · vbb9ebc8

Customers & CRM

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

Plain-English summary

Customers are the people who owe you money or have a standing relationship. Walk-ins don't need a customer record — they pay on the spot and walk out. But corporate clients with monthly tabs, loyal regulars with credit, catering customers, and anyone getting an invoice all need a row here so Foodops can track outstanding balances, payment terms, and credit limits.

When you'd use this

  • A company books catering and pays Net-30 → create a customer record with Net 30 payment term
  • A regular has a running tab → attach their name to orders, pay off the balance weekly
  • You want to see "who are my top 10 customers by revenue" → populated customer records feed Sales By Customer
  • Issuing formal invoices with customer TIN for tax compliance
  • Applying a standing discount to a customer's orders without manually entering it each time

If you only do cash walk-in sales, you'll rarely touch this screen — "Walk In Customer" is the default on every POS order.

How to access

  • URL: /customers
  • Sidebar: customers icon in the Foodops sidebar
  • From POS: order creation screen → Customer field → search or "Add New"

Customers list — 5 tab filters (Active/Inactive/Overdue/Unpaid/All) and columns Customer/Reference/Contact/Outstanding/Credits

Prerequisites

  • Role with Customers: View/Create permissions (default on Counter / Manager / Owner / Administrator)
  • For GST-registered customer invoicing: a Tax configuration with GST (Settings → Taxes)
  • For credit-term customers: knowledge of agreed payment terms and credit limits

Walkthrough — create a customer

1. Open the Customers list

Navigate to /customers. The list has a 5-tab filter — Active · Inactive · Overdue · Unpaid · All — with columns Customer, Reference, Contact, Outstanding, Credits. (Email and Mobile are not separate columns — contact details are merged under Contact; Reference is the customer reference code; Credits shows unallocated credit-note balance.)

2. Click "New Customer"

The full "Add Customer" form opens. Structurally mirrors the Supplier form — both customers and suppliers are financial counterparties with the same field set. This full form is the one reached from the /customers management screen; the quick-add from the POS order screen ("Add New" in the customer picker) is a lighter form — see the note below the table. [code: foodops.counter.app/src/components/sales/CustomerFormDialog.tsx — title "Add Customer"/"Edit {name}", line 230]

Add Customer modal — Name, Registration Number, Preference, Email/Phone/Mobile, Date of Birth, TIN, Default Currency, Discount, Tax Type, Payment Term, Credit Limit, Assigned User, Billing Address

Field Purpose
Name Display name (individual or company)
Registration Number Company registration — for B2B customers (stored as IdentificationNo)
Preference Preferred contact method (Email / Phone / Mobile) — routes automated comms
Email / Mobile / Phone Contact details (Mobile is required)
Date of Birth Optional — feeds birthday/segmentation filters in CRM reports
TIN Number Tax Identification Number — required on GST tax invoices
Default Currency MVR by default; change for international customers
Discount Standing discount stored on the customer record. Note: as of the verify below, the counter (POS) order/invoice pipeline does not auto-apply it — see the "auto-discount not applying" edge case and the discount Q&A.
Tax Type Zero or GST — drives the tax lines on their invoices
Payment Term Due on Receipt / Net 5-60 — drives overdue calculations
Credit Limit Cap on outstanding for credit sales — a credit sale that would exceed it is blocked at the POS (see edge cases / credit-limit Q&A)
Credit Status Required — the customer's credit standing
Assigned User Internal relationship manager
Billing Address Address Line 1, Address Line 2, City, Post Code, Country (default Maldives)

[code: foodops.counter.app/src/components/sales/CustomerFormDialog.tsx — Row labels Name/Registration Number/Preference/Email/Phone/Mobile(required)/Date of Birth/TIN Number/Default Currency(required)/Discount/Tax Type/Payment Term/Credit Limit/Credit Status(required)/Assigned User + Billing Address; posts to /v1/customers via customerService]

Note: Two different customer forms The full form above (with Registration Number, DOB, Phone, Discount, Tax Type, Payment Term, Credit Limit, Credit Status, Assigned User, Billing Address) is CustomerFormDialog — reached from the /customers list and the Customer detail page. The quick-add reached from the POS order screen's customer picker ("+ New Customer") is the lighter NewCustomerModal, which only collects Mobile (required), Name, Email, TIN Number, a Delivery Address, and (as of a3fe6a1) an optional Discount. To set a payment term, credit limit, tax type, etc., use the full form on /customers. [code: foodops.counter.app/src/components/customer/NewCustomerModal.tsx — fields Mobile(required)/Name/Email/TIN Number/Discount(optional) + Delivery Address; src/components/orders/OrderSummary.tsx renders it]

3. Save

Customer is immediately available in the POS customer picker. The Outstanding balance starts at MVR 0.00.

Walkthrough — attach a customer to an order

  1. Start a new order at the POS (Takeout, Dine-in, or Delivery)
  2. In the right-panel order summary, click the Customer field (defaults to "Walk In Customer")
  3. Search by name, email, or mobile — results pull from the customers list
  4. Select the customer — the order now links to their record
  5. The customer's Tax Type drives tax handling; on a credit sale (Charge to customer account), the order amount adds to their Outstanding balance instead of being collected at checkout. Note: a customer's stored standing Discount is not auto-applied to the order by the counter pipeline — discounts on a POS order are applied per-order/per-line (or via a loyalty program), not pulled from the customer record. [code: hubits-api-sales/Hubits.Sales.Api/Services/Counter/Orders/OrderCreationService.cs — discounts come only from viewModel.DiscountId (order), line-item DiscountId, and loyalty DiscountId; the customer's own DiscountId is never read here]

Outstanding balance lifecycle

When a customer on Net-30 terms places an order:

  1. Order saved → Outstanding += order_total on the customer's record
  2. 30 days pass → order becomes overdue, appears on Overdue tab
  3. Customer pays → record the payment against the customer; Outstanding decreases
  4. When Outstanding = 0, customer disappears from Overdue/Unpaid tabs

The Customer Balances and A/R Aging Summary reports (see Reports Catalogue) surface overdue customers to the collections team.

Key concepts

  • Customer — a person or company with a financial relationship beyond single walk-in transactions. Has contact info, default payment terms, currency, tax type, credit limit, assigned user, and an outstanding balance.
  • Walk In Customer — the default "unknown customer" record. POS orders that aren't linked to a specific customer go here. No credit, no receivable — order is paid at checkout.
  • Outstanding balance — sum of (invoiced orders) minus (recorded payments). Shown in MVR on the list.
  • Payment Term — same semantics as supplier: Due on Receipt (immediate) or Net N (N days after order). Drives when an order becomes "overdue".
  • Credit Limit — cap on credit exposure. It is enforced only on credit sales (Charge to customer account / "Credit" / "Credit & Close"): a credit sale whose total would push Outstanding above the limit is blocked at the POS with a "Credit Limit Exceeded" error — there is no in-app override. A cash/card sale is never blocked by the credit limit (it adds nothing to Outstanding). [code: foodops.counter.app/src/components/orders/OrderDetailsPanel.tsx:1472-1484 + PaymentSettlementModal.tsx:184-199 — if (!check.isAllowed) { toast.error('Credit Limit Exceeded'); return; }; hubits-api-sales/CounterCustomersController.cs:381-400 CheckCreditLimit]
  • Standing Discount — a discount can be stored on the customer record (full /customers form), but as of the verify below the counter (POS) order/invoice pipeline does not read or auto-apply it. POS discounts are applied per-order, per-line, or via loyalty programs. [code: hubits-api-sales/OrderCreationService.cs + InvoiceHelper.cs — customer DiscountId never consulted during pricing]
  • TIN — Tax Identification Number. Required on formal GST tax invoices issued to registered businesses.
  • Date of Birth — not just ornamental — feeds birthday-marketing filters in customer segmentation and reports.

Common questions

Q: Is Walk In Customer a real customer record? A: Yes — it's the catch-all row for unidentified orders. Don't delete it; don't try to add a custom "Walk In" — the system expects the default record. It's a protected record: the counter app refuses to edit it (the Edit endpoint rejects the Walk-In / hidden customer). [code: hubits-api-sales/CounterCustomersController.cs:1087 — if (customer.Name == WalkInCustomerSimple || customer.IsHidden) return BadRequest("This customer cannot be edited")]

Q: Can I search for a customer by phone number? A: Yes — the POS customer search matches Name, ID/Registration number, Mobile, Phone, and Reference. It does not match email, and you need to type at least 2 characters. If a customer doesn't come up, they may be Inactive or hidden — check the Customers list (/customers). [code: hubits-api-sales/Hubits.Sales.Api/Controllers/CounterCustomersController.cs:577-583 SearchCustomersInternal — Name / IdentificationNo / Mobile / Phone / Reference; min 2 chars]

Q: How does credit limit work during POS checkout? A: The credit limit is checked only when settling an order on credit (Charge to customer account / "Credit" / "Credit & Close"). If (current Outstanding + order total) > credit limit, the POS shows a "Credit Limit Exceeded" error and blocks the credit sale — there is no in-app override or manager-approval bypass. To proceed, either capture a payment now (a cash/card sale isn't subject to the credit limit), settle some of the customer's outstanding balance first, or raise their Credit Limit on the customer record. A Credit Limit of 0 (or unset) means unlimited credit. [code: foodops.counter.app/src/components/orders/OrderDetailsPanel.tsx:1472-1484 + PaymentSettlementModal.tsx:184-199; hubits-api-sales/CounterCustomersController.cs:381-400 — if (creditLimit <= 0) allow; else if (currentOutstanding + amount > creditLimit) not allowed]

Q: Can I delete a customer with past orders? A: No — a customer who has any transactions or a non-zero outstanding balance cannot be deleted. The two cases return different messages: a non-zero balance gives "Customer has an outstanding balance and cannot be deleted. Settle the balance first, or mark the customer as Inactive."; existing transactions give "Customer has transactions and cannot be deleted. Mark the customer as Inactive instead." A customer with no transactions and a zero balance can be deleted — this is a soft delete (the record is set to a Deleted status and dropped from all views, not physically removed). For customers with history, mark them Inactive instead. (There is no merge-duplicates feature — deduplicate by deactivating the spare record.) [code: hubits-api-sales/Hubits.Sales.Api/Controllers/CustomersController.cs:391-423 Delete — 400 at line 403 if Outstanding != 0, 400 at line 413 if Transactions exist; otherwise sets RecordStatusId = RecordStatus.Deleted (line 419)]

Q: What happens to a customer I set Inactive? A: Status flips to Inactive (a dedicated status-change action — the row's 3-dot menu shows Deactivate for an Active customer and Activate for an Inactive one). POS customer search returns only Active customers, so an Inactive customer no longer appears in the order-screen picker; historical orders remain linked. Reactivate any time via Activate. [code: foodops.counter.app/src/components/sales/CustomerTable.tsx:219-233 — menu items "Activate"/"Deactivate" → onStatusChangecustomerService.changeStatusPUT /v1/customers/{id}/status; CounterCustomersController.cs:574 search filters RecordStatusId == Active]

Q: What's the difference between a customer discount and an order discount? A: A customer-level Discount can be stored on the customer record, but the counter (POS) order/invoice pipeline does not currently pull it onto orders automatically — so in practice POS discounts are order discounts: applied manually at the POS for a specific transaction (e.g., "manager approves 10% off this order only"), or via a loyalty program. If you've set a Discount on a customer and it isn't showing on their orders, that's expected behavior, not a bug. [code: hubits-api-sales/OrderCreationService.cs — only order/line/loyalty DiscountId are applied; customer DiscountId is never read at pricing time]

Q: Can one customer have multiple addresses? A: The form stores one billing address. For customers with multiple delivery locations, add them as separate customer records (e.g., "Acme Co. – Malé" and "Acme Co. – Hulhumalé") or use the Notes field on each order.

Q: Can I bulk-import customers from a spreadsheet? A: Yes — bulk import/export is an admin-app (admin.foodops.io) action, or on-demand through support — it maps columns to Foodops fields. Note: the Import and Export menu items were removed from the in-POS counter Customers list on 2026-05-14 (abfd5b70), so bulk import/export is no longer in the counter app's /customers 3-dot menu. (The /v1/customers/import + /export endpoints and customerService.import/export methods remain live — only the counter UI menu was removed.)

Q: Where does a customer's order history appear? A: The Customer detail page (click a row) shows orders, payments, outstanding, balance history. The Sales By Customer report aggregates across customers for comparative views.

Q: Is there loyalty / points / reward-tier functionality? A: Partial as of 2026-05-17. The customer detail page now has a Loyalty tab that shows the programs a customer is enrolled in, their points balance, the accrued amount, and the enrolment date (read view; the customer detail page must be online — the tab shows a "Loyalty programs require a connection" state when offline). Programs themselves are still configured outside this Customers screen (legacy admin: Loyalty Programs), and there's no points-redemption flow at the POS yet — but per-customer enrolment + points are no longer absent. [code: foodops.counter.app/src/components/sales/CustomerLoyaltyProgramsTab.tsx; src/pages/CustomerDetailsPage.tsx:150 — a13f952c 2026-05-17]

Q: How does this screen differ from the Suppliers screen? A: Structurally almost identical — same field shape, same list filters, same outstanding-balance concept. The key difference is direction: Suppliers = money you owe OUT, Customers = money owed TO you. Under the hood they share most of the partner-management code.

Edge cases and known issues

Customer "standing Discount" does not apply on POS orders

  • Symptom: Customer has Discount = 10% set on their record, but POS orders for that customer don't reflect it
  • Cause: This is expected behavior, not a bug. The counter (POS) order/invoice pricing pipeline does not read the customer's stored Discount — POS discounts are applied per-order, per-line, or via a loyalty program, never auto-pulled from the customer record. [code: hubits-api-sales/Hubits.Sales.Api/Services/Counter/Orders/OrderCreationService.cs — discount application uses only order/line/loyalty DiscountId; customer DiscountId is never consulted]
  • Workaround: Apply the discount on the order itself at the POS (order- or line-level discount), or enrol the customer in a loyalty program that carries a discount. The customer-record Discount field is stored but is not an automatic POS discount today.

Credit sale blocked ("Credit Limit Exceeded") when customer appears to have credit remaining

  • Symptom: A credit sale is blocked with "Credit Limit Exceeded" but the cashier believes the customer is within their limit
  • Cause: The check uses current Outstanding + this order total vs the Credit Limit. Recently-received payments may not have been recorded yet, or a prior order that should have been paid is still Outstanding — so the system's Outstanding is higher than the cashier expects. When offline, the check uses the cached Outstanding, which can be stale. [code: hubits-api-sales/CounterCustomersController.cs:374-400 — currentOutstanding = totalInvoices - totalPayments - totalCreditNotes; foodops.counter.app/src/services/customer/customerCreditService.ts — offline path computes from cachedOutstanding]
  • Workaround: Take a cash/card payment instead (not subject to the credit limit), or record any missing payments / settle outstanding first, then retry the credit sale. There is no manager-override to bypass the block; if the limit is genuinely too low, raise the customer's Credit Limit. Open customer detail to verify current Outstanding and recent payments.

TIN missing on tax invoice

  • Symptom: Tax Invoice prints without the customer TIN even though it's set
  • Cause: Customer was attached to the order AFTER the invoice was generated. Invoices carry their own snapshotted TINNumber (and Phone/Mobile/Email) stamped at generation — they don't live-join back to the customer record — so a TIN added or a customer attached after generation won't appear on the already-generated invoice. [code: haafai.domain/Haafai.Models/Entities/Accounting/Invoices/Invoice.cs — invoice has its own TINNumber/Phone/Mobile/Email fields + GeneratedDocumentContent (the rendered PDF is also stored)]
  • Workaround: Cancel the invoice and regenerate it (or issue a credit note + new invoice) — invoices snapshot customer data at generation time.

Customer appears on Overdue tab despite being paid

  • Symptom: Customer's outstanding shows 0 but they're still on Overdue
  • Cause: Two layers. (1) Customer.Outstanding is a persisted ("cached") column the /customers list and Overdue tab read directly; it's refreshed by Customer.UpdateAccountBalance, which the counter credit-collection settle path calls after a payment [code: hubits-api-sales/Hubits.Sales.Api/Controllers/CounterPaymentsController.cs:882 @ 19190b9 — "await customer.UpdateAccountBalance(_context);"]. (2) The counter app also caches Outstanding client-side and invalidates it after settling, so a list rendered just before the invalidation lands shows the old state briefly. The credit-limit check itself recomputes Outstanding live (invoices − payments − credit notes), so it is never stale [code: CounterCustomersController.cs:357-374 @ 19190b9]. After a counter payment both the cached column and the client cache refresh, so any residual stale-Overdue is a client-cache timing artifact. (Edge note — FIXED as of hubits-api-sales@a4c6ced: an admin-side receive-payment via the sales-api PaymentsController.Create previously did not refresh the cached column, so an admin receive could leave the list's Outstanding stale. PaymentsController.Create now calls RefreshCustomerBalance immediately after recording the payment — and the amount-change Edit path does the same — so admin-side receive-payments refresh the cached column right away, same as the counter path.)
  • Workaround: Refresh the page. If it persists more than a few minutes, check that the payment was actually recorded against the correct customer and order.

Customer search on POS returns nothing for a customer that exists

  • Symptom: A customer is in the system but typing into the POS customer-search box returns no match
  • Cause: POS search matches on name, TIN/ID number, mobile, phone, and reference — it does not match on email — and it needs at least 2 characters typed. Inactive, hidden, and deleted customers are excluded, and results are scoped to the current organization. So an empty result usually means fewer than 2 characters were typed, the value searched lives on a field that isn't indexed (e.g. email), or the customer is Inactive/hidden — not that phone search is unsupported (mobile and phone are both searchable). [code: hubits-api-sales/Hubits.Sales.Api/Controllers/CounterCustomersController.cs — SearchCustomersInternal matches Name / IdentificationNo / Mobile / Phone / Reference; min 2 chars]
  • Workaround: Search by name, mobile, or phone (all work); type at least 2 characters. If the customer still doesn't appear, confirm they aren't Inactive/hidden via the Customers list (/customers).

Plan limits

Plan tiers and their limits are not listed here — they are DB-backed and change with the lineup. See Pricing for the live catalogue.

Confirm exact boundaries against the in-app subscription page.

API and integration notes

  • Customers and Suppliers share much of their data model (both are "BusinessPartner" in the domain)
  • CSV export/import is an admin-side action (admin.foodops.io; /v1/customers/import + /export endpoints) — the 3-dot Import/Export was removed from the in-POS counter Customers list on 2026-05-14 (abfd5b70); the endpoints + customerService.import/export remain live
  • Customer events on key actions: CustomerCreated, CustomerPaymentRecorded, CustomerBalanceUpdated — useful for CRM sync
  • The standing Discount is stored on the customer record (Customer.DiscountId) but is not resolved at counter order-pricing time — the POS order/invoice pipeline never reads it; POS discounts are order/line/loyalty-level. [code: hubits-api-sales/OrderCreationService.cs]
  • POS customer search matches Name / IdentificationNo (ID/Registration No) / Mobile / Phone / Reference — it does not match email — and needs ≥2 characters. (See the search edge case below for the full predicate.) [code: hubits-api-sales/Hubits.Sales.Api/Controllers/CounterCustomersController.cs:577-583 SearchCustomersInternal]
  • Architecture (counter.app vs admin app): the React counter app (foodops.counter.app) holds the in-POS customer surface — src/pages/CustomersPage.tsx, CustomerDetailsPage.tsx, src/components/customer/ (the New/Edit modals), src/components/customers/ (statement-related modals), src/components/sales/CustomerFormDialog.tsx/CustomerTable.tsx, and src/components/ui/customer-combobox.tsx for the order-attached customer picker. The deeper customer admin (full CRUD, imports/exports, credit notes, statements) now lives in the admin app (foodops-admin-app, admin.foodops.io) backed by the sales API (hubits-api-sales CustomersController/CounterCustomersController). The legacy haafai.app.foodops Blazor monolith that previously hosted these views is retired at the July-2026 launch. All surfaces share the underlying BusinessPartner/Customer domain model (haafai.domain). NEW 2026-07 — admin-app customer form is now a full page, not a modal: standalone customer create/edit in the admin app (admin.foodops.io) navigates to dedicated routes — the Customers list's New Customer button goes to /sales/customers/new and row-edit to /sales/customers/:id/edit, both hosted by a new CustomerFormPage wrapping a shared CustomerForm — while CustomerFormDialog is retained only as a thin modal for the inline "New Customer" quick-add on quote/invoice/sales-order forms. (The counter-app form documentation above is unaffected — this changes the admin app only.) [code: foodops-admin-app/apps/admin/src/App.tsx — routes /sales/customers/new + /sales/customers/:id/edit → CustomerFormPage (lazy import); apps/admin/src/pages/sales/CustomersPage.tsx:231,236 — navigate('/sales/customers/new') / navigate to /sales/customers/{id}/edit; apps/admin/src/pages/sales/CustomerFormPage.tsx + apps/admin/src/components/sales/CustomerForm.tsx (added in 839b3b3..d4ac7b5)]

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

The React counter app shipped meaningful customer-flow improvements; the legacy monolith customers views were dormant.

foodops.counter.app — New customer modals + offline-friendly customer creation

  • NewCustomerModal — optional address handling (NEW 2026-04-28, 9974e29) — creating a customer from the in-POS modal no longer forces address entry; cashiers can capture name + contact and add address later. Touches src/components/customer/NewCustomerModal.tsx + src/components/orders/OrderSummary.tsx (the picker that triggers it).
  • CustomerCombobox keyboard navigation (NEW 2026-04-25, 3eb991a) — type-to-search with arrow-key + Enter resolution; faster customer attachment to orders during peak service (touches customer-combobox.tsx, NewCustomerModal.tsx, OrderSummary.tsx, PaymentSettlementModal.tsx).
  • Customer contact + address modals + sync status components (NEW 2026-04-24, de7695c) — broader rollout of CustomerContactModal, EditAddressModal, EditCustomerModal, NewAddressModal, NewCustomerModal under src/components/customer/. Customers can be edited inline from the order screen.
  • Live sync for offline-created customers (NEW 2026-04-28, ecaf946)CustomerDetailsPage.tsx now shows live sync status for customers created while the POS was offline, with explicit retry. Pairs with the broader offline-first POS rewrite.
  • Statement-related modals (early 2026-04 baseline)EmailStatementModal.tsx, StatementPreviewModal.tsx under src/components/customers/ (note: separate dir from the singular customer/) for the A/R workflow.

Legacy monolith — retired at the July-2026 launch

Haafai.Sales/Views/Customers/** and Haafai.Sales/Controllers/CustomersController.cs (the old haafai.app.foodops Blazor monolith) saw 0 commits since 2026-04-15 and are decommissioned at the July-2026 launch — app.foodops.io now runs the new stack (admin app + counter.app + hubits-api-*). The deep admin views it once hosted (Imports, OutstandingComparison, CreditNotesCard, LoyaltyProgramsCard, etc.) are superseded by the admin app (admin.foodops.io). Retained here only as historical context.

Verified 2026-05-04 (Playwright):

  • NewCustomerModal address optional: confirmed. The "Add Customer" modal shows required markers only on Default Currency and Mobile; Name, Email, Date of Birth, TIN Number, and the entire Delivery Address section (Address Line 1, Address Line 2, City, Post Code) have no required indicators. The "Press Enter to save" hint is shown at the bottom.

    NewCustomerModal — Name/Email/DOB/TIN optional, Mobile+Currency required, Delivery Address section optional

  • CustomerCombobox dropdown: confirmed. Opens as a select2 dropdown with type-to-search. The first result is highlighted in blue/purple; "Walk In Customer (WAL001)" appears by default. A "+ New Customer" link appears at the bottom of the dropdown list as a persistent shortcut to showNewCustomerForm().

    CustomerCombobox dropdown — search field, Walk In Customer (WAL001) highlighted, + New Customer at bottom

  • Offline-customer sync banner: not verifiable in the 2026-05-04 pass — CustomerDetailsPage.tsx sync status only appears when a customer was created while the POS was offline and is pending sync. No offline-created customers exist in the E2E test org.

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

The counter app added three customer-facing surfaces since the last verify; the legacy monolith customer views remained dormant.

  • Loyalty tab on the customer detail page (NEW 2026-05-17, a13f952c) — read view of programs the customer is enrolled in, with Points balance, Amount, and Enrolled-on date; requires connectivity. [code: foodops.counter.app/src/components/sales/CustomerLoyaltyProgramsTab.tsx; src/pages/CustomerDetailsPage.tsx:150] See the loyalty Q&A above.
  • Credit-payment collection (NEW 2026-05-14, 1d5ae62f) — an "Accept payment" button on the customer's financial strip (and a CollectCreditPaymentButton on the POS toolbar) opens a PaymentModal to settle outstanding credit directly from the customer detail page or POS, invalidating the cached outstanding balance afterward. This is a faster path than the previously-documented "record the payment against the customer" flow. [code: foodops.counter.app/src/components/layout/Sidebar.tsx; e2e/13-credit-payment-collection.spec.ts — 1d5ae62f]
  • CustomerCombobox now shows last-used delivery address (NEW 2026-05-13/19, 9e5f9ab9/dbf805db) — the order-attached customer picker displays each customer's lastUsedAddress in the dropdown row; the search API (CounterCustomersController) now returns LastUsedAddress, and primary-address demote-on-set bugs were fixed on both address controllers. [code: foodops.counter.app/src/components/ui/customer-combobox.tsx:471; hubits-api-sales/Hubits.Sales.Api/Controllers/CounterCustomersController.cs — dbf805db]
  • Mobile included in modal-created customers (2026-05-30, 06d4e4c0)NewCustomerModal now carries the mobile number through customer creation and onto receipt printing.

Legacy monolith — retired (2026-07)

Haafai.Sales/Views/Customers/** and Haafai.Sales/Controllers/CustomersController.cs saw no further commits through 2026-06 and are retired at the July-2026 launch — the customer admin surface is now the admin app (admin.foodops.io). The deep admin views (Imports, OutstandingComparison, CreditNotesCard, LoyaltyProgramsCard) are superseded there.

Related

  • POS Takeaway Order — where customers are attached to orders

  • Suppliers & Purchase Orders — structurally parallel; shares data model

  • Reports Catalogue — Sales By Customer, Customer Balances, A/R Aging reports

  • Settings Overview — Taxes and Discounts configuration that shapes customer behaviour

  • User Stories: Customers — relationship-management scenarios

  • 2026-08-21 — foodops.counter.app commit range 3900ab4..a3fe6a1 (prod) adds a Discount select to both NewCustomerModal (the POS-order-screen quick-add, previously documented as Mobile/Name/Email/TIN Number + address only) and EditCustomerModal (the inline edit from the order screen), backed by a new listAssignableDiscounts() service. NewCustomerModal now posts discountId on create; EditCustomerModal now hydrates and round-trips the customer's existing discountId on save, fixing a load-bearing bug (an unrelated-field save previously wiped the customer's discount wholesale) — covered by a new EditCustomerModal.discount.test.tsx. CustomerFormDialog.tsx (the full /customers form, already documented as having a Discount field) got the same hydration fix (it previously hardcoded discountId: '' on every edit). This is customer-DATA-MODEL plumbing — setting/persisting/carrying the discount — consistent with the already-logged 2026-08-17/18 hubits-api-sales reviews that carried DiscountId through the counter customer DTOs. The commit range's subject is explicit — 'feat(counter): auto-apply the customer's assigned discount on ordering' — and both new UI blocks say 'Applied automatically to this customer's orders,' but per trust-bodies-not-comments: no hunk in this diff touches order-creation/pricing code (OrderSummary.tsx or any order/pricing service) — only customer-CRUD files are in this entry's tracked surface, so the pricing-side change (if any landed in this same range) isn't visible here. This is the clearest signal yet that the long-flagged discount-auto-apply feature is landing; top priority for the next pass is to pull the order-creation/OrderSummary.tsx diff for this range and live-verify by setting a Discount on a customer via the POS quick-add or edit modal, then placing an order for them, to see whether the bill now auto-discounts. Separately, this same range also rebuilds the counter's Customers list and detail page onto shared admin-style list/table primitives — CustomerTable.tsx (this entry's cited evidence file for the Activate/Deactivate 3-dot menu, lines 219-233) is deleted outright and replaced by a generic DataTable, and CustomersPage/CustomerDetailsPage are restructured (new pagination hooks, StatusBadge, CustomerHeaderActions, tabbed detail layout). The five filter-tab labels (Active/Inactive/Overdue/Unpaid/All) are unchanged in the diff shown, but the new column defs and the Activate/Deactivate menu aren't visible in the truncated hunks — worth confirming those survived the rebuild on the next pass. Two entry edits applied this pass (NewCustomerModal's field list; the Standing-Discount key-concept's 'full /customers form'-only claim) — the counter POS order/invoice pricing pipeline's non-application of the discount remains undisputed by this diff. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-19 — hubits-api-sales commit range f5259ce..98c202d (prod) is a wider superset of the already-logged 2026-08-17 (f5259ce..8808c93: transfer-reference enforcement) and 2026-08-18 (f5259ce..85d9c44: discount carry-through onto CustomerSearchDto/CustomerDetailsDto/OfflineCustomerDto/CreateCustomerDto/UpdateCustomerDto) reviews — both unchanged, no new action on those; OrderCreationService.cs (this entry's evidence that a customer's standing Discount is never auto-applied at POS pricing time) is still untouched in this wider diff. New in this range, from the added commit 'feat(invoices): implement invoice settlement logic and add related tests': CounterPaymentsController.AddPayment's invoice- and retainer-invoice FullyPaid transition, previously an inline Outstanding <= 0 check, now calls a new InvoiceSettlement.IsSettled(outstanding) helper for both document types. This entry's Outstanding-balance-lifecycle section documents Customer.Outstanding reaching zero (a different, persisted column already covered by the 'Overdue tab despite paid' edge case), not the Invoice/RetainerInvoice FullyPaid status flip, so nothing currently written is contradicted — but the helper's method body isn't in this diff, so it's unproven whether IsSettled still means a strict <= 0 or introduces a rounding/tolerance band, which would subtly change exactly when an invoice (and by extension the customer's cached Outstanding via UpdateAccountBalance) is treated as paid off. Also new: SearchCustomersInternal (behind the documented POS customer search) was restructured into an explicit multi-stage search — a shared Candidates() filter plus a 'Stage 1: prefix (sargable) match' on Name/IdentificationNo/Mobile/Phone/Reference with a new deterministic Skip/Take ordering for stable pagination. The visible Stage 1 hunk still matches only those five fields (not email) and doesn't touch the documented 2-character minimum, so nothing here is contradicted either, but the stage split and its 'stays on the fast path' comment imply at least one further fallback stage not shown in this diff — worth checking on the next pass that it doesn't add email matching or otherwise change the documented match/2-char rules. Also unreviewed in this range: 'fix(invoices): raise invoices in the customer's currency' — no hunk intersecting this entry's tracked Customers/Payments/Dto globs was supplied, so no evidence either way. Priority for next pass: pull InvoiceSettlement.IsSettled's body and the full (untruncated) SearchCustomersInternal diff before deciding whether either warrants an entry edit. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-18 — hubits-api-sales commit range f5259ce..85d9c44 (prod) is a superset of the already-logged 2026-08-17 review (f5259ce..8808c93: TransferReferenceValidator wiring on PaymentsController.Create + the new CounterPaymentsController transfer-reference-check endpoint — unchanged, no new action on those). New in this wider range: CounterCustomersController now carries the customer's assigned Discount through the counter API surface that this entry extensively documents as NOT applying to POS orders. CustomerSearchDto, CustomerDetailsDto, OfflineCustomerDto, CreateCustomerDto and UpdateCustomerDto all gain a DiscountId field (CustomerDetailsDto also gets DiscountName), SearchCustomersInternal's three query paths and GetCustomerDetails now project/include it, and CreateCustomer persists dto.DiscountId (normalized: Guid.Empty -> null) onto the new Customer row after a new IsDiscountInOrganisationAsync guard rejects a discount belonging to another org. The added code comments on nearly every one of these DTOs assert the same intent almost verbatim — e.g. CreateCustomerDto: 'the POS auto-applies it as the bill-level discount on new orders for this customer'; IsDiscountInOrganisationAsync: 'The POS auto-applies whatever discount is stored on the customer, so an id from another org would silently discount every one of this org's bills.' Per the trust-method-bodies-over-comments rule, though: every method body shown here only carries/validates/persists DiscountId on the Customer record and its DTOs. OrderCreationService.cs — the pricing engine this entry cites as its evidence that a customer's Discount is never auto-applied at order time — is NOT touched anywhere in this diff. So this diff does not yet prove the counter POS pricing pipeline actually consults customer.DiscountId when creating an order; the entry's 'Standing Discount does not apply on POS orders' edge case, its Key-concepts entry, and the Q&A/tracked-claims citing OrderCreationService.cs are not contradicted by what's shown here. But this is unmistakably the customer-data-model half of a discount-auto-apply feature landing (commit subject: 'feat(counter customers): carry the customer's assigned discount'), and it is the single biggest threat to this entry's current text of anything reviewed to date. Treat as high priority for the next pass: check for a companion OrderCreationService.cs and/or foodops.counter.app diff, and live-verify by setting a Discount on a customer via the full /customers form, then placing a POS order for that customer, to see whether the bill-level discount is now actually applied automatically at checkout. Separately, a companion fix in this range ('fix(counter customers): update never persisted — missing AsTracking') is worth logging for completeness: CounterCustomersController.UpdateCustomer's Customers query was missing .AsTracking() under this API's global NoTracking query-tracking behavior, so every field assignment on the edit path — including the new DiscountId — was silently discarded and SaveChangesAsync wrote nothing while still returning 200; .AsTracking() has now been added. This doesn't contradict anything currently documented (the entry doesn't describe edit-persistence reliability) but explains why a prior customer edit-and-save on this endpoint may silently not have stuck. No entry text changes proposed this pass since the pricing-engine behavior itself is unproven by this diff alone. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-17 — hubits-api-sales commit range f5259ce..8808c93 (prod) adds server-side enforcement of the org's Transfer (blaze) payment-reference policy. PaymentsController.Create (the admin Receive Payment path, underlying this entry's documented 'record the payment against the customer' / Outstanding-balance-lifecycle step 3) now calls a new TransferReferenceValidator.ValidateAsync before creating the payment and returns a 400 if the reference is missing when the org's 'Soft Enforce Payment Reference Number' setting is on, or already used elsewhere in the org when 'Enforce Unique Blaze Number' is on — the added code comment states this closes a gap where the admin path could previously bypass rules the counter already enforced. CounterPaymentsController also gains a new GET transfer-reference-check endpoint so the counter can pre-validate a reference while online (blocking a bad one at checkout) using the same validator, closing a cross-session/admin-created-duplicate gap the counter's offline-only view couldn't see on its own. This entry documents payment recording and payment reference-number GENERATION (outlet-aware sequencing, already logged 2026-08-02) but says nothing about reference VALIDATION/uniqueness enforcement, so nothing currently written is contradicted — this is pure addition. Worth a doc note under API notes or the Outstanding-balance-lifecycle section describing that a Transfer/bank-transfer payment can now be rejected for a missing or duplicate reference depending on org settings, plus a live-verify (attempt a Receive Payment on admin.foodops.io and a counter transfer payment with a blank or duplicate reference on an org with these settings enabled, confirm the 400/block and its message) on the next pass. The commit range's other subject, 'fix(invoices): raise invoices in the customer's currency', has no diff hunk intersecting this entry's tracked Customers/Payments controller globs in the supplied evidence, so no action is taken on it here. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-15 — foodops.counter.app commit range 3900ab4..6cadf15 (prod) changes the order-attached CustomerCombobox's keyboard selection (customer-combobox.tsx). It previously required a two-step Enter: the 1st Enter merely highlighted the first (or current) row without selecting, and a 2nd Enter was needed to confirm it — an 'armedForSelectRef' gate enforced this. That gate is removed: a SINGLE Enter now selects whichever row is highlighted (the first row by default, or the row reached via arrow keys), with the highlight still reset on query change or popover close. This entry's 'CustomerCombobox keyboard navigation' addition (logged 2026-04-25, 3eb991a) and the 2026-05-04 Playwright-verified dropdown note both describe 'type-to-search' with 'arrow-key + Enter resolution' but never documented how many Enter presses were required to confirm a selection, so nothing here is contradicted — this is additive detail, not a correction. Worth a one-line update to the 'attach a customer to an order' walkthrough (step 4, 'Select the customer') and the CustomerCombobox dropdown note clarifying that a single Enter now confirms the highlighted customer (previously two Enters were needed), plus a live-verify of the picker's Enter-to-select behavior on the next pass. The other two commits in this range ('Fix print in same tab issue in counter'; 'Fix zero qty removing from cart') don't intersect this entry's tracked Customers surface. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-06 — foodops-admin-app commit range b288f36..fef5be1 (prod) touches this entry's tracked apps/admin/src/pages/sales/CustomersPage.tsx surface with one small bug fix and one internal refactor, neither contradicting anything documented. (1) Bug fix: the debounced search box previously called an unconditional resetPage() every time its 250ms timer fired, including the timer armed on initial mount — so if an operator paged past page 1 within 250ms of landing on the Customers list, the still-pending mount timer would fire and silently snap them back to page 1. A new committedSearch ref now short-circuits resetPage() unless the debounced term actually changed, so paging is no longer reset by a no-op search-timer tick. This is a fix to the pagination behavior already logged in the 2026-07-18 verification-history entry (usePagination/usePagedRows rework) — that entry didn't claim the reset was bug-free, so nothing here is contradicted, just improved. (2) The Reference and Contact columns' responsive-hiding mechanism was refactored from literal cellClassName/headerClassName Tailwind strings ('hidden lg:flex' / 'hidden md:flex') to a declarative hideBelow: 'lg'/'md' prop — this is a mechanical internal refactor with no visible behavior change (columns still hide at the same breakpoints), so it doesn't affect this entry's column documentation. The other three commits in this range (TanStack Query caching for operational/inventory/finance reports, a test-timeout tweak) don't intersect the Customers surface. Minor and not urgent — worth a one-line mention under the admin-app pagination note next pass; no live-verify needed given the small, self-contained nature of the fix. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-02 — hubits-api-sales commit range bb69312..df4179d (prod) extends the already-logged 2026-08-02 review (bb69312..1b4f871: outlet-aware payment reference numbering) and 2026-08-01 review (bb69312..0d94f42: GetOrganisationStatements, LinkedRefunds, PaymentTypeId) — all unchanged, no new action on those. New in this wider range: CounterCustomersController.CreateCustomer's duplicate-mobile-number guard, which previously ran an AnyAsync boolean check and returned a flat 400 ('A customer with this mobile number already exists'), now runs a FirstOrDefaultAsync and returns the matched customer's Id/Name/Reference alongside the message. The added code comment explains the motivation: the counter app creates customers offline and syncs them later, and when that sync hits this duplicate-mobile rejection it needs to map its local temp customer (with an order and payment already chained behind the temp id) onto the real customer record — previously it had to re-search /customers/search, a prefix-only match on the raw stored mobile string that misses normalized-but-differently-formatted numbers (e.g. a stored '+960 777-1234' invisible to a '7771234' query), which the comment attributes to a 2026-07-24 incident where payments never reached the server and cashiers double-settled. This entry doesn't currently document duplicate-mobile validation on customer creation at all, so this is pure addition, not a contradiction — it doesn't touch Discount, credit limit, delete, POS search, TIN-snapshot, loyalty, or Walk-In/Inactive status. Worth a doc note under the customer-creation walkthrough or API notes describing that creating a customer (full form or POS quick-add) with a mobile number already used by an Active customer in the org is rejected, with the existing customer's id/name/reference now returned in the error payload, plus a live-verify (attempt to create a customer with a mobile already on an existing Active customer via both the /customers full form and the POS quick-add, confirm the 400 and its payload) on the next pass. The other commits in this range (tenancy package pin, cache-invalidation seam, SignalR routing envelope, haafai.domain dependency bump) don't intersect this entry's tracked Customers/Payments controller globs. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-02 — hubits-api-sales commit range bb69312..1b4f871 (prod) extends the already-logged 2026-08-01 review (bb69312..0d94f42: the new org-wide GetOrganisationStatements endpoint, LinkedRefunds population on payment detail, and the companion PaymentTypeId field on GetPayments/CustomerTransactionDto — all already logged, no new action on those). New in this wider range: payment reference-number generation becomes outlet-aware. PaymentsController gains a ResolvePaymentOutletIdAsync helper — most-specific wins: the client-picked OutletId if that outlet actually has outlet-sequence numbering enabled, else the register session's outlet, else the outlet of the first invoice being settled, else falls back to whatever was requested — and Create() now resolves this before building the payment, so a Receive-Payment against an outlet with unique numbering draws its reference from that outlet's own Payment sequence (e.g. PT-0090) instead of the org-wide series. CounterPaymentsController.AddPayment's reference-number generation changed the same way: it previously always called the flat org-wide SequenceNumberTypeHelper.GetAndUpdateNextNumber; it now resolves the register session's outlet first via OutletHelper.ResolveOutletIdForRegisterSessionAsync and calls a new OutletHelper.GetPaymentReferenceAsync, which falls back to the org-wide sequence when there's no session or the outlet isn't outlet-numbered. This entry does not currently document how payment reference numbers are generated (org-wide vs per-outlet) anywhere, so this is pure addition, not a contradiction — it doesn't touch customer Discount, credit limit, delete, POS search, TIN-snapshot, loyalty, Walk-In/Inactive status, or the documented Outstanding-balance lifecycle. The other commits in this range (tenancy package pin, cache-invalidation seam, SignalR routing envelope) don't intersect this entry's tracked Customers/Payments controller globs. Worth a doc note under API-notes on outlet-scoped payment reference numbering, plus a live-verify (take a counter payment on an outlet with outlet numbering enabled, and a Receive Payment on admin.foodops.io against an invoice from that outlet, and confirm the reference prefix matches the outlet's own sequence rather than the org-wide one) on the next pass. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-08-01 — hubits-api-sales commit range bb69312..0d94f42 (prod) adds three new user-facing surfaces to features this entry references only generically (the 'statements' word appears just in the counter.app-vs-admin-app architecture note, never formula- or endpoint-documented). (1) A new org-wide paged statements endpoint, GetOrganisationStatements (CounterCustomersController), replaces what the admin Customer Statements page previously built client-side by listing up to 200 customers and calling GetCustomerStatements once per customer (~201 requests per page-open, refired on every tab switch) — now one call returns a paged, joined, totalled list, scoped by 'Active'/'Overdue' customer list and optional name/fromDate filters, with KPI totals (statementCount, customerCount, balanceDue) computed across the full filtered set server-side (BalanceDue is a string column, summed in C# since SQL can't SUM it). (2) PaymentsController.MapPaymentResult's LinkedRefunds field, previously hardcoded to an empty list ('the admin Related tab never showed refunds'), is now populated from the payment's actual linked-refund chain (refund payment id, amount, reference, date, status) — a bug fix on a Related-tab feature already implied to exist but silently broken. (3) A brand-new POST /{id}/refund endpoint lets an admin refund the UNALLOCATED portion of a received payment back to the customer as cash/bank, guarded against voided payments, non-'Pay'-type payments, zero/negative amounts, over-the-unapplied-balance amounts, and locked fiscal periods — this is new capability distinct from the already-documented 'Accept payment' (collect outstanding credit) flow; it's the reverse direction (give money back), not previously described anywhere in this entry. A companion PaymentTypeId field was added to GetPayments' response and to CustomerTransactionDto so list/statement consumers can distinguish a refund row from a received payment. None of this touches customer Discount, credit limit, delete, POS search, TIN-snapshot, loyalty, or the Walk-In/Inactive-status behavior already documented — purely additive on the payments/statements surface. The other three commits in this range (tenancy package pin, cache-invalidation seam, SignalR routing envelope) don't intersect this entry's tracked Customers/Payments controller globs. Worth a doc note under API-notes on the new org-wide statements endpoint and the payment-refund capability, plus a live-verify (does admin.foodops.io's Customer Statements page now load in one request, and does a payment's Related tab show a refund after issuing one) on the next pass. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-28 — hubits-api-sales commit range 8c00ab8..9f561de (prod) adds a ContactNumber field to the payment-recording flow this entry references only generically (Outstanding-balance-lifecycle step 3, 'record the payment against the customer'). PaymentsController.Create now persists request.ContactNumber onto the new Payment row alongside ReferenceNumber/Remarks, and the amount-change Edit path (EditPaymentWithAmountChange) now accepts and inherits ContactNumber too — extending the previously-documented PaymentDate/ChartOfAccountId edit capability (2026-07-14/2026-07-17 verification-history entries) to a contact number as well. A companion commit in the same range adds server-side length validation on the new field. Purely additive — does not touch deposit-account resolution, RefreshCustomerBalance/cached-Outstanding behavior, credit limit, Discount, delete, search, or TIN-snapshot logic, so nothing currently documented is contradicted. The other two commits in this range (tenancy: keep org listing/switching/outbox alive under enforcement; a settings revert) don't intersect this entry's tracked PaymentsController/CounterCustomersController globs. Worth a one-line doc note under Outstanding-balance-lifecycle or API-notes on payments now optionally carrying a contact number; no live-verify urgency since it's a non-breaking additive field. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-23 — hubits-api-sales commit range 34ca3ff..fec6461 (prod) fixes GenerateStatement (CounterCustomersController, the API behind the EmailStatementModal/StatementPreviewModal A/R workflow this entry references but doesn't formula-document) for the 'unpaid-transactions' and 'overdue-transactions' statement filter types. Previously these netted views listed payments as separate rows and computed Opening/Balance Due from full invoiced amounts; they now drop payment rows entirely and net each invoice's own outstanding into its row (Payment Amount = amount paid so far on that invoice, Balance = its outstanding), via a new pure ComputeNettedStatementTotals helper. Opening balance now carries the prior period's OUTSTANDING (not full invoiced) total, and closing = opening + Σ current-period outstanding — so a partially-paid invoice shows its real paid/outstanding split instead of the full invoiced amount on an unpaid/overdue-filtered statement. The 'all-transactions' statement view is explicitly unchanged. Purely additive on a feature this entry already references (statement generation via EmailStatementModal/StatementPreviewModal) but has never formula-documented — does not contradict anything currently written, including the already-logged 2026-07-20 opening/closing credit-note-netting fix to the same method. Worth a doc note under Reports/API-notes on unpaid/overdue statement netting, plus a live-verify (generate an unpaid or overdue statement for a customer with a partially-paid invoice on admin.foodops.io and confirm Payment/Balance reflect the netted split) on the next pass. The other two commits in this range (haafai.domain bump for modifier recipe deduction; AWS SSM BML encryption key loader) don't intersect this entry's cited surface. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-22 — foodops-admin-app commit range 985570b..35146e3 touches this entry's tracked apps/admin/src/pages/sales/CustomersPage.tsx surface with one additive UI change: the customer name in the admin Customers list row is now rendered as a real react-router Link to /sales/customers/{id} instead of a plain <div>, with stopPropagation on its own click so the row's existing onClick handler doesn't double-navigate. Practical effect: right-click, Cmd/Ctrl-click, and middle-click on a customer's name in the list now open that customer's detail page in a new tab/context menu, which wasn't previously possible (only a full in-app left-click navigation existed). The link target is /sales/customers/{id} (detail), not /sales/customers/{id}/edit — consistent with, and not contradicting, this entry's existing 'New Customer → /sales/customers/new, row-edit → /sales/customers/:id/edit' note, which describes a separate edit affordance. Does not touch customer Discount, credit limit, delete, search, loyalty, or TIN behavior. The other two commits in this range (an 'alcyon issues' fix and Service-item/recipe-output changes) don't intersect this entry's cited surface. Minor, not urgent — worth a one-line mention under the admin-app architecture note next pass; no live-verify needed given the small, self-contained nature of the change. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-20 — haafai.domain commit range 4087060..360025e (prod) begins wiring multi-tenancy scaffolding into the domain layer: CallContext tenant accessors + ambient system scope, tenant marker interfaces + a transitive registry, dormant runtime config flags, and a fail-closed global EF query filter that is explicitly flag-gated (off by default). The only hunk intersecting this entry's tracked Customer.cs surface is the class declaration itself gaining : INullableTenantScoped plus the corresponding using Haafai.Models.Tenancy; — no method body, query filter application, or default-flag state for Customer is shown in this diff. That's too thin to prove any behavior change: it doesn't touch Discount/pricing, credit-limit checks, delete rules, POS search, loyalty, or invoice TIN snapshotting, and the filter itself is described as dormant/flag-gated rather than active. Flagging for awareness only: Customer is now tenant-scopable plumbing-wise, so if/when the global query filter flag is flipped on, customer list/search/Outstanding queries could become tenant-filtered in ways this entry doesn't currently describe (it already documents org-scoping on POS search, so the two may turn out to be the same mechanism or may not) — worth a source check on the flag's default and rollout status on the next pass, no doc edit needed now. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-20 — hubits-api-sales commit range acfa332..99e6847 (prod) extends the already-logged 2026-07-17 review (which covered acfa332..8ee2cf4: GetCustomerTransactionsList openingBalance, PaymentsController.Create's deposit-account resolution chain/400, GetPayments allocation filter — all unchanged, no new action). The NEW hunks in this wider range land in CounterCustomersController.GenerateStatement, the method behind the EmailStatementModal/StatementPreviewModal A/R workflow this entry references but doesn't formula-document: (1) the statement's opening AND closing balance previously excluded credit notes on the stated assumption that 'their AR effect is already in the received/allocation side, subtracting them again double-counts' — that assumption is now reversed with a fix that subtracts a CreditNotes-journal-type sum from both figures, on the reasoning that a credit note's approval posts Cr AR and, like a payment, must reduce the receivable; practical effect is that a fully-paid invoice plus an unallocated credit note, which previously opened/closed the statement period at 0, will now correctly show a credit balance. (2) Same-day statement rows were sorting by the full JournalDate — whose time-of-day is midnight for invoices/credit notes but a real posting time for receipts — which wrongly pushed same-day payments to the end; now orders by JournalDate.Date then CreatedDate, and CustomerTransactionDto gains a new serialized SortAt field so the on-page transaction-list preview (GetCustomerTransactionsList, already covered 2026-07-17) can match the PDF's chronological order. Neither change contradicts anything currently documented — this entry doesn't describe the statement's balance formula or row ordering today, only that EmailStatementModal/StatementPreviewModal exist for the A/R workflow — so this is pure addition. A minor code-cleanup note: the PaymentsController.Create comment claiming PaymentsHelper.Create doesn't map ChartOfAccountId was corrected as never true (Mapperly already did it) and the redundant manual-set block was deleted; no behavior change, deposit-account resolution is unchanged from the 2026-07-17 description. Worth a doc note under the Reports/API-notes section on statement opening/closing balance now including credit notes, plus a live-verify (email/preview a statement for a customer with an unallocated credit note on admin.foodops.io and confirm the balance reflects it) on the next pass. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-18 — foodops-admin-app commit range 70e66bc..31fa044 touches this entry's tracked apps/admin/src/pages/sales/CustomersPage.tsx surface, purely additive UI/UX plumbing: the Customers list's pagination was reworked from a hardcoded PAGE_SIZE=50 (page reset to 1 on every filter/search/sort change) to a shared usePagination/usePagedRows hook pair with a per-list persisted (storageKey: 'customers') page and page size, rendered via a new shared numbered Pagination component — so a page-size selector is new, admin-side-only UI not currently described in this entry. A secondary effect: row-selection (useRowSelection, feeding the bulk action bar) now scopes to only the current page's pagedItems rather than all loaded items, so 'select all' on the Customers list will select only the visible page going forward, not every matching customer. Neither change touches customer Discount, credit limit, delete, search, loyalty, or TIN behavior, nor the full-page create/edit form already documented in the 'admin-app customer form is now a full page' note — both remain accurate. The other commits in this range (organization timezone handling in expense forms, a Phase-3 pagination planning doc) don't intersect this entry's cited surface. Worth a doc note plus a live-verify (does the Customers list on admin.foodops.io show a page-size control, and does bulk select-all now scope to only the current page) on the next pass. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-17 — hubits-api-sales commit range acfa332..8ee2cf4 (prod) touches two surfaces in this entry's tracked PaymentsController/CounterCustomersController globs, both additive and not contradicting any documented claim. (1) GetCustomerTransactionsList (customer statement/transaction-list preview, feeds the EmailStatementModal/StatementPreviewModal A/R workflow referenced in this entry) now returns an openingBalance for period-scoped queries, computed via the same ChartOfAccountHelper.GetOpeningBalance the statement PDF uses — previously a fromDate-scoped preview silently started its running balance at 0, making the closing balance wrong by exactly the opening-balance amount; now correct. (2) PaymentsController.Create's deposit-account resolution, which underlies this entry's documented "record the payment against the customer" / receivables-collection flow, changed from an unconditional default to the org's "Undeposited Funds" account to a priority chain (explicit ChartOfAccountId on the payment > the payment method's mapped account > org default) via a new ResolveDepositAccountId helper, and now returns a hard 400 ("No deposit account is configured...") instead of silently proceeding when none resolve — so a receive-payment call can now fail outright in orgs with no deposit-account mapping, where before it always fell back to Undeposited Funds. The Edit paths (EditPaymentWithAmountChange, UpdateSimple) also newly accept and persist ChartOfAccountId, extending the 2026-07-14-logged PaymentDate-correction capability to let a payment's deposit account be corrected after entry (UpdateSimple's comment notes this reverses and re-posts the payment's journal). A related GetPayments change adds an orthogonal allocation-state filter (Allocated/PartiallyAllocated/Unallocated) alongside the existing type/status filter (Pay/Refund/Pending) — a payment-list-tab addition not currently described in this entry. None of these touch customer Discount, credit-limit, delete, search, loyalty, or TIN-snapshot behavior — only the payment/statement plumbing behind the already-documented "record a payment" step. Worth a doc note in the Outstanding-balance-lifecycle / API-notes sections plus a live-verify (does a receive-payment on an org with no deposit-account mapping now surface the new 400 in admin.foodops.io, and does a statement preview's opening balance look right) on the next pass. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-14 — hubits-api-sales commit range 2ccc00b..acfa332 adds a customer-detail-page fix not yet reflected in this entry: CustomersController.GetById now returns Credits = customer.Unused in its response (comment: "the list DTO already surfaces this (as UnusedCredits); the detail omitted it, so the customer header's 'Credits' tile always read 0"). This entry documents a Credits column on the /customers list (unallocated credit-note balance) but says nothing about a Credits value on the customer detail page — so this is new, money-adjacent surface: the detail page's Credits tile was silently stuck at 0 before this fix and should now show the real unused-credit balance. Worth a doc note plus a live-verify (open a customer with known credit-note balance on admin.foodops.io, confirm the detail header Credits tile is non-zero) on the next pass. Two other hunks in this range are additive and don't intersect a documented claim: PaymentsController.GetPayments now returns IsVoided per payment row (a voided-payment flag not previously surfaced in this entry's payment-history description), and payment edits (EditPaymentWithAmountChange/UpdateSimple) now accept and persist an updated PaymentDate, extending the previously-documented "record the payment against the customer" flow to allow correcting a payment's date after entry. The IdentificationNo additions in this same commit range (CounterCustomersController, CustomerSearchDto, OfflineCustomerDto) were already logged in the 2026-07-13 verification-history entry for 2ccc00b..ee41246 — no new action there. The remaining two commits in ee41246..acfa332 (haafai.domain package bump to 1.0.998; Hangfire log-level change) don't touch this entry's cited surface. Not yet reflected in the Plain-English summary, field table, or API notes — flag for the next live-verify pass. (auto-applied by the truth pipeline; adversarially gated) (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-13 — hubits-api-sales commit range 2ccc00b..ee41246 ("feat(customers): add IdentificationNo to customer DTOs for enhanced searchability") adds IdentificationNo to CustomerSearchDto (both SearchCustomersInternal result-mapping sites in CounterCustomersController.cs, lines ~531 and ~603) and to OfflineCustomerDto (LoadOfflineCachePageAsync). This is the backend counterpart to the counter.app Reg: {identificationNo} dropdown line already logged in this entry's verification history (foodops.counter.app 5d29831..2ad5cdf) — it's what actually puts registration number on the wire for CustomerCombobox to render, both for live search results and, newly, for the offline customer cache. Practical effect: registration-number matching/display in the order-attached customer picker should now also work while the counter app is offline, which wasn't previously confirmed. Purely additive — does not contradict the existing search-predicate claim (Name/IdentificationNo/Mobile/Phone/Reference, no email, ≥2 chars) since IdentificationNo was already matched server-side; this diff only adds it to the returned DTO shape. The other two commits in this range (invoice-query NRE fix in credit-note allocation; credit-note soft-delete tracking fix) don't intersect this entry's cited surface. Not yet reflected in the CustomerCombobox description, the API notes section, or a fresh screenshot — worth a doc note (and offline-mode verification) on the next live-verify pass. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-13 — foodops.counter.app commit range 5d29831..2ad5cdf ("add identification number to customer records and enhance search functionality"; "update CustomerCombobox to display registration number and mobile number correctly") adds a Registration Number line to the order-attached customer picker (customer-combobox.tsx): when customer.identificationNo is set, a Reg: {identificationNo} label now renders directly under the customer's name in each dropdown row, alongside the existing Mobile line. This is additive to the already-documented lastUsedAddress row (added 9e5f9ab9/dbf805db) and to the already-documented search predicate (Name/IdentificationNo/Mobile/Phone/Reference) — it does not contradict any existing claim, it just makes registration-number matches visually identifiable in the results. Not yet reflected in the CustomerCombobox description or screenshot 163; worth a doc note + a fresh screenshot capture next live-verify pass. Diff-only this pass — the paired 'test: cover pending-local identificationNo search branch' and 'fix: fall back to default when a category image URL 404s' commits in the same range don't intersect this entry's cited surface. (auto-applied by the truth pipeline; adversarially gated)

  • 2026-07-04 — admin-side receive-payment cache-refresh gap CLOSED (codebase, hubits-api-sales 30a126a..a4c6ced). The gap flagged in the 2026-06-29 pass (code-audit R1) — that PaymentsController.Create does not refresh the customer's persisted Outstanding column after an admin-side receive-payment — is fixed: Create now calls RefreshCustomerBalance(newPayment) immediately after recording the payment, and the amount-change Edit path was given the same call. The shared RefreshCustomerBalance helper was also changed to load the customer via dbContext.Customers.AsTracking() instead of the prior CustomerHelper.GetCustomer path, which under the API's global NoTracking query behavior would not have persisted UpdateAccountBalance mutations via SaveChangesAsync — so the refresh now reliably persists for every caller (Create, Edit, Void, ChangeStatus). Corrected the 'Customer appears on Overdue tab despite being paid' edge case's Cause and the kb-claim:overdue-despite-paid-mixed-cache HTML annotation to reflect the fix. Money-sensitive; recommend a live re-verify once an admin-side receive-payment can be exercised end-to-end against admin.foodops.io. (auto-applied by the truth pipeline; adversarially gated)

Note: Code + Playwright verified 2026-05-04 · codebase + live E2E (partial) Verified_by: playwright · Last verified: 2026-05-04 Playwright pass (2026-05-04) confirmed: CustomerCombobox opens with type-to-search select2 dropdown; Walk In Customer (WAL001) is first result; "+ New Customer" shortcut at bottom (screenshot 163). NewCustomerModal is a right-side panel slide-in titled "Add Customer"; required fields are Default Currency and Mobile only; Delivery Address section fully optional (screenshot 164). Offline-sync banner not verifiable (no offline-created customers in E2E org). Code-verified claims unchanged (curated globs confirmed as of 2026-05-04). Status remains needs-review — offline-sync banner not captured.

Note: 2026-06-04 — Content re-verify (CHANGED) · codebase Verified_by: codebase · Last verified: 2026-06-04 Re-verified against foodops.counter.app@06d4e4c (main) and hubits-api-sales@720407c (main). The brief's driver 28823e2 (2026-05-15) is actually a Quotes/Sales-Orders comment+status change, not a customer-CRUD change. Real customer-facing changes since 2026-05-04: (1) Loyalty tab added to customer detail (points/amount/enrolled-on, a13f952c) — corrected the "loyalty not built-in" Q&A from a flat "no" to "partial: read view of enrolment + points". (2) Credit-payment collection ("Accept payment" button on detail page + POS toolbar, 1d5ae62f) — added. (3) Import/Export removed from the in-POS Customers list (abfd5b70 2026-05-14) — corrected the bulk-import Q&A and the CSV-export API note (import/export is now admin-side only; legacy monolith still has the Imports views). (4) CustomerCombobox shows last-used delivery address, search API returns LastUsedAddress, primary-address demote bugs fixed (9e5f9ab9/dbf805db). (5) Mobile carried through modal-created customers + receipts (06d4e4c0). Existing 2026-05-04 Playwright findings (NewCustomerModal optional address, combobox dropdown) unchanged. Legacy monolith customer views still dormant. NOT re-verified live this pass — Loyalty tab + Accept-payment modal marked UI-TODO.

Warning: 2026-06-14 — Truth audit (CORRECTIONS) · codebase Verified_by: codebase · Last verified: 2026-06-14 Code-truth audit against foodops.counter.app@main and hubits-api-sales@main (prod branches per _meta/repo-prod-branches.md). Fixed the same failure class as the customers.md "search by phone" incident — plausible-sounding behaviors the code does not do:

  1. Standing customer Discount does NOT auto-apply on POS orders (was: "automatically applied to every order"). OrderCreationService + InvoiceHelper apply only order/line/loyalty DiscountId; the customer's own DiscountId is never read at pricing. Corrected the field table, attach-to-order step 5, Key concepts, the discount Q&A, the "auto-discount not applying" edge case, and the API note.
  2. Credit limit is a HARD BLOCK on credit sales, not a soft warning with manager override (was: "triggers a warning but doesn't block" / "cashier can override with manager approval"). OrderDetailsPanel.tsx:1472-1484 and PaymentSettlementModal.tsx:184-199 do if (!check.isAllowed) { toast.error('Credit Limit Exceeded'); return; } — no override path. Only credit sales are checked; cash/card never blocked. Corrected Key concepts, credit-limit Q&A, and the credit-limit edge case.
  3. Delete is real (soft-delete) and conditionally allowed, not "soft-delete only / can only mark inactive". CustomersController.Delete (line 391-423) sets RecordStatus.Deleted when the customer has no transactions and zero Outstanding; otherwise returns 400 "Mark the customer as Inactive instead." Corrected the delete Q&A.
  4. Two distinct customer forms: full CustomerFormDialog (/customers page — all fields incl. Discount/Credit Status/Billing Address) vs lighter NewCustomerModal (POS quick-add — Mobile/Name/Email/TIN + Delivery Address only). The KB form table is the full form; added a note + corrected the implication that the POS "Add New" opens the full form. Added "Credit Status" (required) to the table.
  5. POS search does NOT match email — the API note previously said "supports name and email"; the predicate matches Name/IdentificationNo/Mobile/Phone/Reference only (already correct in the edge case; fixed the contradicting API note).
  6. "Overdue despite paid" cause flagged UNVERIFIED — claimed "materialized view cache lag" but Outstanding is computed live (invoices − payments − credit notes); the transient-stale is client-side cache. Left as flagged HTML comment. Confirmed-true + cited: Walk-In is un-editable (CounterCustomersController.cs:1087); Inactive flip via PUT /v1/customers/{id}/status + search filters Active-only; invoice TIN is snapshotted on the Invoice entity (Invoice.TINNumber); search predicate (already cited). NOTE: abfd5b70 removed Import/Export from the CustomersPage UI menu only — the /v1/customers/import + /export endpoints and customerService.import/export methods are still live; the 2026-06-04 note's "admin-side only" framing overstates the removal. NOT re-verified live this pass (code-only).

Warning: 2026-06-15 — Adversarial re-verify of the 2026-06-14 corrections · codebase Verified_by: codebase · Last verified: 2026-06-15 Re-checked every 2026-06-14 correction against prod (foodops.counter.app@main, hubits-api-sales@main, haafai.domain@staging — staging IS the prod branch for the domain pkg). All six corrections hold on the substance. Two tightenings applied (overcorrected detail, not wrong direction):

  1. Inactive-status menu labels — the 2026-06-14 Q&A said the menu reads "Set Active / Set Inactive". Actual labels in CustomerTable.tsx:219-233 are "Activate" / "Deactivate". Tightened the wording + re-cited to CustomerTable (the status flip still goes through customerService.changeStatusPUT /v1/customers/{id}/status, confirmed).
  2. Delete-Q&A error message — the 2026-06-14 Q&A quoted only the transactions message ("…has transactions and cannot be deleted…") while asserting the rule covers both transactions AND a non-zero balance. The balance branch returns a distinct message ("Customer has an outstanding balance and cannot be deleted. Settle the balance first, or mark the customer as Inactive." — CustomersController.cs:403-406); the transactions branch is :409-416; soft-delete sets RecordStatus.Deleted at :419. Q&A now quotes both messages with line-accurate citations. Strongly re-confirmed (read the code, REFUTE bias, no refutation found): customer Discount never read at pricing — OrderCreationService loads the customer with only .Include(CustomerLoyaltyPrograms) (no Discount include) and a repo-wide code search for customer.DiscountId returns exactly one hit (CustomersController CRUD), zero in any order/invoice/pricing path; Counter/Invoices/InvoiceHelper.cs applies only viewModel/loyalty DiscountId. Credit limit is a hard block with NO override path — OrderDetailsPanel.tsx:1472-1481 and PaymentSettlementModal.tsx:181-194 both do if (!isAllowed) { toast.error('Credit Limit Exceeded'); return; } and the check fires only for Credit/CreditAndClose submission types; server CheckCreditLimit at CounterCustomersController.cs:374-397 (creditLimit <= 0 → unlimited; currentOutstanding + amount > creditLimit → blocked; currentOutstanding = totalInvoices − totalPayments − totalCreditNotes). Two-forms split, field table (IdentificationNo / Mobile-required / Credit-Status-required), POS search predicate (no email, ≥2 chars), Invoice TIN snapshot (Invoice.cs carries own TINNumber/Phone/Mobile/Email + GeneratedDocumentContent) — all verified field-by-field. The "Overdue despite paid" UNVERIFIED hedge is the correct call: no server-side materialized view exists; Outstanding is computed live. NOT re-verified live this pass (code-only).

Warning: 2026-06-29 — NEW-stack re-verification for July-10 launch (legacy monolith retired) · codebase Verified_by: codebase · Last verified: 2026-06-29 Re-pinned to prod HEADs: foodops.counter.app@32669a9, hubits-api-sales@19190b9, foodops-admin-app@f6ffd0b, haafai.domain@e1389444 (staging). Dropped the retired haafai/haafai.app.foodops from source_repos/verified_against_commit and re-pointed the customer-admin surface to the admin app (admin.foodops.io / foodops-admin-app) + sales API. MONEY-SENSITIVE refinement — Customer.Outstanding is a persisted ("cached") column, not purely computed-live. Confirmed: Customer.Outstanding is an EF-mapped column (haafai.domain Customer.cs:259 + builder.Property:536) refreshed by Customer.UpdateAccountBalance (:480). The /customers list + Overdue tab READ this cached column (CounterCustomersController.cs:223,533,604); the credit-limit check recomputes live (CheckCreditLimit:357-374, does not read the cached column). The counter credit-collection settle path refreshes the cached column (CounterPaymentsController.AddPayment:882). Corrected the "overdue despite paid" cause to reflect both layers, and FLAGGED that the admin sales-api PaymentsController.Create (receive-payment) does NOT call RefreshCustomerBalance (only Void:793 + ChangeStatus:849 do) → an admin-side receive can leave the cached Outstanding stale (code-audit R1). Re-confirmed still-true: credit limit is a HARD BLOCK on credit sales with no override (OrderDetailsPanel.tsx/PaymentSettlementModal.tsx; server CheckCreditLimit creditLimit<=0→unlimited); customer Discount never read at pricing; Walk-In un-editable; soft-delete rules; POS search predicate (no email, ≥2 chars). NOT re-verified live this pass (code-only). FLAGGED for orchestrator re-check (customer-outstanding refresh, money-sensitive).

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.