# Business rules — indawo

Evidence date: 2026-10-12. Tree: `master` at `ed7defd`.

Rules below are taken from PHP conditions, SQL, and comments in that tree. A comment that disagrees with the code is recorded as a disagreement. Nothing here is a new rule.

Live settings in `sysSettings`, menu rows, and `sysCompany` can change behaviour without a code change. Those databases are not in git.

## Identity and access

**Implemented.** Login requires all of the following (`login.php`):

- A `sysUser` row whose email matches the typed username.
- The joined `sysSecurityGroup` row has `blnActive = 1`. If the group is inactive, the audit text is "User not found or SG not active".
- `sysUser.blnActive = 1`. Otherwise the message is the password class string for an inactive user.
- `sysUser.blnLocked = 0`. Otherwise the account-locked message.
- `NemoPassword::checkPassword` succeeds for type `User`.
- The typed PIN equals `sysSettings` key `LoginPIN`.

**Implemented.** Password policy keys, inserted when missing (`_nemo.password.cls.php`):

| Key | Seed value | Effect in code |
|---|---|---|
| `PASSWORD_Enable_Policy` | `1` | Use `sysPassword` |
| `PASSWORD_Strength_Check` | `true` | Browser check requires length, lower, upper, and a digit |
| `PASSWORD_Strength_Check_MinLength` | `8` | Minimum length |
| `PASSWORD_MaxAge` | `false` | Expiry check is skipped while the value is the string `false` |
| `PASSWORD_MaxHistory` | `8` | Number of previous passwords rejected |
| `PASSWORD_Restricted_Words` | `password\|123` | Those words fail the browser check |
| `PASSWORD_Reset_Interval` | `60 MINUTE` | Reset token must be newer than this interval |

The seed comment says max age is a `strtotime` period such as `30 days` when it is not `false`. **Documented** in the INSERT comment. **Implemented** skip when the value is `false`.

**Implemented.** `CheckSecurityLock` locks the user at five failures. It only counts `sysLogin.strResult` values `Login failed: Incorrect password` and `Login successful`. Current login failures are stored as `Login failed: Incorrect username, password or pin.` **Inferred:** those failures do not increment the lock. Needs confirmation.

**Implemented.** On the first of the month, cron 1 replaces `LoginPIN` and emails active users, only when `EnablePINPolicy` equals `True`.

**Implemented.** Security group maintenance:

- The group list is `SecurityGroupID >=` the signed-in user's group (`includes/security.group.cls.php`).
- Saving a group id lower than the signed-in user's group is rejected (`security.group.php`).
- Assigning a user a group id lower than the signed-in user's group is rejected (`user.php`).
- User export is hidden when the signed-in group id is greater than 2 (`user.php`).

**Inferred.** Group `-1` is super admin, groups `1` and `2` are admin, and group `5` is a sales person. The first two names are comments. The sales name is inferred from `DashSP`. The `sysSecurityGroup.strSecurityGroup` labels are **Unknown**.

## Company

**Implemented.** At login the session company is `sysUser.refCompanyID`. `whereOWC()` limits a query to that company. `setOWC()` switches it.

**Implemented.** When a cash-flow or related PDF loop has no explicit company filter (`project.functions.inc.php`):

- Groups `-1`, `1`, and `2` load every active `sysCompany` row.
- Every other group loads active companies whose id is `1` or `9`.

The code comment states that rule. **Documented** and **Implemented**.

**Implemented.** My Tasks pre-agreement lists are requested for company ids `1`, `9`, and `6` regardless of that loop (`my.tasks.php`).

**Implemented** as committed files, not as a company master: cash-flow PDF folders exist for ids 1, 2, 4, 5, 6, 7, 8, and 9, with these folder names:

- `1-INDAWO CAPE (PTY) LTD`
- `2-INDAWO_GAUTENG`
- `4-INGOZI`
- `5-INDAWO_CONSTRUCTION`
- `6-owINDAWO_GAUTENG`
- `7-PRODUCTIONPOWERCC`
- `8-WARAC`
- `9-LUVTHIS INDAWO CONSTRUCTION`

There is no folder for id 3. The live names and which companies are active are in `sysCompany`. **Unknown** beyond these artefacts.

## My tasks by group

**Implemented** in `my.tasks.php`. Ids not listed fall through and do not get that block.

| Queue | Group ids that see it |
|---|---|
| Order authorisations, with Accept and Decline | `-1`, `1`, `2`, `7`, `10` |
| Suggestion approvals, with Accept and Decline | `-1`, `1`, `2`, `8`, `9`, `10` |
| Pre-agreements pending | `-1`, `1` through `15` |
| Budget agreements pending | `-1`, `1` through `15` |

**Implemented.** After login, only group `5` stays on the sales home dashboard. Other groups are sent to My Tasks (`home.php`).

**Implemented.** KPI manager charts are groups `-1`, `1`, and `2`. A comment dated 2018-01-25 says groups `7` and `11` were removed from that switch (`kpi.php`).

## Job suggestions

**Implemented** (`includes/job.suggestion.cls.php`):

- Status `1` means Accept. Status `2` means Declined. The dropdown SQL says those words.
- Accept writes the suggested value onto `tblForeman` for the job number and company, then updates SQL Server through `SyncRecord` entity `Foreman`.
- Decline only sets status `2`.
- Delete is refused with "Unable to delete Sync Entity."

## Orders

**Implemented.** Authorisation list is limited to the session company (`getOWC()`).

**Implemented.** Status values on both admin and production columns:

- `0` shows a checkbox.
- `1` shows the accept image.
- `2` shows the decline image.

**Inferred** that 1 means accepted and 2 means declined, from the images and the My Tasks buttons.

**Implemented.** Who may tick:

- Admin checkbox: disabled for group `10`. Enabled for everyone else, including `-1`.
- Production checkbox: disabled for groups `1` and `2`. Group `-1` is then turned back on. Other groups stay enabled.

**Implemented.** Monthly cancel (`cron.daily.ordercancel.php`):

- The body runs only when the calendar date is the 15th.
- Only company id `1`.
- Only `refOrderStatus = 'New'`.
- Only `dtDateRequested` strictly before the first day of the previous month.
- Status becomes `Cancel`, `dtCronCancel` is set, and `sysOrderCancelLog` receives start and per-order notes.
- The email uses company id `1` for the company lookup, not the session company.

The header still says "older than 30 days". That sentence is **Documented** and is not the query.

The commented Friday schedule (2nd or 4th Friday) is not active. **Documented** as comments, **removed** from the running condition.

## Cash and job cost

**Implemented.** Cash-flow PDF generation considers a company when the day of month is `23` or the weekday is Monday (`cron.CashFlow.php`). The comment says it must be Monday because the file name uses a week number. The Boolean is OR. Disagreement: comment versus code.

**Implemented.** VAT percent for that cron is `$SystemSettings[VAT]` (comment 2018-02-27). The numeric VAT rate is a setting, not a literal in that header. **Unknown** until the database is read.

**Implemented.** Job and dashboard filters in `project.functions.inc.php` can restrict by contract manager, project manager, sales person, quantity surveyor, and measuring surveyor codes. Those are optional filters, not a standing rule about who may see a job.

**Implemented.** QS incentive report checks groups `-1` and `1` (`reportQSIncentive.php`). `reportForeman.php` checks group `-1` for one branch. The rest of each report's rights come from the menu tables.

## Foreman sync

**Implemented.** Nightly order is the six cron headers: lookups, then clients and contacts, then Foreman, then actuals/invoices/receipts, then costings/open items/cost codes/dates, then data dumps. Logs go to `sysSyncLog`. Field mapping is `sysSyncMappings`.

**Implemented.** If `RemoteDatabase` on the mapping is the literal `$COMPANY`, the remote database name is taken from the company sync field on the row (`_sync.cls.php`).

**Implemented.** `SyncRecord::Save()` sends the INSERT or UPDATE SQL through `Sync::doQuery()`, which uses the SQL Server connection. The class comment says the class "does not interact with the REMOTE DB, local only". That comment disagrees with `Save()`.

**Implemented.** On `SERVER_NAME == "lamp"`, `UpdateLocalSyncKey()` returns immediately and does not write the sync key back to MySQL. A comment in the constructor path says `eval` of ODBC expressions fails on dev because `odbc_*` is missing.

**Implemented.** Active companies that have `syncRemoteDatabase` set are the ones included when cost-code-in-use is updated from SQL Server (`Sync::` cost-code method in `_sync.cls.php`). A 2014 comment in cron 1 says an older cost-code process timed out and was replaced.

## Host and configuration rules that change behaviour

**Implemented** in `system.HTTP.php` and `system.HTTPS.php`, which no include statement references. **Unknown / Requires Confirmation** whether production `system.php` is a copy of either file.

If those files are what runs:

- An unknown host name is rewritten to `online.indawo.local` before the connection is chosen. The comment marks that key "LIVE LIVE".
- `www.silicon-overdrive.co.za` is rewritten to `silicon-overdrive.co.za`.
- `session.gc_maxlifetime` is set to `7200` seconds.
- MySQL failure prints "Database is unavailable!" or "Database Connection has Failed!" and does not redirect.

**Implemented** in `index.php`, which does run: localhost loads `_system.php`; any other host loads `system.php`.

Secret values in the committed host files are not recorded in this document.

## Rules that are not in this repository

- The display names of security groups.
- Whether `EnablePINPolicy` is on, and the current PIN, VAT, and SMTP settings.
- Which cron file the server's crontab invokes. The order-cancel comment names the `.test` file.
- Whether company 1 is still the only company whose new orders are auto-cancelled. Later repositories documented elsewhere in Silicon Ark cancel more than one company. This tree cancels company 1 only.
- Whether the unmerged `Jacques` branch changed any of the rules above. Its diff was not read as behaviour.
