# Technical documentation — edi_system (SAWIS Label System)

Evidence date: 2026-10-16.

Source: `git@bitbucket.org:overdrive/edi_system.git`, `master` at `75a67b5`.

Secrets are committed in this repository. This document names the files and the kind of secret. It does not copy passwords, tokens, webhook URLs, or keys.

## Stack

| Item | Evidence |
|---|---|
| Language | PHP `^8.0` (`composer.json`, `README.md`) |
| Framework | `codeigniter4/framework` `^4.0` |
| Web root | `public/` (`README.md`, `public/index.php`) |
| Database | MySQL via MySQLi. Template database name `labelsystem` (`env`). Tests use SQLite memory (`app/Config/Database.php` group `tests`) |
| PDF | `mpdf/mpdf` `^8.1` for certificates. Imagick for upload preview |
| Spreadsheets | `phpoffice/phpspreadsheet` `^1.25`, used by `Car.php` and `MasterFile.php` |
| HTTP client | cURL in the ChatGPT helper and deploy scripts. AWS SDK is required and unused under `app/` |
| OpenAI | `orhanerday/open-ai` is required. `chatgpt_helper.php` does not use that library; it calls `https://api.openai.com/v1/completions` with cURL |
| Tests | PHPUnit `^9.1` (`phpunit.xml.dist`) |
| Licence | MIT, CodeIgniter Foundation copyright, in `LICENSE` |
| Package name | `codeigniter4/adminstarter` |

`README.md` also lists ext-intl, ext-mbstring, ext-mysqlnd, ext-json, ext-xml, and libcurl. Imagick and Ghostscript are required by the upload path and are not in that list.

## Application structure

```text
app/Config          CodeIgniter config. Routes.php and Filters.php are the live files
app/Controllers     Page and API controllers. Labels.php is the label workflow
app/Models          CI models and query helpers
app/Views           SAWIS layouts, label screens, starter screens, embedded Adminer
app/Filters         Authentication, Authorization. Cors.php exists and is not aliased in Filters.php
app/Helpers         mail, menu, user access, chatgpt, salesforce (empty), africastalking (commented)
app/Libraries       JwtLibrary.php
app/Database        Migrations and seeds
app/Common.php      Label display helpers and tamper check
public              Front controller, assets, deploy scripts, info.php
tests               Framework health test and examples
writable            cache, logs, session placeholders
```

Auto-routing is enabled. Explicit routes in `app/Config/Routes.php` cover pixel tracking, login, register, home, ChatGPT, user-admin posts, system settings, label status, groups, email templates, menu-management posts, the organisations resource, and user list. Label workflow methods (`labels/createLabel`, `labels/approveLabel`, and the rest) are reached by auto-routing to `App\Controllers\Labels`.

`app/Config/Routes.php` calls `setDefaultNamespace` after the pixel routes are registered. Those two pixel routes are registered first, against whatever default namespace was already in force.

Duplicate files `Users_old.php`, `Filters_old.php`, `Routes_old.php`, and `Kint_old.php` sit beside the live files. The autoloader resolves `Users` and `Filters` from the filenames without the `_old` suffix. `Routes.php` does not include `Routes_old.php`.

There is no `app/Commands` directory and no `spark` custom command.

## Configuration and environments

`CI_ENVIRONMENT` in the committed `env` is `development`. `CI_DEVELOPER` is `jaco`. Boot files exist for development, production, and testing under `app/Config/Boot/`.

`env` (template, mostly uncommented for the lines that matter):

| Key | Committed value |
|---|---|
| `app.baseURL` | `http://localhost/label_system/` |
| Session | Database handler, cookie `ci_session`, save path `sessions`, match IP true, time-to-update 1440, regenerate-destroy true |
| Database | host `localhost`, database `labelsystem`, user `root`, empty password, driver MySQLi |
| `encryption.key` | A hex key is present. Value omitted |

`app/Config/App.php` defaults, used when `.env` does not override them:

| Key | Default in App.php |
|---|---|
| `baseURL` | empty string |
| `appTimezone` | `America/Chicago` |
| Session | file handler path under `writable/session`, expiration 7200, match IP false, time-to-update 300, regenerate-destroy false |

`app/Config/Database.php` default group has empty username, password, and database name, so a running app depends on `.env`. Two extra groups, `sawisAPIDev` and `sawisAPIQa`, point at private hosts `10.0.0.145` and `10.0.0.108`, database `sawis_api`, with usernames and passwords in the PHP file. Login uses `sawisAPIQa` only. The dev group is referenced in a comment.

`app/Config/Email.php` sets protocol `smtp`, host `smtp.overdriveqa.co.za`, user `admin@overdriveqa.co.za`, port 587. The password is in that file and is omitted here.

Commented keys in `env` with empty values: `OPENAI_API_KEY`, `SENDGRID_API_KEY`, `GOOGLE_MAP_API_KEY`, `SMTP_HOST`, `SMTP_USERNAME`, `SMTP_PASSWORD`. The ChatGPT helper and the email config do not read those keys.

## Data model

Migrations live in `app/Database/Migrations/`. Table names below are the forge names (no DB prefix is set).

### Identity and menus

| Table | Migration |
|---|---|
| `users` | `2022-11-20-124016_Users.php`, later `UsersExtraField` |
| `user_role` | `2022-11-20-125608_UserRole.php` |
| `user_access` | `2022-11-20-125818_UserAccess.php` |
| `user_menu`, `user_submenu`, `user_menu_category` | November 2022 menu migrations |
| `organisations` | `2022-11-20-124016_Organisations.php` |
| Session table | `2022-11-20-124435_Session.php` |

`users` seed columns used by code: `fullname`, `username` (email), `password`, `role`, `id`.

### Labels

| Table | Role |
|---|---|
| `label` | One submission. Columns from the October 2023 migration plus `agent_id`, `trademark`, `meeting_group_id`, `provisions`, `cc_list` |
| `file` | PDFs and the certificate. `type`, `file_version`, `file_approved`, `active`, `path` |
| `label_note` | Free-text notes. Code also writes `label_note_id` back onto the new row |
| `label_log` | `action`, `submission`, `label_note_id`, `updated_by`, `active` |
| `label_status` | `name`, `priority`, `provision_id`, later `status`, `group`, `parent` |
| `label_provision` | Provision names |
| `label_email_template` | `type`, `body`, `active`, `sent` |
| `sent_queue` | `to`, `from`, `subject`, `body`, `label_id`, `label_email_template_id`, `processed`, later `tracking_code` and `send_date`. Class and file spell the queue "Qeue" |
| `meeting_groups` | `name`, `type`, `active` |
| `user_groups` | Migration `2023-11-28-130041_UserGroups.php` |

`label.alcohol_percentage` starts as INT and is altered to `DECIMAL(10,2)`.

Create inserts a subset of `label` columns. See FR-012 in `prd.md`.

### Other modules

Car and session tables (`car`, cost categories A/B/C, `car_tier`, `car_type`, `car_service`, `app_session` and several column-add migrations), `master_file`, `master_file_overland`, `uploaded_file`, `media_file`, `site_health` (model `SiteHealthModel`), `test`, `test_log`, settings, language, cronjob log, cronjob actions. Those cronjob tables are created by migration. No PHP writer for them was identified in the label path.

Seeds that `Users` seeder calls: `UserAccess`, `UserMenu`, `UserMenuCategory`, `UserRole`, `Organisations`, `CarType`, `UploadedFile`. `Holidays.sql` is in the seeds folder and is not invoked by the `Users` seeder class.

## Auth

Session auth is `app/Filters/Authentication.php`: redirect to base URL when `session('isLoggedIn')` is not true.

Menu auth is `app/Filters/Authorization.php`: `MenuModel::getMenuByUrl(first segment)` then `UserModel::checkUserAccess(role, menu id)`.

Organisations and `ApiSimulation` use `JwtLibrary` bearer checks. `generate_jwt` / `is_jwt_valid` take a secret argument that defaults to `secret`. Expiry is read from the payload `exp` claim. Organisation login sets `exp` to `time() + 6000`.

SAWIS login builds SQL by concatenating the submitted email and numeric user id into the query string in `Welcome::getHasLoginFromSawisOnline`. That is implemented behaviour. This document does not repeat a payload.

`public/info.php` is `phpinfo()`. `app/Views/projects/adminer.php` is a full Adminer script inside the view tree. Both are reachable as files or screens if the web server and menu grants allow them.

`labelFormsTamperProtection` in `app/Common.php` compares session ids to posted ids and `die()`s on mismatch.

## Email, files, and jobs

No queue worker, cron entry point, or scheduler config is in the repository. `sent_queue.processed` changes only in:

- insert (`0`) — `Email::updateSentQue`
- manual send (`1`) — `Labels::sendEmailNow`
- pixel (`2`) — `EmailPixelTrackingModel::logEmailPixel`

`sendMail` (`app/Helpers/mail_helper.php`) uses `Config\Services::email()`, sets from, to, subject, and HTML body, calls `send()`, then `printDebugger(['headers'])`.

Uploaded label PDFs are moved with `move_uploaded_file` to `uploads/` relative to the working directory. On Windows, `Labels` appends `C:/Program Files/gs/gs10.02.1/bin` to `PATH` and, for local development, appends `/public/` to `DOCUMENT_ROOT` before Imagick reads the file. Imagick settings: resolution 300, compression quality 100, first page only, JPEG output.

Certificates are written by mPDF to `uploads/approvalcerts/` with mode `0777` if the directory is created by the request.

## External systems

| System | Where configured | Used by |
|---|---|---|
| SAWIS `sawis_api` | `Database::$sawisAPIQa` (live call) and `$sawisAPIDev` (unused except a comment) | Login |
| SAWIS Online HTTP | `sawis-menu.php` base URL switch | Browser links only |
| SMTP overdriveqa | `Config\Email` | Manual label email |
| OpenAI completions | Hardcoded in `chatgpt_helper.php`. SSL verify host and peer are 0 | `ChatGPT::sent` |
| Slack | Hardcoded in `public/deploy.php` | Deploy script |
| Internal deploy URL | `public/master_deploy.php` calls `https://10.0.0.58/deploy.php` with a token query parameter | Case matched on `tkn` |

`master_deploy.php` allows a list of CIDR ranges (Bitbucket's published ranges are the shape of that list) and switches on the token. One case calls the internal deploy URL. `deploy.php` checks a token and a smaller IP list, posts to Slack, and runs `/usr/bin/git pull`.

Tokens and the Slack credential are in those PHP files. They are omitted here and should be treated as exposed, because they are in Git.

## Deployment and CI

Bitbucket Pipelines (`bitbucket-pipelines.yml`):

- Image `maven:3.3.9` (a Java/Maven image, not a PHP image).
- On branch `master` only.
- One step: `sonarsource/sonarqube-scan:2.0.1` with `SONAR_HOST_URL` and `SONAR_TOKEN` from repository variables.
- Clone depth full.
- `sonar.projectKey` in `sonar-project.properties` starts with `overdrive_label_system_`.

The pipeline does not run Composer, migrations, or PHPUnit.

`README.md` documents a Windows Apache vhost `adminOverdrive.local` with document root `public`, and `php spark serve` as the other run option. `sonarqube_scan.bat` is present for a local scan. Its body was not required to establish the pipeline path.

`builds` at the repository root is a file from the replica commit, not a directory of artefacts.

## Security notes (implemented, not a pentest)

These are visible from reading the tree. They are listed so a reviewer can confirm impact. No exploit steps are included.

- Database passwords, SMTP password, OpenAI bearer token, Slack webhook, deploy tokens, JWT default secret, encryption key, and the seed user's password are in Git.
- SAWIS login concatenates request input into SQL.
- `public/info.php` exposes `phpinfo()`.
- Adminer is stored in the application views.
- CSRF global filter is off.
- ChatGPT cURL disables SSL verification.
- `deploy.php` and `master_deploy.php` disable SSL peer verification on their outbound calls.
- Upload filenames keep the original name after a numeric prefix.
- Approval-certificate directory is created world-writable (`0777`).
- Auto-routing plus a public `Labels::test` and `checkPDFValidation` exposes debug methods to any role that passes the `labels` menu grant.
- Organisation JWT secret defaults to a short static string.
- Tracking pixel and organisations routes skip the session filter.

## Development and tests

Setup commands **documented** in `README.md`:

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

The seeder class is `Users`. CodeIgniter's spark seeder match is case-sensitive on some installs; the README uses lowercase `users`. **Requires confirmation** on the team's PHP build.

Tests under `tests/`:

| Test | What it asserts |
|---|---|
| `tests/unit/HealthTest.php` | `APPPATH` is defined; base URL is set in `.env` or config |
| `tests/database/ExampleDatabaseTest.php` | Framework example |
| `tests/session/ExampleSessionTest.php` | Framework example |

`tests/README.md` is the CodeIgniter test readme. No label, login, or permission test exists. CI does not execute this suite.

`phpunit.xml.dist` bootstraps CodeIgniter's test bootstrap, covers `./app` except views and `Routes.php`, and writes clover, HTML, and JUnit under `build/logs/`.

## Monitoring

No application monitoring SDK was found. SonarQube is the only automated analysis. Site health is a screen (`SiteHealth` controller) backed by a model; it is not an external uptime check. Cronjob tables exist without a matching runner in this tree.

## Related Silicon Ark note

`wca_dms` is a different repository. It mentions an "EDI Overdrive" view folder. That fact is not evidence that `edi_system` implements EDI. This tree was inspected on its own.
