# Business rules — sandwichbaronstore.co.za

Evidence date: 2026-10-04. Source: `master` at `c19a5e7` unless a rule says `dev` or `qa`.

Rules are taken from code and from the refund page. Nothing in this file was invented to fill a gap. Where the code implies a rule that nobody wrote down, the rule is marked **Inferred**.

Numeric status ids below are the ids written in comments or in SQL literals. The running code looks up most statuses by **name** in `tOrderStatus`. If a database row was renumbered, the name wins.

## Identity and access

### BR-001 — Customer password storage

**Inferred** from `_framework/_nemo.password.cls.php` and `login.php`.

- New passwords go through `NemoPassword` (salt plus hash). The class comment describes the hash as SHA-256 of salt plus password.
- If `sysUser.strPasswordMD5` is already set, login uses that legacy field instead of `NemoPassword`.
- The class's own install SQL seeds: minimum length 8 (`PASSWORD_Strength_Check_MinLength`), history of 8 (`PASSWORD_MaxHistory`), restricted words `password|123`, reset-link lifetime `60 MINUTE`. Those seeds apply when the class creates the settings rows. Live values can be edited in the database. **Unknown / Requires Confirmation** for each environment.
- The class also contains expiry and "already used" messages (`PASSWORD_Expired`, `PASSWORD_Already_Used`).

### BR-002 — Who may sign in as a customer

**Implemented.** `login.php`.

The user row must join an active security group (`sysSecurityGroup.blnActive = 1`) and the user must be active (`blnActive = 1`).

### BR-003 — Branch owners are not store customers

**Implemented.** `login.php`.

A match on `OLOowner.kpoloUserName` and `kpoloPassword` never creates a customer session. The browser is redirected to `webadmin/login.php` with MD5(username) and MD5(password) on the query string.

### BR-004 — Web-admin authority is the security group

**Implemented** as machinery. `webadmin.security.group.php`, `customer.security.group.php`, Nemo menu.

Which group can open which screen is stored in the database. The group names and grants are **Unknown / Requires Confirmation**.

### BR-005 — Branch API authority

**Implemented.** `API/api.php`, `API/testorder.php`.

The caller must pass a `branchid` that exists on `OLObranch` and a `user` / `password` that match that branch's `OLOowner` Kpolo username and password. There is no second factor and the password travels on the query string.

## Catalogue and what can be ordered

### BR-006 — Suburb chooses the branch

**Implemented.** `oloSuburb.branchId`, `getUserBranch`.

The customer orders from the branch linked to their address suburb.

### BR-007 — Placeholder and deleted suburbs are not orderable

**Implemented.** `client.menu.php`, `client.register.php`.

- Suburb id `2356828000001` is labelled in a comment as no-suburb and is excluded from the registration list. The menu sends that customer to the profile page.
- A missing address, or a suburb whose `OLOobject` is `deleted` or `deletionPending`, also sends the customer to the profile page.

### BR-008 — A closed branch does not take net orders

**Implemented.** `client.menu.php`.

If `OLObranch.acceptNetOrders` is `0` or `isClosed` is `1`, the customer is sent to the profile page.

### BR-009 — A silent branch is offline

**Implemented.** `client.menu.php`, `oloRuleProvider.branchOfflineTimeout`.

If the minutes since `OLObranch.dtLastCheckin` exceed `branchOfflineTimeout`, the customer is sent to `branchOffline.php` and told the order cannot be placed. The page shows the branch telephone from the customer's branch.

The code comment swaps the meaning of the flag. The redirect runs when the check-in is stale. That is the rule.

On `master` and `dev`, a successful API poll sets `dtLastCheckin` to `Now()`. On `qa`, the poll sets it to `Now()` plus 2 hours, so a `qa` branch looks freshly checked-in for two hours longer than wall time. **Implemented** on that branch only.

Import code can write `dtLastCheckin` to `2000-04-01 00:00:00` (`webadmin/cron.daily.importStagingOLO.php`). **Inferred:** after that write the branch fails BR-009 until it polls again.

### BR-010 — Happy hour

**Implemented.** `checkHappyHour`.

An item tied to a happy-hour group can be selected only when today matches `OLOhappyHour.dayOfWeek` and the current time is inside `startTime` and `endTime`, and the happy-hour object is not deleted or pending delete. Otherwise the customer is shown the configured days and times. A window of 00:00 to 23:59 is described as the whole day.

`dayOfWeek` values in this function use 1 = Monday through 6 = Saturday. Sunday's case was not in the portion read past Saturday. **Unknown / Requires Confirmation** whether Sunday is case `0` or `7`.

### BR-011 — Modifier groups

**Implemented.** `ajaxfunctions.php` `getMessageGroup*`.

A menu item can require compulsory modifiers, additional compulsory modifiers, add-ons, exclusions, final-prep choices, and a quantity. Group ids that are compulsory for an order come from `oloRuleProvider.compulsoryOrderSalesAGroups`.

### BR-012 — Future-order items

**Implemented.** `checkFutureOrderable`, `oloRuleProvider.futureOrderSalesAGroups`.

Whether the basket may be a future order depends on those sales groups. The group names are database content.

### BR-013 — Minimum order

**Implemented** on `master` and `qa`. **Changed on `dev`.**

`getMinimumOrderValues`:

- Read `oloRuleProvider.minimumOrderValues`.
- Read `oloSuburb.minOrderValue` for the customer's suburb and province.
- On `master`: minimum = provider value + suburb value.
- On `dev` (`56891d5`, 2020-11-23): if the suburb value is not 0, the minimum is the suburb value; otherwise it is the provider value. They are not added.
- Two hardcoded customer user ids always receive minimum `0`. The source comment names those accounts. The ids are not repeated here.

The checkout button passes this number into `jsCheckoutOrder`. Provenance of the button: **Implemented**. The JavaScript that compares the basket to that number lives with the page script and was not quoted line by line. **Inferred:** the button is how the minimum is applied. A server-side reject of an under-minimum `ajaxPlaceOrder` was not found.

### BR-014 — Prices and delivery charge id

**Implemented.**

Line prices are the `dbPrice` values on the basket objects, multiplied by the master item quantity. `tOrder.dbDeliveryCharge` is inserted as `0`. The POS XML `<DeliveryCharge>` is `oloRuleProvider.defaultDeliveryChargeId` (a PLU id, not a rand amount). **Inferred:** the POS, not this site, turns that id into money. **Unknown / Requires Confirmation.**

Contactless help text says delivery is free. The site does not apply a different delivery charge for contactless. **Documented** on the control. **Inferred** that "free" is a message to the customer, not a separate price rule in this PHP.

## When an order may be placed

### BR-015 — Business hours and public holidays

**Implemented.** `ajaxPlaceOrder`, `API/api.php`, `getBranchHours`.

Hours come from `OLObranchHours`: `dayOfWeek`, `dayOfWeekEnabled = 1`, opening time, closing time.

`dayOfWeek` for this check is PHP `date('w')` (0 = Sunday through 6 = Saturday), except that a `publicHoliday` row whose month and day match today forces `dayOfWeek = -1`.

Cash (and a successful card or EFT) inside those hours is status `Queued`. The same payments outside those hours are status `OutsideBusinessHours`.

The API will release `OutsideBusinessHours` orders only while the branch is currently inside hours. Otherwise it releases `Queued` orders. One order per poll. That order becomes `InProcess`.

### BR-016 — Delivery methods

**Implemented.** `client.order.confirm.php`.

| Customer choice | POS `SalesModeId` | Note prefix |
|---|---|---|
| Delivery | 4 | none |
| Collect | 5 | `THIS IS A COLLECT ORDER` |
| Contactless | 4 (delivery remains selected) | `THIS IS A CONTACTLESS DELIVERY` |

Contactless is in the same radio group as "As soon as possible" and "Future Delivery". Choosing it is not a third sales mode.

Future delivery sends `<DeliveryTime>` as the chosen date and time with a `T` between them. ASAP sends no `DeliveryTime`.

Collect hides the ASAP / future / contactless block (`hideDeliverMethod`).

### BR-017 — Payment method availability

**Implemented.**

| Method | Rule |
|---|---|
| Cash | Offered only when `OLObranch.kpoloEnableCash = 1`. Inserts `tOrderPayment` type id `1` |
| Card | Always shown on checkout. NuPay fields that contain `?` are treated as unset |
| EFT | Offered only when `branchFastPay.merchantId` and `merchantKey` are both non-empty and not `?`. Inserts type id `3` |

### BR-018 — An unpaid card or EFT order is not queued

**Implemented.** `ajaxPlaceOrder`, `paymentEFTNotify.php`, `ajaxUpdateOrderStatusCC`.

Card and EFT insert status `NotPaid`. The EFT notify updates the status only when it is still `-1`. Commit `732b5b9` states the same rule for the card path: set Queued only from NotPaid.

Cash does not wait for a gateway. It is queued or marked outside hours immediately, and `OrderCustomerAccept` is sent. Card and EFT do not send that mail at insert.

### BR-019 — Driver tip

**Implemented.**

Tips are the percentages on `oloDriverTip`. The chosen fraction is stored on `tOrder.dbDriverTipPerc`. NuPay is charged `(food + tip) * 100` cents. FastPay is charged food + tip in rand. The tip element in the POS XML is the rand tip amount, not the percentage. A comment shows an older percentage format; the active line uses the rand amount.

### BR-020 — Cash change note

**Implemented.**

If the customer enters a "Need Change" amount that is not equal to the order total, the note `Need Change for R{amount}` is appended to the POS printable notes.

## Order status

### BR-021 — Status names the POS may send

**Implemented** in the `switch` in `API/api.php`. A status is written only when it differs from the current status. Mail is sent only on that change.

| Incoming name | Customer mail | Business mail |
|---|---|---|
| `Acknowledge` | `OrderCustomerAcknowledge` | `Order Test Success` only when the order number is not greater than 0 |
| `Reject` | `OrderCustomerReject` | `OrderBusinessReject` |
| `NoResponse` | `OrderCustomerNoResponse` | `OrderBusinessNoResponse` |
| `Success` | `OrderCustomerSuccess` | none |
| `Aborted`, `Fail` | `OrderCustomerFail` | `OrderBusinessFail` (or `Order Test Failed` when the order number is not greater than 0) |
| `InProcess`, `Queued` | none in this switch (the cases are commented) | none |
| `Dispatch` | `Dispatch Notification` | none |

`Dispatch` is **not** on `master`. It is **Implemented** on `dev` and `qa`.

The sample XML in a comment spells the status `Acknoledge`. The `switch` does not match that spelling. **Inferred:** a POS that sends the misspelled value will not take the acknowledge branch.

### BR-022 — Timeout is a fail

**Implemented.** `cron.daily.orderTimeoutCustomer.php`.

- A `Queued` order whose `dDateModified` is older than `oloRuleProvider.orderFailTimeout` minutes becomes `Fail`, reason `ASAP Order Timed Out!`.
- An `InProcess` order uses `sysSettings.InProcessOrderTimeOutPeriod` the same way, with the same reason text.
- Customer template `OrderCustomerFail`. Business template `OrderBusinessFail`, with a message that says the queued or in-process ASAP order timed out.

`OutsideBusinessHours` orders are not selected by this cron. **Implemented** as an omission. **Inferred:** a future order waiting for the branch to open is not failed by this job.

### BR-023 — Cancelled payment

**Inferred** from commit `732b5b9` ("Added OrderStatus -3 for payments canceled").

Status id `-3` means a cancelled payment in that commit message. The status name in `tOrderStatus` was not found in the PHP that was read. **Unknown / Requires Confirmation.**

## Communication and policy

### BR-024 — Session length

**Implemented.**

Customer idle timeout is `oloRuleProvider.consumerSessionTimeout` minutes (`CheckTimeout` multiplies by 60 and compares with `$_SESSION['timestamp']`). PHP also sets `session.gc_maxlifetime` to 7200 seconds. The cron deletes `tAuthUser` rows older than one day and session files older than two days.

### BR-025 — Refund policy

**Documented** on `refund.php`. Not implemented as an order action.

- The customer must be unsatisfied and the business must have been unable to put it right.
- The request must be in writing: post, email, or fax. Phone is not accepted.
- The refund is the exact amount paid, within 30 days of an authenticated request, back to the card used to pay.
- The business may change the policy by posting it on the website.
- The page says it was last updated on 3 November 2009.

Cash and EFT are not mentioned. **Unknown / Requires Confirmation** how a cash or EFT refund is done.

### BR-026 — Email identity

**Implemented** as configuration.

`oloRuleProvider.emailFromName` is the from-name. `oloRuleProvider.orderFailNotificationEmail` is read by `getOrderFailNotificationEmail`. Which messages use that address was not traced through every template. **Unknown / Requires Confirmation** for the recipient list of each template.

## Data rules that affect behaviour

### BR-027 — Two databases

**Implemented.**

The storefront uses the customer database. Web admin uses a web-admin database and the customer database. Import copies staging from web admin into the customer database and will not start a ZIP import while `sysSettings.Maintenance` is `1` or `ImportedZIP` is `0`.

### BR-028 — Public-holiday import

**Implemented** on `dev` only (`254187a`).

The public-holiday import must ignore deleted entries. `master` and `qa` do not contain that fix. **Inferred:** on those branches a deleted holiday can still be imported.

### BR-029 — Time zone

**Implemented.** Both `system.php` files set `Africa/Johannesburg`.

A commented query mentions `Africa/Harare`. It is not the active path.

## Explicit non-rules

- Copy files (`*Copy*`, `*.bkp.php`) are not extra business rules.
- Comments that describe a test NuPay merchant are not the active gateway rule. The active rule is the branch's `OLObranchNuPay` row plus the success and failure URLs in system settings.
- There is no evidence in code of a loyalty programme, a coupon engine, or a multi-brand store. Do not add those as rules.
