# PRD — indawo_84

Evidence date: 2026-10-12. Source: `git@bitbucket.org:overdrive/indawo_84.git`, `main` at `6ce3e8d` (2026-10-12, "days differences added").

The onboarding issue recorded `223198b` (2026-09-21). That commit is on this `main` history. Requirements below are from the current tree unless marked otherwise.

There is no prior Silicon Ark PRD. The repository already contains `docs/INDAWO-Online-Technical-Documentation.md` (2026-05-26), `README.md`, `CHANGELOG.md`, `NewKnowledgeBase.md`, `setup_composer.md`, and `README-odbc-dashboard.txt`. Where those documents disagree with `main`, the disagreement is stated. The May technical document's claim that there is no realtime sync queue is superseded by code on `main`.

The product name in the UI is Indawo Online. The repository name is `indawo_84`.

## Product purpose

Staff at Indawo companies use a browser application to work with clients, contacts, jobs and enquiries, purchase orders, invoices, pre-start agreements, budgets, assets, and management reports. The online MySQL database is kept in step with a Microsoft SQL Server Foreman system: nightly (and manual) jobs pull Foreman data into MySQL, and selected saves push MySQL changes back to SQL Server.

Provenance: **Documented** (`docs/INDAWO-Online-Technical-Documentation.md`) and **Implemented** (page scripts, `_sync.cls.php`, cron scripts).

Who the operating companies are in production is stored in MySQL `sysCompany`, which is not in Git. Committed cash-flow PDF folders use these directory names: `1-INDAWO CAPE (PTY) LTD`, `2-INDAWO_GAUTENG`, `4-INGOZI`, `5-INDAWO_CONSTRUCTION`, `6-owINDAWO_GAUTENG`, `7-PRODUCTIONPOWERCC`, `8-WARAC`. Provenance: **Implemented** artifacts. Whether that set is the live company list is **Unknown / Requires Confirmation**.

## Users

No persona document was found. Roles below are inferred from tables and screens.

| User | What the code shows | Provenance |
|---|---|---|
| Staff user | Row in `sysUser` joined to an active `sysSecurityGroup`. Login is the user's email. `blnActive = 1` and `blnLocked = 0` are required | **Implemented** (`login.php`) |
| Security group | Menu rows in `sysMenuLevel1` / `sysMenuLevel2` with flags `blnView`, `blnNew`, `blnSave`, `blnDelete`, `blnSpecial` | **Implemented** (`includes/security.group.cls.php`) |
| Company context | Session fields `OWCOMPANYID`, `OWCOMPANYNAME`, `OWCOMPANYFOLDERPATH`, set by `setOWC()` | **Implemented** (`project.functions.inc.php`) |
| Sales-person and manager dashboards | Pages `dashboard.sp.php`, `dashboard.cm.php`, classes `dashboard.home.sp.cls.php`, `dashboard.home.manager.cls.php`, `dashboard.users.cls.php` | **Implemented**. Which security group sees which dashboard is data in `sysMenu` and is **Unknown** from Git |
| Order approver | Order authorisation has separate admin and production status columns | **Implemented** (`includes/order.authorisation.cls.php`) |
| Sync operator | `ODBC_connections.php` and cron scripts. HTTP cron also requires a logged-in user with menu view rights on that cron file, or `?odbc=1` plus HTTP basic auth | **Implemented** (`http_cron_gate.php`, `README-odbc-dashboard.txt`) |
| Public visitor | `index.php` sends an anonymous browser to `login.php` | **Implemented**. No customer self-service site was found |

## Main workflows

### W1 — Sign in

Trigger: browser opens `index.php` without `$_SESSION["ONLINE"]`.

1. `index.php` redirects to `login.php`. If the session exists, it redirects to `home.php`.
2. The user submits email, password, and login PIN (`Action=Login`).
3. The user row must belong to an active security group, be active, and not be locked.
4. Password check uses `NemoPassword::checkPasswordMatch("User", ...)`. The PIN must equal `SystemSettings["LoginPIN"]`.
5. If `PASSWORD_Expiry_Check` is the string `true` and the password is expired, login fails and the page offers a reset link.
6. If the user has MFA enabled or required, and no unexpired remembered-device cookie matches `tblUserMFARememberedDevices`, the flow continues to TOTP verification (`two.factor.authentication.verify.php` / setup). The TOTP issuer string is `Indawo Online`.
7. Logout (`Action=Logout`) unsets `$_SESSION["ONLINE"]` and destroys the session. The remembered-device cookie clear is commented out.

Provenance: **Implemented** (`index.php`, `login.php`, `MFATables.sql`). The exact post-password MFA branch after line 120 of `login.php` was not line-traced in full. The presence of setup and verify pages, the RobThree library, and the remember-device query is **Implemented**.

### W2 — Work inside one company

Trigger: a signed-in user uses a list or edit page.

`getOWC()` returns `$_SESSION["ONLINE"]->OWCOMPANYID`. `whereOWC()` adds `refCompanyID = <that id>` to queries. `setOWC($CompanyID)` loads `sysCompany` and stores the id, name, and folder path on the session.

Provenance: **Implemented**. How the user picks the company (which page calls `setOWC`) was not fully traced. **Unknown** which screen is the switcher.

### W3 — Clients, contacts, jobs, and enquiries

Trigger: user opens `client.php`, `contact.php`, `contact.log.php`, `job.php`, or `enquiry.php`.

Saves of Client, Contact, and Foreman are realtime sync entities. A successful push goes to SQL Server through `SyncRecord`. When `realTimeSync` is false, the write is skipped, logged on `sysSyncQueue`, and excluded from morning and evening retry. Exceptions that still allow writes are documented in the `env` template: `ODBC_connections.php`, pages with `?odbc_test=1`, and `cron.evening.realtime-retry.php`.

Provenance: **Implemented** (`includes/realtime_entities.inc.php`, `env`, `_sync.cls.php`).

Job and enquiry folder icons call `Foreman::folderUiState()` and `Foreman::browserFileUrl()`:

- Empty or invalid `dtFirstEdit` is not SharePoint-era.
- A first-edit date on or after three years before today is SharePoint-era.
- If the disk path exists, the UI state is `icon`.
- If it does not exist and the date is SharePoint-era, the state is `create`.
- Otherwise the state is `archive_alert`.
- File URLs use `SHAREPOINT_SITE` (default `https://indawocape.sharepoint.com/sites/OnlineProjects`) and `SHAREPOINT_LIBRARY` (default `Indawo Online`).

Provenance: **Implemented** (`includes/foreman.cls.php`, `tests/ett_odbc_regressions.php`).

### W4 — Suggest a job change, then accept or decline

Trigger: a suggestion is stored on `tblJobSuggestions`.

- New suggestion rows are inserted with `intSuggestionStatus = 0` from pre-start agreement code.
- `JobSuggestion::Accept()` sets status `1`, stores `refApprovedByUserID` from `$_SESSION["ONLINE"]->ID`, and updates `tblForeman.<strFieldName>` to the suggested value for that job number and company.
- `JobSuggestion::Decline()` sets status `2` and the same approver id. It does not update `tblForeman` in the method body that was read.
- The job page approvals list shows the approver name from `sysUser` and a column "Approved / Declined By". The list is limited to 100 rows.
- Money impact compares current and suggested labour, material, subcontractor, and contract value, and shows total cost, gross profit, and GP%. A negative money change is red (`#c0392b`); a positive money change is green (`#1a7f37`).
- Date impact compares planned and actual start, finish, and duration. A later date (positive day difference) is red. An earlier date is green. Blank dates, `0000-00-00`, and `2000-01-01` are treated as missing.

Provenance: **Implemented** (`includes/job.suggestion.cls.php`, `includes/foreman.cls.php` `getJobSuggestionList`, `includes/job.money.impact.inc.php`, `includes/prestart.agreement.cls.php` inserts). Whether every suggestion insert path uses status 0 is **Unknown**; the pre-start path does.

### W5 — Authorise or cancel a purchase order

Trigger: an order exists on `tblOrder` / `tblOrderAuthorisations`.

- The authorisation list filters admin status, production status, and requested date. Pending rows sort first (`intAuthorisedStatusAdmin = 0` or `intAuthorisedStatusProd = 0` sorts as `-1`).
- `cron.daily.orderauth.php` emails when authorisations are still pending. The May document says the recipient is `sysSettings.EmailOrderAuthorisation`, twice a day. Provenance of the schedule: **Documented**. The script file exists: **Implemented**. The schedule is not in Git.
- `cron.daily.ordercancel.php` cancels `New` orders for company 1 requested before the first day of the previous month, and only runs the cancel body when the date is the 15th. It emails the supplier and a summary. Provenance: **Documented** in the May technical document and **Implemented** as a script. Sibling scripts `cron.daily.ordercancel.1.php`, `.4.php`, `.6.php`, and `.9.php` exist. Their relationship to the company-1 script is **Unknown / Requires Confirmation**.
- Authorisation screens gained a money-impact display in late September 2026 (`9d28fe3`, `7342054`, `a16c9c5`, `4099f45`). The impact helper is `includes/job.money.impact.inc.php`. An order verdict says "Exactly on budget", "Within budget", "Puts this line over budget", or "Already over without this order".

### W6 — Nightly Foreman pull and morning catch-up

Trigger: CLI cron, or an authorised HTTP request rewritten to `http_cron_gate.php`.

The May document describes this chain (times are SAST, Sunday–Friday, with a later Saturday block):

1. `Cron.daily.1.Lookups.php` — client types, sales people, job types, suppliers. On the 1st of the month, if PIN policy is enabled, a new login PIN is emailed.
2. `Cron.daily.2.Clients.php` — clients, contacts, customer addresses.
3. `Cron.daily.3.Foreman.php` — job/enquiry headers. Completion-percent changes insert `tblForemanStatusUpdater`.
4. `Cron.daily.4.Foreman.php` — actuals, invoices, receipts.
5. `Cron.daily.5.Foreman.php` — costings, open items, cost codes, start/end history. Current file header of `cron.morning.realtime-catchup.php` says this cron (and `cron.daily.manuel.php`) then runs morning catch-up.
6. `Cron.daily.6.DataDumps.php` — Excel dumps.
7. `cron.CashFlow.php` — PDF only when the run falls on a Monday or the 23rd.

Morning catch-up re-applies pending `sysSyncQueue.txtAttemptValues` onto local MySQL after the pull, then pushes to SQL Server when ODBC is up.

Provenance of the chain and the Monday/23rd gate: **Documented** (May technical document). Provenance of morning catch-up invocation: **Documented** in `cron.morning.realtime-catchup.php` and **Implemented** as that script. Whether Cron 5's source still includes the call was not re-read line by line. **Requires Confirmation** against the live crontab.

Batch delete removes MySQL rows that already have a non-NULL sync key, then inserts from staging. Rows with a NULL sync key are kept. Provenance: **Documented** (May doc §4.14 and §5.4).

### W7 — Evening realtime retry

Trigger: `cron.evening.realtime-retry.php`.

The file header says schedule `0 20 * * *` SAST, retries open failed pushes from the last 7 days, and emails `ettiene@overdrive.co.za`. Skips logged as `realTimeSync=false` are excluded.

Provenance: **Implemented** header and `includes/realtime_retry.inc.php`. Live crontab entry: **Unknown / Requires Confirmation**.

### W8 — Cash-flow under/over

Trigger: cash-flow PDF or the sales-person dashboard job block.

`CashUnderOver = AmountPaid - ((dblActualLabour + dblActualMaterial) * 1.3 + dblActualSubContractor)`.

The previous expression `AmountPaid - ActualPrime * 1.3` (where actual prime included subcontractor) remains as a comment dated 2026-10-07. The same new expression is in `cron.CashFlow.php` and `includes/dashboard.users.cls.php`.

Provenance: **Implemented** (`41f6b52`).

### W9 — Operate sync from the ODBC dashboard

Trigger: a developer or operator opens `ODBC_connections.php`.

The dashboard checks ODBC, maps crons to tables, can run crons in `?odbc=1` diagnostic mode, creates marked "ODBC Test" rows, shows the realtime queue, and can run pull steps and retry/catch-up. Local README example URL: `http://localhost/indawo_84/ODBC_connections.php`.

Provenance: **Documented** (`README-odbc-dashboard.txt`) and **Implemented** (page and includes).

## Functional requirements

### Access

FR-001 — Anonymous entry. The system sends a browser with no `ONLINE` session to `login.php`, and a browser with that session to `home.php`. Evidence: `index.php`. Implementation: Implemented. Confidence: High.

FR-002 — Login factors. Login requires an email that matches `sysUser.strEmail`, a password accepted by `NemoPassword`, and a PIN equal to the system login PIN. The security group must be active. The user must be active and not locked. Evidence: `login.php`. Implementation: Implemented. Confidence: High.

FR-003 — Password expiry. When `PASSWORD_Expiry_Check` is `true` and the password is expired, login fails and the message points at password reset. Evidence: `login.php`, `resetPassword.php`. Implementation: Implemented. Confidence: High.

FR-004 — MFA. Users can have MFA enabled and MFA required. A remembered-device cookie skips MFA when its SHA-256 hash matches an unexpired `tblUserMFARememberedDevices` row. Evidence: `login.php`, `MFATables.sql`, `two.factor.authentication.setup.php`, `two.factor.authentication.verify.php`. Implementation: Implemented. Confidence: High for the gate. The full verify UI was not walked.

FR-005 — Menu rights. Security groups grant view, new, save, delete, and special per menu item. Evidence: `includes/security.group.cls.php`. Implementation: Implemented. Confidence: High. The live matrix is data, not code.

FR-006 — HTTP cron lock. A browser request for a cron PHP file is rewritten to `http_cron_gate.php`. The caller must have an `ONLINE` session and menu view rights on that file, unless `?odbc=1` and HTTP basic auth are both present. CLI crontab does not use this rewrite. Evidence: `.htaccess`, `http_cron_gate.php`. Implementation: Implemented. Confidence: High.

FR-007 — Denied debug URLs. Apache denies `.env*`, `info.php`, `email.test.php`, `_sync.php`, `odbc_connect.php`, `odbc_connect_2.php`, `zSandBox.php`, and `_debug_*`. Evidence: `.htaccess`. Implementation: Implemented. Confidence: High. `ODBC_connections.php` is not in that deny list.

### Company and records

FR-008 — Company scope. Operational queries that call `whereOWC()` or `getOWC()` limit rows to the session company. Evidence: `project.functions.inc.php`. Implementation: Implemented. Confidence: High for callers that use those functions. A full audit of unscoped queries was not done.

FR-009 — Client and contact maintenance. Users with rights can open `client.php` and `contact.php`. Saves are realtime-sync entities. Evidence: page files, `includes/client.cls.php`, `includes/contact.cls.php`, `includes/realtime_entities.inc.php`. Implementation: Implemented. Confidence: High.

FR-010 — Jobs and enquiries. `job.php` and `enquiry.php` edit Foreman records. Foreman save is a realtime-sync entity. Evidence: page files, `includes/foreman.cls.php`. Implementation: Implemented. Confidence: High.

FR-011 — Job suggestions. Accept writes the suggested field onto `tblForeman` and records the approver. Decline records the approver and status 2. The approvals list shows who approved or declined. Evidence: `includes/job.suggestion.cls.php`, `includes/foreman.cls.php`. Implementation: Implemented. Confidence: High.

FR-012 — Money and date impact. Suggestion and task UI can show current versus suggested cost, GP%, and date differences, including an order budget verdict. Evidence: `includes/job.money.impact.inc.php`, commits `9d28fe3` through `6ce3e8d`. Implementation: Implemented. Confidence: High for the helper. Every screen that includes it was not enumerated.

FR-013 — Pre-start and budget. `prestart.agreement.php` and `budget.adjustment.php` exist with matching classes. Pre-start code inserts job suggestions at status 0. Evidence: those files. Implementation: Implemented as modules. Field-level rules are **Unknown** beyond the suggestion insert and the May document's note that some Foreman update paths are disabled.

FR-014 — Orders. Users maintain orders on `order.php`. Authorisation is a separate list with admin and production statuses. Evidence: `order.php`, `order.authorisation.php`. Implementation: Implemented. Confidence: High.

FR-015 — Invoices. `raiseInvoice.php` and `includes/invoicePDF.cls.php` / `includes/raiseInvoice.cls.php` generate invoice requests. A September 2026 commit creates a `RequestInvoice` folder before writing PDFs. That folder is gitignored. Evidence: files and commit `2301408`. Implementation: Implemented. Confidence: Medium (folder creation not re-read).

FR-016 — Assets. `asset.contact.php` and `asset.movement.php` maintain asset contacts and movements. Empty asset-movement dates are stored as NULL for MySQL 8 (`5534919`). Evidence: page files and that commit. Implementation: Implemented. Confidence: Medium.

FR-017 — People and admin. Screens exist for users, user settings, security groups, sales people, companies, settings, usage log, email, email templates, messages, and my tasks. Evidence: matching `*.php` and `includes/*.cls.php`. Implementation: Implemented. Confidence: High that the screens exist. Business rules inside each screen were not fully extracted.

FR-018 — Reports and dashboards. Report entry points: `report.php`, `reportForeman.php`, `reportQSIncentive.php`, `reportAssetManagement.php`, `onlineUsage.rpt.php`, `kpi.php`, `dashboard.sp.php`, `dashboard.cm.php`, `home.php`. Report classes live under `includes/reports/`. Evidence: those paths. Implementation: Implemented. Confidence: High for existence. Report formulas other than cash under/over were not extracted.

### Sync and files

FR-019 — Batch pull. Cron 1–5 copy SQL Server rows into staging tables and merge them into `tbl*` rows that already have a sync key. Evidence: May technical document and the cron files. Implementation: Implemented scripts. Confidence: Medium (algorithm taken from the May document; cron files were not re-read line by line).

FR-020 — Realtime queue. Client, Contact, Foreman, and Foreman Job Start End Date History attempts are logged on `sysSyncQueue`, including a JSON snapshot used to re-apply local rows after a morning pull. Evidence: `includes/realtime_entities.inc.php`, `includes/syssyncqueue_schema.inc.php`, `includes/realtime_morning_catchup.inc.php`. Implementation: Implemented. Confidence: High.

FR-021 — Local sync guard. When `realTimeSync` is false, normal pages do not push to SQL Server. The skip is logged and is not retried. Evidence: `env`, `system.php`, `_sync.cls.php`. Implementation: Implemented. Confidence: High.

FR-022 — Document links. Job/enquiry file links for SharePoint-era dates, or for files that exist on disk, are SharePoint URLs. Older missing files produce an archive alert rather than a `filesys.indawo.local` link. Evidence: `Foreman::isSharePointEra`, `folderUiState`, `browserFileUrl`, regression test. Implementation: Implemented. Confidence: High.

FR-023 — Cash-flow PDF. The cash-flow cron writes per-company PDFs on Monday or on the 23rd, using the under/over formula in W8. Evidence: May document §4.11 and `cron.CashFlow.php`. Implementation: Implemented formula. The Monday/23rd gate is **Documented** and was not re-read in this pass.

FR-024 — Data dumps. Cron 6 writes Excel Foreman dumps per company, including combined output. Evidence: May document §4.10, `Cron.daily.6.DataDumps.php`, `project.functions.inc.php`. Implementation: Implemented script. Output paths differ for local versus server (`getFilesystemBase()`).

## Integrations

| System | Direction | Trigger | Data | Auth | Failure |
|---|---|---|---|---|---|
| MySQL `indawo_online` | App reads and writes | Every page and cron | Operational tables `tbl*` and `sys*` | `.env` on named production hosts; local fallbacks in `system.php` | `mysqli_connect` result is not fully traced. Connection failure behaviour is **Unknown** |
| SQL Server Foreman / `CLIENTS` on `CPTSQL02` | Pull SQL to MySQL; push MySQL to SQL | Crons and `SyncRecord` saves | Clients, contacts, jobs, financials, lookups | ODBC string and SQL login from `.env` or local fallback | ODBC errors log to `sysSyncLog`. Realtime failures can sit on `sysSyncQueue` |
| SMTP email | Outbound | Login PIN, order authorisation, order cancel, sync error mail | Templates such as "Order Authorisation Request", "Login PIN" | Stored in settings / Nemo email. Credentials **Unknown** | May document: many sync failures do not email. Evening retry header says it emails one address. Order crons email when they have work |
| SharePoint | Browser links only | User opens a job/enquiry file icon | Library-relative path | No SharePoint API client was found. URLs are built in PHP | Missing old files show archive alert instead of a URL |
| SonarQube | CI analysis | Bitbucket pipeline on branch pattern `{master}` | Source scan | `SONAR_TOKEN` pipeline variable | No `master` head was on the remote at evidence time. Whether scans run is **Unknown** |

## Constraints

- PHP 8.4 is the declared runtime (`composer.json`, `setup_composer.md`).
- `composer install` is the production install. `composer update` is discouraged by comments in `composer.json`.
- `.env` must not be committed (`setup_composer.md`, `.gitignore`).
- Display errors are turned on inside every `system.php` host case, including `online.indawo.biz`. Provenance: **Implemented**.
- Menu and company data live in MySQL. A code deploy does not by itself publish a menu change.
- The May technical document, the June plan, and `CHANGELOG.md` are not fully aligned with October 2026 code.

## Unknown / requires confirmation

- Live crontab versus the May schedule.
- Which security groups map to which menus in production.
- Whether order-cancel scripts `.1`, `.4`, `.6`, and `.9` are per-company variants still scheduled.
- Whether `tblForemanStatusUpdater` rows written by Cron 3 are consumed by the desktop Foreman system. The May document leaves this open.
- Production document root (`/var/www/html/indawo_online/` versus `/var/www/indawo`).
- Contents of `development` and `staging`.
- Whether money-impact colours (green = increase, red = decrease) are the approved business meaning for both cost and profit lines. The code uses one colour rule for every money change.
