# Technical documentation — wca_dms (Admin Overdrive)

Evidence date: 2026-09-29.

Source: `git@bitbucket.org:overdrive/wca_dms.git`, `main` at `9f51c26` (2025-09-01).

Credential values found in the tree are named, not copied.

## Stack

| Layer | What the repo shows |
|---|---|
| Language | PHP. `composer.json` requires `^8.0`. `README.md` says 8.3. **Requires Confirmation** |
| Framework | CodeIgniter 4, package `codeigniter4/framework` `^4.6`. Package name in Composer is `codeigniter4/adminstarter`. Description "Admin Overdrive". License MIT. `LICENSE` is the CodeIgniter / BCIT MIT text |
| Database | MySQL through MySQLi. `env` sets host `localhost`, database `wca_dms`, username `root`, empty password |
| Web root | `public/` (`README` Apache example and CodeIgniter layout) |
| UI | AdminKit PRO v3.0.2 (`public/assets/css/app.css`), Bootstrap 5.1.1 (`bootstrap.min.css`), DataTables (Bootstrap 5 build, copyright 2008–2023), Feather icons, jQuery |
| HTTP client | CodeIgniter CURLRequest in `Rest.php`. PHP `SoapClient` in `Soap.php`. curl in the ChatGPT and Slack helpers |
| CSV | `league/csv` `^9.16` |
| Declared and unused in app code | `aws/aws-sdk-php` `^3.254`, `mpdf/mpdf` `^8.1`, `phpoffice/phpspreadsheet` `^1.25`, `orhanerday/open-ai` `^3.5` |
| Tests | PHPUnit `^9.1`, Faker, vfsStream. `phpunit.xml.dist` bootstraps CodeIgniter's test bootstrap and includes `./tests` |
| CI | Bitbucket Pipelines, SonarQube scan only |

`overview.md` is a generic structural description. It says the database is "likely" MySQL and lists features as likely because of dependencies. Where it conflicts with code, the code is the implementation record.

## Repository layout

```text
app/            Controllers, Models, Views, Config, Database, Filters, Helpers, Libraries, Commands
public/         Front controller and assets
tests/          PHPUnit examples and HealthTest
writable/       CodeIgniter writable tree (logs, cache, uploads, session files if used)
env             Environment template (not named .env)
spark           CodeIgniter CLI
composer.json
bitbucket-pipelines.yml
phpunit.xml.dist
sonar-project.properties
sonarqube_scan.bat
README.md
overview.md
LICENSE
```

`README.md` links to `PROJECT_RULES.md`, `CHANGELOG.md`, and `NewKnowledgeBase.md`. Those paths are not in the tree.

`builds/` was listed at task creation. It was not present as a tracked top-level documentation source in this clone. **Unknown** whether it exists only on the scan host.

## Request path

1. `public/index.php` boots CodeIgniter.
2. Routes load only from `app/Config/Routes.php` (`Routing::$routeFiles`). `$autoRoute` is false. Default controller `Home`, default method `index`. `App::$baseURL` is `''`. `App::$indexPage` is `index.php`.
3. Global `before` filters (`app/Config/Filters.php`):
   - `isLoggedIn` → `App\Filters\Authentication`. Except: `/`, `getLogin`, `register`, `organisations` and children, `tracking-pixel`, `show404`, `404`, `cronJobs`, `identity-unknown`, `store-api` and children, `2fa-validation`, `confirmCode`, `forgot-password`, `sendResetPasswordLink`, `reset-password`, `resetPassword`, `slack`.
   - `isGranted` → `App\Filters\Authorization`, with those paths plus `blocked`, `home`, `Welcome/*`, and `chatGPT/*`.
4. CSRF and honeypot are aliased and not global.
5. Toolbar runs after the request except `chatGPT/*`.
6. `Cors` filter class exists at `app/Filters/Cors.php` and is not registered in `Filters.php`.

Login fails closed by redirecting to `/`. Grant failure with no menu row redirects to `/`. Grant failure with a menu row and no `user_access` redirects to `blocked`.

## Routing map

Explicit routes that resolve to classes present in `app/Controllers`:

| Method and path | Controller |
|---|---|
| `GET /`, `POST GetLogin` | `Welcome::index` |
| `GET show404`, `blocked`, `forgot-password`, `reset-password`, `2fa-validation` | `Welcome` |
| `POST resetPassword`, `sendResetPasswordLink`, `ConfirmCode` | `Welcome` |
| `GET home` | `Home::index` |
| `GET chatGPT`, `GET chatGPT/sent` | `ChatGPT` |
| `users/*` permission and user routes, `GET users/userlist` | `Users` |
| `system_settings` and the cron, email, holiday, login-history, blocked, and general-settings routes whose methods exist | `SystemSettings` |
| `POST /upload` | `Upload::uploadFiles` — method is not defined. `Upload` only implements `index` |
| `POST csv/upload`, `POST csv/save` | `Csv` |
| `system_users` and company/role routes | `SystemUsers` |
| `menuManagement` GET and create POSTs | `Developers\MenuManagement` |
| `GET crudGenerator` | `Developers\CRUDGenerator` |
| `GET monitoring` and usage/log/threshold routes | `ServerMonitoring` |
| `GET cronJobs` | `CronJobs::index` |
| `GET tracking-pixel`, `GET Tracking-pixel` | `EmailPixelTracking` |
| `GET slack` | `Slack::sendMessage` |

Routes whose controller class is **not** in the tree:

| Path prefix | Class named in the route |
|---|---|
| `products`, `products/*` | `Products` |
| `labels/status/*` | `LabelStatus` |
| `getXMLData` | `GetXMLData` |
| `orders/getOrders` | `Orders` |
| `labels/groups/*` | `LabelUserGroups` |
| `labels/email-templates/*` | `LabelEmailTemplates` |
| `company/*` | `Company` |
| `faultfinder`, `faultfinder/*` | `FaultFinder` |
| `resource organisations` | `Organisations` (CodeIgniter resource convention) |
| `resource store-api` | `StoreApiController` |

`SystemSettings` routes with no matching method: `getCompaniesToProcessForFaults`, `changeErrorCheck`, `getTransportMethods`, `changeTransportMethodActive`, `getFaultTypes`, `updateOrCreateFaultType`.

`Reports` is not given a route. The view calls many `reports/...` URLs. One such route is commented out.

Controllers present with no route found in `Routes.php` (unreachable while auto-route stays off): `Soap`, `Rest`, `File`, `Email`, `LoginHistory`, `SiteHealth`, `DbAdmin`, `Test`, `TestLog`, `UploadedFile`, `Store`, `ApiSimulation`, `Developers\DbTableDataView`. `Soap` and `Rest` are written to be called from PHP, not from a public route.

## Authentication details

Session keys on success: `username`, `role`, `isLoggedIn`, `userID`, `labelID` (empty string), plus company keys from `setCompanyUsersAndAdminSessionVars`.

`UserModel::getUser` joins `user_role` and left-joins `user_groups`, and exposes `users.id` as `userID`.

`JwtLibrary` builds HMAC-SHA256 JWTs. Default secret argument is the string `secret`. Used by `ApiSimulation`, which is not routed.

Password reset and 2FA tables: `password_reset` (no migration), `email_authentication_codes` (migration `2024-09-12-123957_AddTwoFactorAuthenticationTable.php`).

## Database

Migrations under `app/Database/Migrations` create:

| Table | Migration |
|---|---|
| `organisations` | `2022-11-20-124016_Organisations.php` |
| `users` | `2022-11-20-124016_Users.php` |
| `sessions` | `2022-11-20-124435_Session.php` |
| `user_role` | `2022-11-20-125608_UserRole.php` |
| `user_access` | `2022-11-20-125818_UserAccess.php` |
| `user_menu` | `2022-11-20-130307_UserMenu.php` |
| `user_submenu` | `2022-11-20-130307_UserSubmenu.php` |
| `user_menu_category` | `2022-11-25-102709_UserMenuCategory.php` |
| `media_file` | `2023-06-02-162422_MediaFile.php` |
| `cronjob_log` | `2023-06-02-163712_CronjobLog.php` |
| `cronjob_actions` | `2023-06-04-181855_CronjobActions.php` |
| `settings` | `2023-06-04-181945_Settings.php` |
| `language` | `2023-06-04-182128_Language.php` |
| `user_groups` | `2023-11-28-130041_UserGroups.php` |
| `label_email_template` | `2023-12-18-113954_LabelEmailTemplate.php` |
| `sent_queue` | `2024-01-18-092523_SentQeue.php` (filename spelling) |
| `public_holidays` | `2024-03-19-101249_AddPublicHolidaysTable.php` |
| `email_template` | `2024-07-09-081109_SystemEmailTemplates.php` |
| `email_template_types` | `2024-07-09-110123_AddTableEmailTemplateTypes.php` |
| `email_authentication_codes` | `2024-09-12-123957_AddTwoFactorAuthenticationTable.php` |
| `login_history` | `2024-09-18-132105_AddLoginHistoryTable.php` |
| `blocked_users_list` | `2024-09-20-084946_AddBlockedUsersListTable.php` |

Later migrations alter cron actions (`order`, and fields from `2024-07-24-073848`) and `sent_queue.send_date`.

`users` columns in the create migration include `fullname`, `username`, `password`, address fields, `postcode`, `phone`, `role`, `active`, and timestamps. The seeder inserts a subset of those columns. If the migration marks address columns as non-null without defaults, the seeder insert can fail. That nullability was not re-checked column by column beyond `fullname`, `username`, and `password` being present. **Requires Confirmation** if `db:seed users` succeeds on a fresh schema.

Tables referenced by code with **no** migration in this tree (not a complete SQL inventory): `company_users`, `companies`, `company_transport_methods`, `password_reset`, `uploaded_file`, `server_monitoring_log`, `site_health`, `test`, `test_log`, `app_session`, `files`, `label_note`, `blockedusers` (model `$table` only; queries use `blocked_users_list`).

Seeds: `Users` (also calls `UserAccess`, `UserMenu`, `UserMenuCategory`, `UserRole`, `Organisations`), plus `UploadedFile`. README tells the developer to run `php spark db:seed users` only.

`BlockedUsersModel::$table` is `blockedusers`. `getBlockedIP` and `addBlockedUser` use `blocked_users_list`.

No SQL in the repository enables row-level security.

## Cron and processes

`CronJobs::index` is an HTTP job runner. It is not a CodeIgniter command and it is not registered in the pipeline. Who calls `GET cronJobs` in production is **Unknown / Requires Confirmation**.

Runnable controller methods: `sendScheduledEmails`, `getAllUnsentEmails`, `sendEmailsViaCron`, `checkServerLogs`.

`php spark monitor:system` (`App\Commands\MonitorSystemUsage`) writes a pid file `writable/monitoring.pid`, loops forever, shells out to `top`, and inserts `server_monitoring_log` when CPU or memory crosses the threshold. The `unlink` after the loop is unreachable because the loop does not end.

`Email::sendEmailNotification` builds a message and `updateSentQue` inserts `sent_queue`. It also inserts `label_note` from the cron mail path (`CronJobs` around the sent-queue update). `label_note` has no migration here.

## Integrations

| System | Direction | Auth as coded | Notes |
|---|---|---|---|
| MySQL `wca_dms` | App reads and writes | `env` user `root` on localhost | Production credentials not in git except the template |
| SMTP `smtp.overdriveqa.co.za:587` | Outbound mail | Username `info@overdriveqa.co.za`. Password is committed in `app/Config/Email.php` | BCC `admin@overdrive.co.za` |
| OpenAI completions | Outbound | Bearer token committed in `app/Helpers/chatgpt_helper.php` | Model `text-davinci-003`. SSL verify off |
| Slack webhook | Outbound | URL committed in `app/Controllers/Slack.php` | `GET slack` is unauthenticated |
| Shoprite HTTP | Outbound GET and PUT | Basic auth from the method array. Contract id header hardcoded in `Rest.php` | SSL verify off. Bodies saved under `writable` |
| Pick n Pay SOAP | Outbound | Username and password from the method array, inside the SOAP header | Test/UAT by default; production URL switches WSDL |
| SonarQube | Pipeline | `SONAR_HOST_URL` and `SONAR_TOKEN` from repository variables | `sonar.projectKey=overdrive_wca_dms_01994a78-900c-4314-9c83-2866b9eecfd5` |

`env` comment placeholders, all commented: `OPENAI_API_KEY`, `SENDGRID_API_KEY`, `GOOGLE_MAP_API_KEY`, `SMTP_HOST`, `SMTP_USERNAME`, `SMTP_PASSWORD`. The live SMTP settings are in `Email.php`, not in those placeholders. `encryption.key` is set in the committed `env` file.

## Environment

`CI_ENVIRONMENT = development` and `CI_DEVELOPER = jaco` are set in `env`.

Session: database handler, table name via save path `sessions`, match IP, time to update 1440, regenerate destroy true.

`app.forceGlobalSecureRequests` is commented and false in the comment.

README Apache vhost listens on port 80, `ServerName adminOverdrive.local`, `Require local`, document root under `c:/wamp64/www/adminOverdrive/public`.

## Security notes for operators

These are observations, not a pentest.

- Login, grant, and password hashing are present.
- CSRF is off.
- Several URLs skip login, including cron and Slack.
- Production cron key is a placeholder constant.
- SMTP password, OpenAI token, Slack webhook, encryption key, and the seed password are in the repository.
- REST and SOAP disable TLS verification.
- JWT helper default secret is `secret`.
- `monitor:system` runs `shell_exec('top -b -n 1')`.
- Sonar exclusions drop controllers, models, filters, helpers, libraries, and views from analysis, and coverage is disabled. A green Sonar result would not mean those files were reviewed.

## Tests and quality

- `composer test` → `phpunit`.
- Suites in `phpunit.xml.dist`: directory `./tests`. Coverage includes `./app` except views and `Routes.php`.
- Application tests do not cover login, cron, or EDI.
- `tests/README.md` still describes PHPUnit 8.5.13. `composer.json` requires PHPUnit `^9.1`.
- Pipeline (`bitbucket-pipelines.yml`): clone depth full, on branch `main` only, one step, pipe `sonarsource/sonarqube-scan:3.0.2`, Docker service memory 3072 MB, cache `~/.sonar`. No test, build, or deploy step.
- `sonarqube_scan.bat` is present for a local scan. Its script body was not required to describe the pipeline.

## Local development commands (from README)

```text
copy env to .env
composer install
php spark db:create
php spark migrate
php spark db:seed users
php spark key:generate
php spark serve
```

README also says `composer update` on framework releases and `php composer install`. The working Composer invocation is `composer install`.

Documented local login URL: `http://localhost/wca_dms/`. Username is documented as `admin@overdrive.co.za`. The password is in the README and the seeder and is omitted here.

## Related documents in Silicon Ark

- `project-plan.md` — scope and status
- `prd.md` — behaviour and requirements
- `features.md` — feature inventory
- `business-rules.md` — rules and inferences
