# Business rules — indawo_65

Evidence date: 2026-10-12. Revision: `INDAWO_84_2026` `08157c5`.

Rules below are taken from code or from comments and headers in the repository. Each rule is marked **Documented**, **Implemented**, or **Inferred**. No rule was invented to fill a gap. Gaps are marked **Unknown / Requires Confirmation**.

Passwords and connection strings from `system.php` are not restated here.

## Identity and access

BR-001 — Login identity is `sysUser.strEmail`. The form field is named `strUsername` (fallback query parameter `u`). The security group must be active (`sysSecurityGroup.blnActive = 1`). **Implemented** (`login.php`).

BR-002 — A user with `blnActive` not equal to 1 cannot log in. The login log reason is "Login failed: Inactive User". **Implemented**.

BR-003 — A user with `blnLocked` not equal to 0 cannot log in. Comment in `login.php` dates the lock to 2014-06-23. The login log reason is "Login failed: Locked User". **Implemented**.

BR-004 — Login requires both a matching password (`NemoPassword::checkPasswordMatch` for entity `User`) and a login PIN equal to `SystemSettings["LoginPIN"]`. **Implemented**.

BR-005 — When `EnablePINPolicy` equals `True` and the server date is the first day of the month, cron 1 replaces `LoginPIN` with the last four characters of `Obfuscate()` and emails template "Login PIN" to every `sysUser` row with `blnActive = 1`. **Implemented** (`Cron.daily.1.Lookups.php`). The comparison is the string `True`, not `true`.

BR-006 — When `PASSWORD_Expiry_Check` equals the string `true` and `NemoPassword::checkPasswordExpiry` returns 1, login fails. The message tells the user the password has expired and offers reset. **Implemented** (`login.php`).

BR-007 — `checkPasswordExpiry` treats a missing `dtPasswordExpiry` as expired. If the in-object flag is not truthy it returns not expired. If today is on or after the stored expiry timestamp it returns expired. **Implemented** (`_nemo.password.cls.php`).

BR-008 — When a password is saved and the expiry setting equals `true`, the code sets expiry to 30 days after the password date. It does not read `PASSWORD_Expiry_Days`. The INSERT names five columns and supplies six values, including the expiry date. **Implemented**. Whether the row is stored is **Requires Confirmation**.

BR-009 — Seed rows inside `NemoPassword` default expiry checking to `false`, expiry days to 30, minimum length to 8, history to 8, restricted words to `password|123`, and reset-link interval to `60 MINUTE`. **Implemented** as that SQL. Live settings may already differ. **Unknown** which values production uses.

BR-010 — Logout clears `$_SESSION["ONLINE"]` and destroys the session. Clearing the MFA remember-device cookie on logout is commented out, so a remembered device can survive logout. **Implemented**. **Inferred** from the comment: the cookie is intentionally kept. Whether that is still wanted is **Requires Confirmation**.

BR-011 — MFA remember-device: if cookie `INDAWO_MFA_REMEMBER_COOKIE` hashes with SHA-256 to a matching `tblUserMFARememberedDevices` row for that user, MFA is skipped and `dtUsedAt` is set to now. A cookie that does not match is expired in the browser. **Implemented**. The constant `INDAWO_MFA_REMEMBER_DAYS` is 30. The lookup predicate for `dtExpiresAt` was not re-read past the hash match in this pass.

BR-012 — `blnMFAEnabled = 1` sends the user to verify before the full session is stored. `blnMFARequired` with MFA not enabled sends the user to setup. **Implemented**.

BR-013 — Menu permission flags are view, new, save, delete, and special, per security group and menu item. **Implemented** (`includes/security.group.cls.php`).

BR-014 — Session company is `$_SESSION["ONLINE"]->OWCOMPANYID`, taken from `sysUser.refCompanyID` at login. `whereOWC()` limits SQL to that `refCompanyID`. `setOWC` reloads `strCompany` and `strFolderPath` from `sysCompany`. **Implemented** (`project.functions.inc.php`).

## Users leaving the system idle

BR-015 — The six-month action sets `blnActive = 0` for users returned by a login query: result text "Login successful" or "Login successful (MFA verified)", grouped by user, having max `strDateTime` null or older than `DATE_SUB(NOW(), INTERVAL 6 MONTH)`. **Implemented**. The `OR` is not wrapped with the user join, so the query may not mean what the comment says. **Requires Confirmation**.

BR-016 — Selected-user deactivation sets `blnActive = 0` for posted ids and does not test the six-month interval. **Implemented** (`DeactivateSelectedUsers`).

BR-017 — Both deactivation paths send template "User Account Deactivation Notification", whose body says the account was deactivated for six months without login. The selected-user path uses that template anyway. **Documented** body versus **Implemented** selected-user path.

BR-018 — Before send, both paths replace the recipient with a hardcoded test address (comment: "for testing") and BCC `jacques@overdrive.co.za`. The administrator template is sent to the same test address. A comment says the administrator address is still up for discussion; `louis.s@Indawo.biz` is commented out. **Implemented**. Production intent is **Requires Confirmation**.

BR-019 — The user-list button asks the operator to confirm deactivating everyone who has not logged in for six months. **Implemented** (`user.php`). The confirm text describes the intended rule. The SQL in BR-015 is what runs.

## Job suggestions

BR-020 — Pre-start inserts a suggestion with `intSuggestionStatus = 0`. **Implemented** (`includes/prestart.agreement.cls.php` around the insert that sets status 0). A later update in the same file sets status 1 on a max id. The condition for that update was not fully traced.

BR-021 — The approvals list shows a checkbox only for status 0, an accept image for status 1, and a delete image for status 2. **Implemented**.

BR-022 — Accept sets `intSuggestionStatus = 1`, writes `txtSuggestedValue` into `tblForeman.<strFieldName>` for the suggestion's job number and company, then pushes `SyncRecord("Foreman", ..., "UPDATE")`. **Implemented** (`JobSuggestion::Accept`). The field name is concatenated into SQL. That is **Implemented** behaviour, not an endorsement of the SQL shape.

BR-023 — Decline sets `intSuggestionStatus = 2`. The decline method does not update `tblForeman` and does not call `SyncRecord`. **Implemented**.

BR-024 — This branch does not store an approver user id on accept or decline. **Implemented** absence. The later `indawo_84` repository does store `refApprovedByUserID`. That rule is not in this tree.

BR-025 — Boolean suggestion fields display `0` as No and `1` as Yes. Contact, job type, and planning status display the related name. The sales-person suggested name is read from property `strCstrSalesPersonontact`. **Implemented**. That property is **Requires Confirmation** as a likely defect.

## Orders

BR-026 — Authorisation status words in the filter are Pending (`0`), Accept (`1`), and Declined (`2`), plus All (`-1`). A comment in the same class says `-1` Not Required, `0` Pending, `1` Accepted, `2` Declined. The outstanding-email renderer uses Approved for `1`, Declined for `2`, and a blank for anything else, including pending `0`. **Implemented** inconsistency in labels.

BR-027 — The outstanding-authorisation cron selects rows where `intAuthorisedStatusAdmin = 0` OR `intAuthorisedStatusProd = 0`. It sends template "Order Authorisation Request" only when the count is not zero. Recipient is setting `EmailOrderAuthorisation`. Company logo uses `sysCompany` id 1. The subject time uses the current hour plus 2. **Implemented** (`cron.daily.orderauth.php`). The schedule that launches the script is **Unknown**.

BR-028 — On the 15th of the month, `cron.daily.ordercancel.php` selects `refOrderStatus = 'New'`, `dtDateRequested` before the first day of the previous month, and `refCompanyID = 1`. **Implemented**. Comment from 2017-05-02 says the run day was changed to the 15th.

BR-029 — `cron.daily.ordercancel.1.php`, `.4.php`, `.6.php`, and `.9.php` set the company id to 1, 4, 6, and 9 respectively and filter on that id. **Implemented**. Whether each file is on the live crontab is **Unknown / Requires Confirmation**.

## Cash and job cost

BR-030 — Cash-flow PDFs are built only when the day of the month is 23 or the weekday is Monday. **Implemented** (`cron.CashFlow.php`). The comment says Monday is required because the file name uses the week number.

BR-031 — In the cash-flow PDF, cash under/over is `AmountPaid - (ActualPrime * 1.3)`. **Implemented**. The business meaning of 1.3 was not found in this repository. **Requires Confirmation**.

BR-032 — On the sales-person dashboard, overheads actual are `(material actual + labour actual) * (OverheadsActualPercentage / 100)`. Total actual cost adds subcontractor and those overheads. Cash under/over is cash receipts minus that total. **Implemented** (`includes/dashboard.users.cls.php`). This is not BR-031. The percentage value comes from the job row's data, not from the literal 1.3.

BR-033 — A negative cash under/over, valuation under/over, or nett profit on that dashboard is styled bold red. A non-negative value has no extra colour. **Implemented**.

BR-034 — Budget on that dashboard is `(labour budget + material budget) * (OverheadsActualPercentage / 100 + 1) + subcontractor budget`. When total actual cost exceeds budget, the cost cell is bold red. **Implemented**.

## Foreman sync

BR-035 — The lookup cron comment says the batch deletes live rows whose sync key is not NULL, then inserts from staging, so local rows with a NULL sync key stay. **Documented** in `Cron.daily.1.Lookups.php`. The matching SQL in every cron was not re-read statement by statement.

BR-036 — Accepting a job suggestion pushes the Foreman row to SQL Server immediately through `SyncRecord`. There is no `sysSyncQueue` in this tree. **Implemented**.

BR-037 — `cron.daily.repeater.update.php` does not sync. It prints that it is a temporary file. **Implemented**.

BR-038 — Active company lists used by some report helpers restrict companies to ids 1 and 9 in one branch and to all active companies in another (`project.functions.inc.php` around the `CompanyID in (1, 9)` queries). **Implemented**. Which caller takes which branch was not mapped. **Requires Confirmation** before treating 1 and 9 as the only trading companies. Order-cancel scripts also name companies 4 and 6.

## Host and configuration rules that change behaviour

BR-039 — `www.silicon-overdrive.co.za` is rewritten to `silicon-overdrive.co.za` before the settings switch. **Implemented**.

BR-040 — An unknown `SERVER_NAME` falls through to the default case, which stores settings under `localhost` and sets `SERVER_NAME` to `localhost`. **Implemented**. That host does not receive the SQL Server fields the `localhost` case would have set, because the default case does not assign `hostnameSQL`.

BR-041 — `online.indawo.biz` assigns database settings twice in one case. The later assignment replaces MySQL host with the name `database` and SQL host with `CPTSQL02` (no driver string). The earlier FreeTDS DSN in that case does not remain. **Implemented**.

BR-042 — `127.0.0.1` reads MySQL settings from the environment and does not set SQL Server fields. `localhost` does set SQL Server fields from the environment, with non-empty fallbacks in source. **Implemented**. Fallback secrets are not copied here.

## Rules that are not in this repository

The following are true of `indawo_84` on 2026-10-12 and were searched for here without a match: `sysSyncQueue`, `realTimeSync`, SharePoint file links, an HTTP cron gate, and the October 2026 cash-flow change that stopped multiplying subcontractor actuals by 1.3. Do not apply those rules to `indawo_65` unless a later commit adds them.
