# PRD — indawo (Indawo Online)

Evidence date: 2026-10-12. Inspected revision: `master` at `ed7defd` (2020-06-03).

This document recovers behaviour from that tree. It is not a proposal. Provenance labels: **Documented** (a file states it), **Implemented** (code does it), **Inferred** (reasonable reading of code that the code does not state), **Unknown**.

## Product purpose

Indawo Online is the browser application construction staff use to work with clients, jobs, orders, invoices, assets, and dashboards while a nightly job copies Foreman data from SQL Server into MySQL.

Evidence: page scripts listed in `features.md`, login audit string "Indawo Online" in `login.php`, cron headers in `Cron.daily.*.php`, and `SyncRecord` in `_sync.cls.php`. **Implemented**.

The README describes a different getting-started story (CodeIgniter-style config, AWS CodeDeploy, vhost `local.indawo`). That story is **Documented** and is not what the PHP files do. See project plan, Current status.

## Users and roles

A person signs in with email, password, and a shared PIN (`login.php`). The session key is `$_SESSION["ONLINE"]`. The user's security group id is `sysUser.refSecurityGroupID`.

Group names are rows in `sysSecurityGroup`. They are not hardcoded. The code does use these ids:

| Id in code | What the code does with it | Provenance |
|---|---|---|
| `-1` | Comment in `home.php` and `kpi.php` calls this super admin (2017-03-22). Can use both order-authorisation checkboxes. Included in admin company lists | **Implemented**. The name "super admin" is a comment |
| `1` and `2` | Treated as admin for company lists, user export, and the KPI manager dashboard. Production-authorisation checkboxes are disabled for these two ids. Admin checkboxes stay enabled | **Implemented** |
| `5` | `home.php` shows the sales-person dashboard (`DashSP`). Everyone else is sent to `my.tasks.php`. `kpi.php` also shows `DashSP` for this id | **Implemented**. Calling the group "sales" is **Inferred** from the class name `DashSP` |
| `8` | `job.php` has a branch for this id, with the comment "ON LAMP IT IS 9" | **Implemented**. What the group is called is **Unknown** |
| `10` | Admin authorisation checkbox is disabled. Still included in the My Tasks authorisation and approval switches | **Implemented** |
| Greater than `2` | User list hides the export button (`user.php`) | **Implemented** |
| Less than `3` | `job.php` comment: "Admin and Super users" | **Implemented** for the comparison. The comment is **Documented** |

Menu entries and the flags view, delete, save, new, and special come from `sysSecurity` joined to the menu tables (`_nemo.menu.cls.php`, `includes/security.group.cls.php`). A security-group screen only lists groups whose id is greater than or equal to the signed-in user's group.

Which id maps to which job title in the business is **Unknown / Requires Confirmation**. The names live in MySQL, and there is no seed file.

## Systems the product talks to

| System | Role | Provenance |
|---|---|---|
| MySQL | Online data: users, menus, orders, jobs, settings, sync maps, login log | **Implemented** |
| Microsoft SQL Server via FreeTDS | Foreman / back office. Nightly copy into MySQL. Suggestion accept writes back | **Implemented** |
| SMTP | PIN mail, lockout mail, order-authorisation reminders, sync logs. From-address is the setting `SMTP Send As` | **Implemented** |
| FTP (Bitbucket Pipelines) | Deploy. See technical documentation | **Implemented** in YAML. Whether it was used in production is **Unknown / Requires Confirmation** |
| Browser | The only client. Pages are server-rendered HTML plus jQuery-style scripts in the pages | **Implemented** |

There is no MFA, no TOTP, no queue worker, and no HTTP API for other products in this tree. **Implemented** absence: those files and symbols are not on `master`. Later Indawo repositories documented in Silicon Ark are a different tree.

## Major workflows

### WF-1 — Sign in

Trigger: the person opens the site.

1. `index.php` includes `_system.php` when `SERVER_NAME` is `localhost`, otherwise `system.php`. Both files are gitignored.
2. If `$_SESSION["ONLINE"]` is set, the browser is sent to `home.php`. Otherwise to `login.php`.
3. `login.php` clears `ONLINE`, then on `Action=Login` loads `sysUser` joined to an active `sysSecurityGroup` by email.
4. The user must be active (`blnActive = 1`) and not locked (`blnLocked = 0`).
5. `NemoPassword::checkPassword("User", ...)` must succeed, and the typed PIN must equal `sysSettings` value `LoginPIN`.
6. The session stores user id, name, email, security group, user code, company id, company name, and company folder path. The browser is sent to `index.php`.

Failure messages distinguish inactive, locked, bad password or PIN, and unknown user or inactive security group. Each attempt is written with `tblLoginInsert` and the label "Indawo Online".

**Implemented** in `index.php` and `login.php`.

### WF-2 — Shared PIN rotation

On cron 1 (`Cron.daily.1.Lookups.php`), if today is the first of the month and `EnablePINPolicy` is the string `True`, the job writes a new four-character PIN into `sysSettings.LoginPIN` and emails every active user using the template "Login PIN".

**Implemented**. Whether the setting is `True` in any database is **Unknown**.

### WF-3 — Password reset and policy

The login page has a "Forgot Password?" panel that posts to an AJAX reset. `NemoPassword` reads policy from `sysSettings`. If the keys are missing, the constructor inserts these defaults: policy enabled, minimum length 8, a strength check that requires lower, upper, and a digit, max age `false` (the expiry check is skipped when the value is the string `false`), history 8, restricted words `password` and `123`, reset-link interval `60 MINUTE`.

Live values can differ because they are updated in `sysSettings`. **Implemented** seed. Live policy is **Unknown**.

### WF-4 — Account lock

`CheckSecurityLock` in `system.functions.inc.php` counts the last five `sysLogin` rows for the user whose result is `Login failed: Incorrect password` or `Login successful`. Five failures without a success set `blnLocked = 1`, last user "Security Lockout", and send a warning email.

`login.php` writes `Login failed: Incorrect username, password or pin.` It does not write `Login failed: Incorrect password`. **Inferred:** the lock query does not see current failure rows, so this lock may not run after the 2016 password change. **Unknown / Requires Confirmation** whether another path locks accounts.

### WF-5 — Company context

`getOWC()` returns the session company id. `whereOWC()` adds `refCompanyID =` that id. `setOWC()` replaces the session company from `sysCompany`.

In the cash-flow PDF company loop inside `project.functions.inc.php`, groups `-1`, `1`, and `2` load every active company. Other groups load only company ids 1 and 9. The comment on that line says the same thing.

My Tasks pre-agreements always call the list for company ids 1, 9, and 6 (`my.tasks.php`).

**Implemented**.

### WF-6 — Home and my tasks

After login, group `5` sees the sales dashboard on `home.php`. Every other group is redirected to `my.tasks.php`. The manager dashboard on `home.php` is commented out.

`my.tasks.php` shows, by group id:

- Order authorisations pending, with Accept and Decline, for `-1`, `1`, `2`, `7`, `10`.
- Suggestion approvals, with Accept and Decline, for `-1`, `1`, `2`, `8`, `9`, `10`.
- Pre-agreements pending for groups `-1` and `1` through `15`.
- Budget agreements pending for the same set of ids as pre-agreements.

`kpi.php` still renders the manager dashboard for `-1`, `1`, and `2`, and the sales dashboard for `5`. A 2018 comment says cases 7 and 11 were removed.

**Implemented**.

### WF-7 — Job suggestion approval

From My Tasks, Accept and Decline post selected ids.

- Decline sets `tblJobSuggestions.intSuggestionStatus = 2`. No SQL Server write.
- Accept sets status `1`, copies `txtSuggestedValue` into `tblForeman` column `strFieldName` for that job number and company, then `new SyncRecord("Foreman", "ID", ..., "UPDATE")` and `Save(0)`.
- Delete returns "Unable to delete Sync Entity." and does not delete.

**Implemented** in `includes/job.suggestion.cls.php`.

### WF-8 — Orders and authorisation

Orders are `tblOrder`, filtered to the session company on the authorisation list. Authorisation rows are `tblOrderAuthorisations`, with separate admin and production statuses:

| Value | Screen |
|---|---|
| `0` | Checkbox (pending) |
| `1` | Accept image |
| `2` | Decline image |

**Inferred** labels from the images and the Accept / Decline buttons. The code does not spell the words "pending".

Group `10` cannot tick the admin checkbox. Groups `1` and `2` cannot tick the production checkbox. Group `-1` can tick both. **Implemented** in `includes/order.authorisation.cls.php`.

`cron.daily.orderauth.php` emails a list of orders that still need authorisation. **Implemented**.

### WF-9 — Monthly order cancel

`cron.daily.ordercancel.php` runs the cancel body only when today is the 15th. It selects `tblOrder` rows with status `New`, `refCompanyID = 1`, and `dtDateRequested` before the first day of the previous month. It sets status `Cancel`, writes `dtCronCancel`, and logs to `sysOrderCancelLog`.

The file header still says "older than 30 days". The query is the first-of-previous-month rule added in a 2017-05-02 comment. **Documented** header, **Implemented** query.

The crontab comment in that file points at `cron.daily.ordercancel.test.php`, not this file. Which file a server actually scheduled is **Unknown / Requires Confirmation**.

Copies `cron.daily.ordercancel_old.php` and `cron.daily.ordercancel.test.php` are also committed.

### WF-10 — Nightly sync

Six scripts, headers "CRON JOB n OF 6":

| File | Header says it syncs |
|---|---|
| `Cron.daily.1.Lookups.php` | Client type, sales person, job type, supplier. Also the monthly PIN |
| `Cron.daily.2.Clients.php` | Client, contact |
| `Cron.daily.3.Foreman.php` | Foreman |
| `Cron.daily.4.Foreman.php` | Foreman actual, invoices, receipts |
| `Cron.daily.5.Foreman.php` | Foreman costings, open items, cost codes, job start and end date history |
| `Cron.daily.6.DataDumps.php` | Data dump normal, quote, and full |

Each run writes `sysSyncLog`. Mappings are rows in `sysSyncMappings`. **Implemented**.

On host name `lamp`, `SyncRecord::UpdateLocalSyncKey()` returns without writing the local sync key. **Implemented**.

### WF-11 — Cash-flow PDF

`cron.CashFlow.php` loads every `sysCompany` with `CompanyID > 0`. For each company, if the day of the month is `23` or the weekday is Monday, it calls `GenerateCashFlowPDF`. VAT percent is read from `$SystemSettings[VAT]` (comment dated 2018-02-27).

The comment above the condition says the report must be Monday because the file name uses a week number. The condition is OR, so the 23rd runs even when it is not a Monday. **Documented** comment, **Implemented** condition. Whether that OR is intentional is **Unknown / Requires Confirmation**.

Committed samples of those PDFs are under `Cash Flow Report/`.

## Functional requirements recovered from the implementation

FR-1. A person cannot use the application until email, password, and PIN succeed and the user and security group are active and the user is not locked. **Implemented.** `login.php`.

FR-2. The session company is the user's `refCompanyID` at login. List filters that call `whereOWC()` stay on that company until `setOWC()` changes it. **Implemented.**

FR-3. Menu items and view/save/new/delete/special rights are data, not PHP constants. **Implemented.** `_nemo.menu.cls.php`.

FR-4. A user cannot open a security group or, on the user screen, assign a group whose id is lower than their own. **Implemented.** `security.group.php`, `user.php`.

FR-5. Only groups `-1`, `1`, and `2` see every active company in the cash-flow company loop. Other groups see companies 1 and 9. **Implemented.**

FR-6. Accepting a job suggestion updates the Foreman row in MySQL and sends an UPDATE to SQL Server. Declining does not. **Implemented.**

FR-7. Job-suggestion rows are not deleted from the suggestion screen. **Implemented.**

FR-8. On the 15th, new orders for company 1 requested before the first of the previous month are cancelled and logged. **Implemented** in `cron.daily.ordercancel.php`. Scheduling is **Unknown**.

FR-9. Order authorisation is two decisions (admin and production), each pending, accepted, or declined. **Implemented** statuses. The words for 0, 1, and 2 are **Inferred**.

FR-10. Cash-flow PDFs are generated per company on Mondays and on the 23rd. **Implemented.**

FR-11. The first of the month can rotate the shared PIN and email it, when `EnablePINPolicy` is `True`. **Implemented.**

FR-12. Five matching login failures lock the user and email them. The result string the lock searches for is not the string login writes. **Implemented** both sides. The combined effect is **Inferred** and needs confirmation.

## Constraints

- PHP pages and the `mysql_*` API. No `vendor/autoload.php` include was found in application code.
- Host secrets and the live `system.php` are not in git. Committed `system.HTTP.php` and `system.HTTPS.php` contain plaintext passwords. Those values are not repeated here.
- No schema is committed, so reports and screens that only exist as SQL against views such as `vJob` cannot be verified against a database.
- Copy files (`login - New.php`, `order.cls - Copy.php`, and the rest) are in the tree. The pages that were read include the unsuffixed file. Whether a web server could still request a copy by URL was not tested. **Inferred** risk.

## Out of scope for this repository's current tree

- MFA, remember-device, recovery codes, and inactive-user deactivation. Those are described for other repositories in Silicon Ark (`indawo_65`, `indawo_84`). They are not in this `master`.
- A realtime `sysSyncQueue`. Not present. Sync here is the nightly scripts plus `SyncRecord` on suggestion accept.
- Docker packaging. That is `indawo_docker` and `indawo_online_docker`, other repositories.
- The AWS CodeDeploy application named in the README. No CodeDeploy file is committed, and the pipeline does not call it.
