# Business rules — wca_dms (Admin Overdrive)

Evidence date: 2026-09-29.

Source: `git@bitbucket.org:overdrive/wca_dms.git`, `main` at `9f51c26`.

Rules below are taken from code, seeds, and `README.md`. Nothing here is a product decision. Where the code behaves a certain way and no document states the rule, the rule is marked **Inferred**.

Secrets are not copied.

## Identity

| ID | Rule | Provenance |
|---|---|---|
| BR-001 | The operator-facing name is Admin Overdrive. The repository and the `env` database name are `wca_dms`. | **Documented** in `README.md` and `composer.json`. **Implemented** in views and mail |
| BR-002 | What "WCA" stands for is not stated. | **Unknown / Requires Confirmation** |

## Who may use the system

| ID | Rule | Provenance |
|---|---|---|
| BR-010 | Four role names are seeded: Developer, Admin, Company Admin, Company User. | **Implemented** seed. Business meaning of each name beyond the code below is **Unknown** |
| BR-011 | Developer (treated as role id 1) and Admin (role id 2) are "Silicon internal roles" and receive every company and every store in session. | **Inferred** from the comment and the `== 1 \|\| == 2` branch. Ids assume empty-table insert order |
| BR-012 | Company Admin (role id 3) receives only companies linked in `company_users` with `active = 1`. Store access is still set to all. | **Inferred** from `setCompanyUsersAndAdminSessionVars` |
| BR-013 | Company User (role id 4) gets no company session arrays from that method. | **Inferred** omission. Whether that is intentional is **Unknown / Requires Confirmation** |
| BR-014 | A user with `active = 0` is told their access has been revoked and cannot sign in or complete password reset. | **Implemented** |
| BR-015 | The application does not offer self-registration. | **Implemented**. Comment: "We don't do registrations on this application" |
| BR-016 | `registration()` still tells the visitor registration succeeded and does not insert a user. | **Implemented**. Treat as a defect relative to BR-015, not as a second rule |
| BR-017 | A new user created through `UserModel::createUser` is welcomed by email ("Welcome to Admin Overdrive"). When company data is present the insert also writes `company_users`. | **Implemented** |
| BR-018 | Menu visibility is data, not a fixed list. A role may access a URL's first segment only when `user_access` links that role to the matching menu id. Missing menu redirects home. Missing grant redirects to `blocked`. | **Implemented** |
| BR-019 | `home` is excluded from the grant filter, so reaching the dashboard does not require a menu grant. | **Implemented** |
| BR-020 | BI reporting markup is rendered only when session role is 1 or 2. | **Inferred** from the view condition. The chart endpoints themselves are not implemented |

## Sign-in and recovery

| ID | Rule | Provenance |
|---|---|---|
| BR-030 | Login identifier is `users.username`, used as an email. Password check is `password_verify` against the stored hash. | **Implemented** |
| BR-031 | After a correct password, email 2FA is required unless an active code row has `status = 1` and was updated less than 1 day ago. | **Implemented**. Comment mentioning 10 days is not the rule |
| BR-032 | The 2FA code is a random integer from 100000 to 999999, emailed in clear text in the body "Your 2FA code is: …". | **Implemented** |
| BR-033 | A matching code sets `email_authentication_codes.status` to 1. | **Implemented** |
| BR-034 | Wrong password and unknown user are both described to the visitor as "Your ID or Password is Wrong". | **Implemented** |
| BR-035 | Unknown-user lookup calls `$user['active']` before the "user does not exist" branch. | **Implemented**. The friendly message in BR-034 is not reached if `getUser` returns null |
| BR-036 | Login history status codes used in `Welcome`: 1 failure at login, 2 success, 3 logout, 4 failure during code confirmation, 5 invalid reset token, 6 reset requested. | **Inferred** labels from the call sites. No lookup table was found |
| BR-037 | Status 2 and 3 do not count toward lockout. Every other recorded status does. | **Implemented** in `LoginHistoryModel::updateLoginHistory` |
| BR-038 | Five failures with the same email, IP, status, and `active = 1` inside five minutes add an active `blocked_users_list` row. The limit comment still says to make it 5. | **Implemented** |
| BR-039 | If the request IP is already an active blocked row, session `userBlocked` is set when `Welcome` is constructed. | **Implemented** |
| BR-040 | Password reset token is 32 random bytes, hex encoded, stored on `password_reset`. | **Implemented**. No migration for that table is in this repo |
| BR-041 | Reset password must match the confirmation and be at least 8 characters. No other complexity rule is applied on that action. | **Implemented** |
| BR-042 | If the email is not a user, reset still flashes "Reset password link sent to {email}" and does not send mail. | **Implemented** |
| BR-043 | Reset submit parses token and email from the previous URL, not from posted fields. | **Implemented** |
| BR-044 | An email value that contains a space is treated as a `+` (address local-part). | **Implemented** in `checkResetPassword` and `checkPasswordResetToken` |
| BR-045 | Sessions are stored in MySQL, matched to IP, and regenerated by destroy. Cookie name `ci_session`. | **Documented** in `env` as the template values |
| BR-046 | CSRF protection is not applied globally. | **Implemented** by the commented filter |

## Companies, orders, and faults

These rules are visible only where code or views state them. The controllers that would enforce most of them are not in the repository.

| ID | Rule | Provenance |
|---|---|---|
| BR-050 | EDI Overdrive views belong under `app/Views/edioverdrive/`. | **Documented** in `readme.txt` in that folder |
| BR-051 | Shoprite order pull uses HTTP Basic credentials from the method record, sends a fixed contract id header, asks for XML, and does not verify SSL. | **Implemented** in `Rest.php`. Contract id value omitted |
| BR-052 | Shoprite confirmation is an HTTP PUT of order numbers as `ArrayOfLong` to `{endpoint}?action=a`. | **Implemented** |
| BR-053 | Pick n Pay pull defaults to the UAT/test PullService. Production WSDL and endpoint are used only when `api_endpoint` equals the production pullservice URL. | **Implemented** |
| BR-054 | Pick n Pay pull requests message type `Order` and `acceptBatch` Y for a company GLN. | **Implemented** |
| BR-055 | Pulled XML is written under `writable/company_orders_xml/shoprite/` or `.../pnp/`. | **Implemented** |
| BR-056 | Nothing in this repository calls the SOAP or REST methods. | **Inferred** from a repository search. An external scheduler may still call them. **Unknown / Requires Confirmation** |
| BR-057 | CSV import types are only `Upload-Products` and `Upload-Stores`. Rows are stamped with session `companyID`. | **Implemented** in `Csv.php`. Save cannot run: models are missing |
| BR-058 | Vendor numbers, products, orders, invoices, and fault processing are routed, and fault processing is split into orders, invoices, and products. | **Inferred** from route names. Enforcing classes are absent, so the rules are not executable here |
| BR-059 | Public holidays can be stored and activated. The cron entry point does not skip those dates. | **Implemented** storage. Skip rule is **not** implemented |

## Jobs and alerts

| ID | Rule | Provenance |
|---|---|---|
| BR-070 | The cron URL does not require a logged-in user. | **Implemented** filter exception |
| BR-071 | Cron does not run on Saturday or Sunday, before 08:00, or at 23:00 or later, using PHP `date()`. | **Implemented**. Comment records a change from a 17:00 end |
| BR-072 | In production the query parameter `key` must equal `SECURITY_KEY`. The constant is still a placeholder instruction, not an external secret. | **Implemented** |
| BR-073 | Only `cronjob_actions.active = 1` rows run, in `order` ascending. | **Implemented** |
| BR-074 | A job runs when its `function` exists on `CronJobs` and the minutes since the last log are at least `recurrence`. Recurrence 0 never runs. `override=true` ignores timing. `action` limits which row runs. | **Implemented** |
| BR-075 | Scheduled mail sends up to `items_process_total` rows, or 10 if that column is empty, where `processed = 0` and `send_date` is not null and is due. Oldest id first. | **Implemented** |
| BR-076 | If any monitor resource name appears more than 55 times in the last 5 minutes, an email is sent to contact id 2, display name Wessels. | **Implemented**. Comment says to fix the recipient after testing |
| BR-077 | The spark monitor treats CPU at or above 5% and memory at or above 50% as breaches until session thresholds replace them. The author comment says the values are hardcoded for now. | **Implemented** |
| BR-078 | `monitor:system` is a loop with `sleep(5)` and expects Linux `top` and `posix_kill`. | **Implemented**. Not a Windows WAMP behaviour |

## Mail and integrations

| ID | Rule | Provenance |
|---|---|---|
| BR-080 | Outbound mail is SMTP via `smtp.overdriveqa.co.za` port 587, from `info@overdriveqa.co.za`, with BCC `admin@overdrive.co.za`. | **Implemented** in config and `sendMail` |
| BR-081 | The visitor is told not to reply to notification mail. | **Implemented** footer text |
| BR-082 | Greeting name is the part of the address before `@`, with non-letters turned into spaces. | **Implemented** |
| BR-083 | Slack `sendMessage` posts to a webhook hardcoded in the controller. The route is public. | **Implemented** |
| BR-084 | ChatGPT uses the completions endpoint and model `text-davinci-003`. The page requires login. Grant checks are skipped for `chatGPT/*`. | **Implemented** |
| BR-085 | Salesforce and SMS helpers do not send anything. | **Implemented** as empty or commented functions |
| BR-086 | README states MySQL row-level security, AWS, PDF, and spreadsheet features. Application code does not enforce or call them. | **Documented** only. Do not treat as current rules |

## Developer and local setup rules stated by the README

These are instructions to developers, not runtime enforcement.

| ID | Rule | Provenance |
|---|---|---|
| BR-090 | Copy `env` to `.env`, install Composer dependencies, create the database, migrate, seed `users`, generate an encryption key, then `php spark serve`. | **Documented** |
| BR-091 | Local test URL in the README is `http://localhost/wca_dms/`. The Apache example uses host `adminOverdrive.local` and folder `adminOverdrive`. | **Documented**. The two locations disagree |
| BR-092 | README requires MySQL MCP, Playwright MCP, and Context7 MCP. The repository does not contain that tooling. | **Documented** only |
| BR-093 | README points at Jira project SI and Confluence space AO for todos and function documentation. | **Documented** links. Not verified |

## Rules that need a human decision

1. Is role 4 supposed to be scoped like role 3, scoped to stores, or denied company data?
2. Should cron skip `public_holidays`?
3. Is the 1-day 2FA window the approved rule, or the 10-day comment?
4. Is the weekend and 08:00–23:00 cron window approved, and in which timezone?
5. Are the missing order, invoice, product, company, and fault-finder controllers meant to be absent from `wca_dms`?
6. Should the committed mail, Slack, OpenAI, and encryption secrets be rotated? This document does not record their values.
