# PRD — sandwichbaronstore.co.za

Evidence date: 2026-10-04. Source: `git@bitbucket.org:overdrive/sandwichbaronstore.co.za.git`.

There is no prior PRD in the repository or in Silicon Ark. Requirements below were recovered from the code. Provenance is marked on each item.

**Branch rule used in this PRD.** Behaviour is from `master` at `c19a5e7` (2020-06-25) unless a requirement says otherwise. `origin/dev` at `254187a` (2021-08-24) and `origin/qa` at `2ea1d4d` (2020-09-17) differ in the places called out below. Which branch customers hit in production is **Unknown / Requires Confirmation**.

## Product purpose

A Sandwich Baron customer orders food from the branch that serves their suburb. The site builds a POS order (XML), queues it for the branch, and tells the customer by email when the branch accepts, rejects, completes, or fails the order.

Provenance: **Implemented**. The brand name on the refund page is SANDWICH BARON (`refund.php`).

## Users and roles

| Role | What they do | Evidence | Provenance |
|---|---|---|---|
| Guest | Can open the menu when the session user id is `0`. Offline check is skipped for that id | `client.menu.php` | Implemented. How a visitor reaches user id `0` is **Inferred** as a view-menu path |
| Customer | Registers, signs in, maintains profile and address, builds a basket, checks out, pays | `client.register.php`, `login.php`, `client.profile.php`, `client.menu.php`, `client.order.confirm.php` | Implemented |
| Branch owner | Signs in on the customer login with `OLOowner` Kpolo username and password and is redirected to web admin | `login.php` | Implemented |
| Web-admin user | Signs in under `webadmin/login.php`. Menu items depend on security group | `webadmin/login.php`, `webadmin.security.group.php`, `customer.security.group.php` | Implemented. The exact menu rows are in the database and were not readable from Git |
| Branch POS | Polls `API/api.php` with branch id and owner credentials, posts status XML, receives the next order | `API/api.php` | Implemented |
| Cron | Imports catalogue ZIP files and fails timed-out orders | `webadmin/cron.daily.*.php`, `cron.daily.orderTimeoutCustomer.php` | Implemented. The scheduler that calls these URLs is not in the repository |

## Main workflows

### W1 — Register

Trigger: visitor submits the registration form on `client.register.php`.

Compulsory inputs checked in the page script (`ValidateCompulsory`):

- First name, surname.
- Email matching the page's expression.
- Street address, province not `0`, suburb not `0`.
- Cell number, required, matched against `^\(?([0-9]{3})\)?[-. ]?([0-9]{3})[-. ]?([0-9]{4})$`.
- Home and work phones are optional; if present they must match the same pattern.
- Username.
- Security question, one of three hardcoded questions (first pet, mother's maiden name, favourite colour), plus an answer.
- Password and confirmation must match. Strength rules come from `NemoPassword` (see business rules).

The script then calls `validateInUseRegister` so the username, email, and cell are not already in use (`ajaxfunctions.php`).

Provenance: **Implemented**. Server-side checks inside `AddClientDetails` were not line-reviewed in full. Whether the server repeats every browser check is **Unknown / Requires Confirmation**.

Suburb `2356828000001` is excluded from the registration suburb list (`client.register.php`). The menu treats that id, and an empty suburb, as "no suburb" and sends the customer to the profile page.

### W2 — Sign in and session

Trigger: `login.php` with `Action=Login`.

1. If the username and password match `OLOowner.kpoloUserName` / `kpoloPassword`, the browser is sent to `webadmin/login.php` with MD5 of the username and password on the query string.
2. Otherwise the customer is looked up on `sysUser` by email. A value that is not an email is resolved through `sysUser.strUser` to an email. The security group must be active (`blnActive = 1`) and the user must be active.
3. If `strPasswordMD5` is empty, the password is checked with `NemoPassword` (salted hash). If `strPasswordMD5` is set, that legacy path is used. The MD5 comparison itself was not fully traced. Provenance of the branch: **Implemented**. Exact legacy compare: **Requires Confirmation** if a tester needs the byte-level rule.

Session lifetime for a customer is `oloRuleProvider.consumerSessionTimeout` minutes, converted to seconds in `CheckTimeout()`. PHP `session.gc_maxlifetime` is also set to 7200 seconds. Time zone is `Africa/Johannesburg`.

On timeout the customer is sent to `client.logoff.php`.

Password reset links expire after the `PASSWORD_Reset_Interval` seeded by `NemoPassword` (`60 MINUTE` in the class's install SQL). Provenance: **Implemented** as the value the class writes when it creates its settings. Whether that row still has `60 MINUTE` in each database is **Unknown / Requires Confirmation**.

### W3 — Build a basket

Trigger: signed-in customer opens `client.menu.php`. `index.php` redirects there.

Before the menu is shown the page redirects to `client.profile.php` when any of these are true:

- Suburb id is `2356828000001` or blank, or there is no address row.
- Branch `acceptNetOrders` is `0` or `isClosed` is `1`.
- The suburb's `OLOobject` row is `deleted` or `deletionPending`.
- Minutes since `OLObranch.dtLastCheckin` are greater than `oloRuleProvider.branchOfflineTimeout`. That path goes to `branchOffline.php` ("The branch is currently offline") instead of the profile page.

The variable in code is named `$BranchOnline` and the comment says `0 = Offline; 1 = Online`, but the value is set to `1` when the check-in is **older** than the timeout, and that is the value that redirects to the offline page. Provenance: **Implemented**. Treat the redirect, not the comment, as the behaviour.

Menu rendering (`includes/client.menu.cls.php`):

- Items come from OLO menu sheet tables.
- Happy-hour items are offered only when `OLOhappyHour` matches today's `dayOfWeek` and the current time is inside `startTime` / `endTime`. Otherwise the customer is told the days and times.
- A basket line is a master item plus modifier groups: compulsory, additional compulsory, add-on, exclusion, final prep, and order quantity (`ajaxfunctions.php` `getMessageGroup*`).
- Previous orders can be listed (`renderPreviousOrdersMenuList`).
- Checkout passes the minimum order amount from `getMinimumOrderValues` into `jsCheckoutOrder`.

Future-order eligibility uses `oloRuleProvider.futureOrderSalesAGroups` (`checkFutureOrderable`). Compulsory groups use `compulsoryOrderSalesAGroups`. Provenance: **Implemented**. The allowed group names are database values and are not in Git.

### W4 — Checkout: method, time, payment

Trigger: customer opens `client.order.confirm.php`.

Step 1 — delivery details:

| Control | Behaviour | Provenance |
|---|---|---|
| Delivery (default) | `SalesModeId` 4 in the POS XML | Implemented |
| Collect | Hides the delivery-type block. `SalesModeId` 5. Printable note is prefixed with `THIS IS A COLLECT ORDER` | Implemented |
| As soon as possible (default when Delivery is selected) | No `DeliveryTime` element | Implemented |
| Future Delivery | Shows date and time. XML includes `<DeliveryTime>` | Implemented |
| Contactless Delivery | Same radio group as ASAP and Future, so it clears those. It does **not** change `SalesModeId` (delivery stays 4). Note is prefixed with `THIS IS A CONTACTLESS DELIVERY`. Help text says the driver stands away while the customer takes the order, and that this delivery is free | Implemented UI. "Free" is copy on the control. The order row inserts `dbDeliveryCharge` as `0` for every order, and the XML still sends `oloRuleProvider.defaultDeliveryChargeId` as `<DeliveryCharge>`. Whether the POS charges for contactless is **Unknown / Requires Confirmation** |

Notes are a textarea. Cash "Need Change for R …" is appended to the printable notes when the amount differs from the total.

Step 2 — payment, per branch:

| Method | Shown when | Order status at insert | Provenance |
|---|---|---|---|
| Cash (`COD`) | `OLObranch.kpoloEnableCash = 1` | `Queued`, or `OutsideBusinessHours` when the branch is outside hours | Implemented |
| Credit Card | Always rendered on this page | `NotPaid` until the card return page | Implemented |
| EFT | `branchFastPay` has a merchant id and key that are not blank and not `?` | `NotPaid` until the notify page | Implemented |

Card and EFT are treated as paid (`isPaid=true`) in the XML only after the gateway succeeds. Cash inserts `tOrderPayment` with payment type id `1` and `tPaymentLog`. EFT inserts payment type id `3`. The credit-card insert on the success page uses the gateway return fields; a comment in `ajaxPlaceOrder` shows type id `2` for card. Provenance of ids `1` and `3`: **Implemented** in the INSERT. Provenance of id `2`: **Inferred** from a comment.

Driver tip options are loaded from `oloDriverTip`. The tip UI is in the credit-card block. The selected percentage is stored on `tOrder.dbDriverTipPerc` and added to the NuPay and FastPay amounts. Provenance: **Implemented**.

A comment dated 22/08/2018 says cash for future orders was enabled on live. The current cash path does not reject a future delivery date. Provenance: **Inferred** from the absence of a block, plus that comment.

### W5 — Card payment (NuPay)

Trigger: customer places a credit-card order.

1. `ajaxPlaceOrder` inserts the order as `NotPaid` and does not send the acceptance email yet.
2. `getPaymentGateway` reads `OLObranchNuPay` for the branch (merchant id, client reference, username, account id, access key). Rows whose key fields contain `?` are ignored.
3. Amount is basket total plus tip, in cents. Access key sent to NuPay is `md5(accessKey + Y/m/d + generatedId)`. `generatedId` is `SB-{orderId}-{uniqid}`.
4. Success and failure URLs come from system settings `PaymentGatewaySuccessURL` and `PaymentGatewayFailedURL` (database `sysSettings`, loaded in `system.php`).
5. On success, `paymentSuccess.php` calls `ajaxUpdateOrderStatusCC`, which moves the order to `Queued` or `OutsideBusinessHours` and the XML is generated.

Merchant id `10000` uses `clientReference` as the NuPay reference; any other merchant id uses `merchantUsername`. Provenance: **Implemented**.

The code comments record a period on the test portal and a later switch to the live portal (`15af9e3`, `845f6e5`). The active function uses the branch's stored NuPay row and the system-setting URLs, not a hard-coded test merchant. Provenance: **Implemented**.

### W6 — EFT payment (FastPay)

Trigger: customer places an EFT order.

`getEFTPaymentGateway` reads `branchFastPay.merchantId` and `merchantKey`. Amount is rand, two decimals, including tip. Return URLs are system settings `EFTPaymentGatewaySuccessURL`, `EFTPaymentGatewayFailedURL`, and `EFTPaymentGatewayNotifyURL`.

`paymentEFTNotify.php` sets the order to `Queued` or `OutsideBusinessHours` only when the current status is `-1` (NotPaid). Provenance: **Implemented**, consistent with commit `732b5b9`.

### W7 — Branch collects the order

Trigger: branch POS calls `API/api.php` with `branchid`, `user`, `password`, and optional `xml`.

1. Branch must exist on `OLObranch`. Owner must match `OLOowner` Kpolo username and password. Failure returns `<error>Branch not found` or `<error>Username/Password not found` and writes `tOrderXMLAudit`. The failure log stores the supplied username and password. Provenance: **Implemented**.
2. On success, `OLObranch.dtLastCheckin` is set to `Now()` on `master` and `dev`. On `qa` it is set to `Now()` plus 2 hours.
3. If `xml` is present, each order status in the XML is applied when it differs from the current status. Emails are sent for the names in the status table below. The sample comment in the file spells the tag `Acknoledge`; the `switch` matches `Acknowledge`.
4. If the branch is inside opening hours and any order is `OutsideBusinessHours`, the API returns the oldest of those. Otherwise it returns the oldest `Queued` order for that branch. The returned order is set to `InProcess`. The response is `<xml>{order xml}{status list for the day}</xml>`.

Business hours: `OLObranchHours` for today's `dayOfWeek` (`date('w')`), `dayOfWeekEnabled = 1`, and now between `openingTime` and `closingTime`. A row in `publicHoliday` whose month-day matches today forces `dayOfWeek = -1`. Provenance: **Implemented**.

`API/testorder.php` authenticates the same way, inserts a synthetic order with a negative id and user id `27.3`, and does not update `dtLastCheckin` (those statements are commented out). Provenance: **Implemented**.

### W8 — Status mail and timeout

Status names are resolved with `getStatusIDfromStatus` against `tOrderStatus.sStatus`. Numeric ids in comments are not the lookup key. If the live row uses a different id, the name still wins. Provenance: **Implemented**.

| Status name | Comment id in source | When it is set on `master` | Email |
|---|---|---|---|
| `NotPaid` | `-1` | Card or EFT order just created | None at insert |
| `Queued` | `7` | Cash during hours, or successful card/EFT during hours | `OrderCustomerAccept` for cash at insert. Card/EFT acceptance mail is not sent at insert |
| `OutsideBusinessHours` | (name lookup; comment in the API uses this name for orders placed out of hours) | Cash, or paid card/EFT, when the branch is closed | Same as queued for the cash insert path |
| `InProcess` | `2` | API has handed the order to the branch | None in the API switch |
| `Acknowledge` | `6` | POS XML | `OrderCustomerAcknowledge`. If the order number is not greater than 0, `Order Test Success` to the business |
| `Reject` | `4` | POS XML | Customer `OrderCustomerReject` and business `OrderBusinessReject` |
| `NoResponse` | `8` | POS XML | Customer and business NoResponse templates |
| `Success` | `1` | POS XML | `OrderCustomerSuccess` |
| `Aborted` | `3` | POS XML | Customer and business fail templates |
| `Fail` | `5` | POS XML, or timeout cron | Customer and business fail templates. Timeout reason text is `ASAP Order Timed Out!` |
| `Dispatch` | `18` | **Not on `master`.** On `dev` and `qa`, POS XML status `Dispatch` | `Dispatch Notification` to the customer. No business mail in that case |

Cancelled card payments were given status `-3` in commit `732b5b9`. The status **name** for `-3` is not in the PHP that was read. **Unknown / Requires Confirmation** what `tOrderStatus` calls that id.

Timeout cron `cron.daily.orderTimeoutCustomer.php`:

- `Queued` orders whose `dDateModified` is older than `oloRuleProvider.orderFailTimeout` minutes become `Fail`.
- `InProcess` orders use `sysSettings` key `InProcessOrderTimeOutPeriod` (minutes) the same way.
- The customer and the business are emailed.
- `tAuthUser` rows older than 1 day are deleted and their session files removed. Files under `SESSION` and `webadmin/SESSION` older than 2 days are deleted.

`webadmin/cron.daily.orderTimeout.php` only redirects to the customer cron. Provenance: **Implemented**.

### W9 — Catalogue import

Trigger: the import scripts run (scheduler **Unknown / Requires Confirmation**).

`webadmin/cron.daily.importXML.php` (comment dated 22/06/2017) will not import a ZIP when `sysSettings` `Maintenance` is `1` or `ImportedZIP` is `0`. Otherwise it reads the first file in the directory named by system setting `FileLocationImport`.

`webadmin/cron.daily.importStagingOLO.php` copies staging OLO tables from the web-admin database into the customer database, including `branchOfflineTimeout` on the rule provider. On import it can set `dtLastCheckin` to `2000-04-01 00:00:00` (line 1966). Provenance: **Implemented**. Effect on the offline check after an import: **Inferred** — a branch would look offline until it polls again.

On `dev` only (`254187a`), the public-holiday import ignores deleted entries. `master` and `qa` do not have that fix.

### W10 — Refund request

`refund.php` is a static page. It says Sandwich Baron refunds amounts paid if the customer is not satisfied and the business cannot put it right. The request must be in writing (mail, email, or fax). Phone requests are not accepted. The refund is the exact amount, within 30 days, back to the card that paid. The page says the policy can change when posted on the website. Date last updated on the page: 3 November 2009.

Provenance: **Documented** on the page. No refund workflow is implemented in the order code that was read. **Unknown / Requires Confirmation** whether staff process refunds only outside this system.

### W11 — Web admin

Implemented entry points (database-driven Nemo menu; labels in Git are the file names):

- Dashboard: today's orders and orders-by-status chart (`webadmin/index.php`).
- Web-admin users, security groups, settings, email, email templates, audit log, import log, update.
- Customer users, security groups, settings, email, email templates, audit log.
- Branches, branch hours, branch regions, delivery charges, menus, orderable items, item types, message groups, region prices.
- FAQ and FAQ console.
- Reports, including customer order list filtered by status, and a predictive order chart.

Provenance: **Implemented** as pages. Which security groups can open which page is data in `sysSecurityGroup` and was not in Git.

## Integrations

| System | Direction | Trigger | Data | Auth in this repo | Failure |
|---|---|---|---|---|---|
| KingPOS / Kpolo branch | POS calls the site | Poll `API/api.php` | Status XML in; one order XML out; daily status list | Branch id plus `OLOowner` username and password on the query string | Error text and `tOrderXMLAudit` row |
| NuPay | Site posts the customer to NuPay | Card checkout | Amount in cents, merchant fields, return URLs | Branch `OLObranchNuPay` access key, hashed per day | `paymentFailed.php` via the failed URL setting |
| FastPay | Site posts the customer to FastPay | EFT checkout | Amount in rand, merchant id and key, return and notify URLs | `branchFastPay` | Failed URL; notify updates status only from NotPaid |
| Email | Site sends | Status changes, timeout, password reset | Order templates looked up by name | From-name is `oloRuleProvider.emailFromName`. SMTP settings were not confirmed in Git | **Unknown / Requires Confirmation** |
| AWS CodeDeploy | Pipeline pushes a zip | Push to branch `qa` or `prod` | Whole working tree | Bitbucket pipeline variables | Not described in the YAML beyond the script failing |
| Google Tag Manager | Snippet in the customer header | Page render | Container snippet | **On `dev` and `qa` only.** Not on `master` | None in code |

Static HTML files under `emailtemplates/` (`Placed.html`, `Acknoledged.html`, `Successful.html`, `Reject.html`, `Failed.html`, `NoResponse.html`, `ForgotPassword.html`, `ForgotUsername.html`) are in Git. The send functions use template **names** such as `OrderCustomerAccept`. Whether those files are what the mailer sends, or whether the database templates are, is **Unknown / Requires Confirmation**. `webadmin` has email-template editors, which implies the database is the source used at runtime. Provenance of that implication: **Inferred**.

## Constraints

- Time zone `Africa/Johannesburg` is set in both `system.php` files.
- A customer cannot order until they have a real suburb and the branch accepts net orders, is not closed, and has checked in recently.
- Cash, EFT, and card availability are per branch.
- Minimum order is enforced in the checkout button via `getMinimumOrderValues`. On `master` and `qa` the value is `oloRuleProvider.minimumOrderValues` plus `oloSuburb.minOrderValue`. On `dev` a non-zero suburb value replaces the provider value instead of being added. Two hardcoded user ids get `0`. See business rules.
- Public holidays use branch hours stored under `dayOfWeek = -1`.
- Credentials and gateway secrets are stored in PHP source and in database columns. This PRD does not repeat them.

## Requirements discovered from implementation

### FR-001 — Customer registration

The system accepts a new customer only when the compulsory fields in W1 are present and the username, email, and cell are not already in use.

Evidence: `client.register.php`, `validateInUseRegister`.

Implementation: Implemented on `master`.

Confidence: High for the browser rules. Medium for a complete server-side duplicate of those rules.

### FR-002 — Branch selection by suburb

The customer's suburb selects the branch (`oloSuburb.branchId`). A missing, placeholder, deleted, or pending-delete suburb sends the customer to the profile page.

Evidence: `client.menu.php`, `getUserBranch`.

Implementation: Implemented.

Confidence: High.

### FR-003 — Offline branch

If the branch has not checked in within `branchOfflineTimeout` minutes, the menu redirects to `branchOffline.php`.

Evidence: `client.menu.php`, `getBranchOfflineTimeout`, `includes/branchOffline.cls.php`.

Implementation: Implemented.

Confidence: High. The check-in clock is `Now()` on `master` and `dev`, and `Now()` plus 2 hours on `qa`.

### FR-004 — Menu, modifiers, happy hour, previous orders

The customer builds items from the OLO menu with modifier groups, is blocked from happy-hour items outside their window, and can see previous orders.

Evidence: `includes/client.menu.cls.php`, `ajaxfunctions.php`.

Implementation: Implemented.

Confidence: High.

### FR-005 — Delivery, collect, contactless, future time

Checkout offers Delivery (sales mode 4), Collect (sales mode 5), ASAP, future date/time, and contactless (note prefix, sales mode unchanged).

Evidence: `client.order.confirm.php`, `generateXML`.

Implementation: Implemented.

Confidence: High.

### FR-006 — Payment methods per branch

Cash only when `kpoloEnableCash = 1`. EFT only when FastPay merchant fields are real. Card is always offered on the checkout page and still depends on a usable `OLObranchNuPay` row at pay time.

Evidence: `client.order.confirm.php`, `getPaymentGateway`, `getEFTPaymentGateway`.

Implementation: Implemented.

Confidence: High.

### FR-007 — Order is queued only when it should be handed to the store

Cash during hours is `Queued`. Cash outside hours is `OutsideBusinessHours`. Card and EFT stay `NotPaid` until the gateway success/notify path, and that path updates the status only from NotPaid.

Evidence: `ajaxPlaceOrder`, `ajaxUpdateOrderStatusCC`, `paymentEFTNotify.php`, commit `732b5b9`.

Implementation: Implemented.

Confidence: High.

### FR-008 — POS poll

An authenticated branch receives at most one order per call, which moves to `InProcess`, and may update statuses for orders it already has.

Evidence: `API/api.php`.

Implementation: Implemented. `Dispatch` email exists on `dev` and `qa` only.

Confidence: High.

### FR-009 — Timeout

Queued and in-process orders that sit too long become `Fail` and generate customer and business mail.

Evidence: `cron.daily.orderTimeoutCustomer.php`.

Implementation: Implemented. Who schedules the cron is unknown.

Confidence: High for the script. Low for operations.

### FR-010 — Web admin and import

Staff can maintain the catalogue and import KingPOS data, subject to the maintenance flags.

Evidence: `webadmin/`, `cron.daily.importXML.php`, `cron.daily.importStagingOLO.php`.

Implementation: Implemented.

Confidence: Medium. Menu permissions and the full import table list were not exhaustively traced.

## Non-requirements (do not treat as product rules)

- Files named `*Copy*` and `*.bkp.php` are leftover copies (`login - Copy.php`, `paymentEFTSuccess - Copy.php`, and others). They are not a second supported flow.
- `login.lockdown.php`, `login.prelockdown.php`, and `layout.basic.incl.lockdown.php` remain in the tree. Commit `52f3c96` says the lockdown message was reverted. `login.php` is the login page `index` customers are expected to use. Whether the lockdown files are linked from Apache is **Unknown / Requires Confirmation**.
- `phpinfo.php` is present at the web root. It is not a product feature.
- Commented SQL and commented gateway URLs are history, not current behaviour, except where this PRD quotes them as evidence of an earlier change.
