# Technical documentation — edioverdrive (Transactflow)

Evidence date: 2026-10-16. Source: `https://bitbucket.org/overdrive/edioverdrive.git`, `main` at `a33b2b7883666a77dec925f0ba000642b1dd19ee` (2026-10-06). Shallow clone; history before that commit was not walked.

## What this repository is

A CodeIgniter 4 monolith. Composer package name `codeigniter4/adminstarter`, description `Transactflow`, homepage `https://www.overdrive.co.za`, license MIT. The web root is `public/`. CLI entry is `spark`.

## Stack

| Piece | Constraint on this commit | Notes |
|---|---|---|
| PHP | `composer.json`: `^8.4`, platform pin `8.4.0` | README and unreleased changelog say 8.3+. See conflicts. |
| Framework | `codeigniter4/framework` `^4.7` | |
| Database | MySQLi, port 3306, charset `utf8` / `utf8_general_ci` | `app/Config/Database.php`. Local onboarding uses MariaDB. Sample `env` database name `transactflow_qa_1`. |
| Tests DB | SQLite3 `:memory:`, prefix `db_` | `database.tests` group |
| Sessions | Database handler, table `sessions`, match IP, regenerate destroy | `env` keys `app.session*` |
| PDF | `mpdf/mpdf` `^8.1` | |
| Spreadsheets | `phpoffice/phpspreadsheet` `^5.6` | Used by `Car` and `MasterFile` |
| CSV | `league/csv` `^9.16` | |
| TOTP | `robthree/twofactorauth` `^3.0`, `bacon/bacon-qr-code` `^3.0` | |
| XML | `ext-simplexml` | |
| Tests | PHPUnit `^10.5.16` | `phpunit.xml.dist` schema 10.5 |
| Browser tests | Playwright under `e2e/` | `e2e/README.md` |

PHP extensions named in the README: intl, curl, json, mbstring, mysqlnd, xml. `ext-fileinfo` is suggested.

Dev dependencies: `fakerphp/faker`, `mikey179/vfsstream`.

Removed, per changelog: `aws/aws-sdk-php`, `orhanerday/open-ai`.

## Application layout

```text
app/Controllers     HTTP and cron entry points
app/Models          CI models
app/Services        SOAP, FTP, 2FA, reporting, documents, health digest
app/Libraries       PasswordPolicy, JwtLibrary
app/Helpers         menu, mail, encryption, slack, chatgpt, source file, user access
app/Filters         Authentication, Authorization, MaintenanceMode
app/Config          Routes, Filters, Database, App
app/Database/Migrations   182 files, dated through 2026-08
app/Database/Seeds
app/Views           Admin UI, including edioverdrive document screens
public              Front controller, assets, deploy.php
writable            Sessions, logs, keys, payload folders, UI exports
scope               Operations docs, flow maps, QA-to-prod checklists
tests               PHPUnit
e2e                 Playwright
tools               Post-upgrade verify, PhpSpreadsheet smoke
```

Auto-routing is enabled (`Routes.php` calls `setAutoRoute(true)`). Explicit routes cover login, cron, settings, documents, store API, and similar. Other public controller methods are also reachable by framework convention.

Default controller is `Home`. `/` is `Welcome::index`.

## Request pipeline

Global before-filters in `app/Config/Filters.php`:

1. `maintenance` — except `silicon-staff-login`, `getLogin`, `2fa-validation`, `confirmCode`, `cronJobs`.
2. `isLoggedIn` — except `/`, staff login, register, organisations and children, tracking pixel, 404, cron, `identity-unknown`, store-api and children, 2FA, password reset, slack.
3. `isGranted` — those login exceptions plus `home`, `blocked`, `Welcome/*`, `chatGPT/*`, the download-link getter, and orders-reporting data/populate.

CSRF and honeypot aliases exist. Both are commented out of the global filter lists. That is the current code, not a target design.

`Authorization` loads the menu by URL segment 1. No menu row redirects to `/`. No `user_access` row redirects to `/blocked`. Segment `orderResponses` with role 3 always redirects to `/blocked`.

## Authentication

- Session flag `isLoggedIn`.
- Login history statuses: 1 failed, 2 success, 3 logout, 4 2FA failed (`LoginHistoryModel`).
- Lockout: 5 status-1 or status-4 rows for the same email and IP in 5 minutes (`checkFailures`).
- Password rules: `App\Libraries\PasswordPolicy` (minimum 12, character classes, maximum 72 bytes).
- Password hashes: read prefers `user_passwords.password_hash`, falling back to `users.password`. Create, update, and reset write both.
- JWT: `App\Libraries\JwtLibrary`. Organisation login builds an HS256 payload with `name` and `exp` of now plus 6000 seconds. Store API methods check the bearer token themselves because the session filter skips `store-api`.
- TOTP secrets are encrypted (`encryption_helper`, `TwoFactorService`).
- Pending 2FA session TTL is 900 seconds.

Seeded role names, in insert order: Developer, Admin, Company Admin, Company User. Views that treat ids 1 and 2 as source-file viewers, and id 3 as blocked from order responses, match that order only if the live table was seeded that way. Role 7 appears in one label view and is not seeded.

## Cron engine

`GET /cronJobs` maps to `CronJobs::index`.

Checks, in order (scope doc `Task_CronJobs_Implementation_Overview.md`, matching controller constants):

1. Query parameters `key`, `action`, `override`.
2. Hour window from `CRON_DAILY_WINDOW_START_HOUR` (6) through 22:59.
3. In production, `key` must equal `SECURITY_KEY`. The constant is hardcoded. The value is omitted here.
4. `action=heartbeat` and `action=heartbeat_backup` return after heartbeat logic.
5. Active `cronjob_actions` rows, ordered by `order`.
6. The `function` column must name a real method. Recurrence uses `checkCronTiming` unless `note` contains `bypass` or `override=true`.

Log type ids: 9999 CronEngine, 9998 primary heartbeat, 9997 backup heartbeat, 9996 shutdown diagnostic. Engine stale threshold is 12 minutes. From 21:00, `notifyCronHealthDigest` may run an extra time.

Documented poke (change dated 2026-08 in the scope doc): Xneelo user crontab on host `dedi1534`, user `transhrdpd`, every 5 minutes for RUN ALL and every 12 minutes for primary heartbeat. cron-job.org is backup heartbeat only. URLs: `https://admin.transactflow.io/cronJobs` and `https://qa.transactflow.io/cronJobs`. The hosting panel GUI cannot schedule every 5 minutes. SSH notes are in `scope/transactflow-ssh-server-access.md`.

Manual run: `POST system_settings/runCronJob` calls `CronJobs::runJobById`. A session is required. The call ignores the active flag, the hour window, and recurrence.

Writable folders used as payload or export areas include `company_orders_xml`, `company_orders_json`, `company_invoices_xml`, `company_claims_xml`, `company_claims_json`, `front_end_exports`, `temp_export`, plus XML template folders. Cron also writes hourly JSON under the public error-log and success-log paths referenced in `CronJobs`.

## EDI processing shape

`GetXMLData` selects a parser from `docs_type` and transport type. The go-live policy names Folder as type 1 and API as type 2, and also lists FTP, Email, and ShopFlow.

Order save path named in the policy: `processOrder`, then `getOrderDetails*`, then `handleOrderProcessing`, then `saveOrderToDb`.

Outbound invoices are grouped by `invoice_name`. The seller transport must have the same `company_identifier`.

Order responses use `method_identifier` `orders_responses`. The DGB/Makro docs type string is `dgbmakroorderresponsexml`. Hub SOAP is `App\Services\MakroM2NorthSoapService`. Spark helper: `php spark makro:test-dgb-order-responses`.

`Orders::exportOrderResponsesViaCron` returns a notice. It is not a working exporter.

Partner flow maps (QA ids, 2026-07-06) start at `scope/company_flow_maps/README.md`.

## Data the product depends on

Not a full schema. Tables and groups the behaviour uses:

- `companies`, `company_stores`, `company_transport_methods`, `transport_method_options`, `vendor_numbers`
- `clients`, `clients_billing`
- `orders`, order line and party tables, `order_responses`
- `invoices` and invoice lines
- `claims`, claim items, claim item meta
- `products`, `csv_import_templates`
- `orders_reporting`, `invoices_reporting`
- `users`, `user_role`, `user_access`, `user_passwords`, login history, blocked users
- `cronjob_actions`, `cronjob_log`, `error_log`, the mail queue cron reads
- `settings` (maintenance flag, admin team email)
- menu tables behind `MenuModel`

Migrations are in `app/Database/Migrations/`. The latest filename seen is `2026-08-26-130000_AddAdminDetailToErrorLog.php`. Deploy checklists run `php spark migrate` by hand. The FTP pipeline does not.

## External integrations

| Integration | Code | Config |
|---|---|---|
| SMTP | mail helper, CI Email | `.env` `email.SMTPHost`, `email.SMTPUser`, `email.SMTPPass`, `email.SMTPPort`, `email.SMTPCrypto`, `email.fromEmail`, `email.fromName` |
| Slack | `slack_helper`, `Slack` controller, `public/deploy.php` | `slack.webhookUrl`, `deploy.slackWebhookUrl`, `deploy.secretToken` |
| OpenAI | `chatgpt_helper.php` via cURL | `openai.apiKey` |
| Makro M2North SOAP | `MakroM2NorthSoapService` | Transport endpoint and credentials |
| Other retailer REST/SOAP | `Rest`, `Soap`, `GetXMLData` | Per transport row |
| FTP | `FtpService` | Per transport row |
| Store API JWT | `Organisations`, `StoreApiController` | Organisation `name` and `api_key` in the database |
| Xneelo FTP deploy | Bitbucket Pipelines `git ftp push` | Pipeline variables, not in Git |
| cron-job.org | HTTP GET | Backup heartbeat only |

`salesforce_helper.php` and `africastalking_helper.php` do not execute. The SMS file comments contain credential-like text. That text is not copied here. Remove it from Git and rotate the value if it was ever a live key.

## Deployment

`bitbucket-pipelines.yml` uses image `atlassian/default-image:4`.

| Branch | What the script does |
|---|---|
| `main` | `git ftp push` to `$FTP_HOST` |
| `qa` | `git ftp push` to `$FTP_HOST_QA` |
| `storefront` | `git ftp push` to `$FTP_HOST_SHOPQA` |

Step titles say “Build and Test”. The scripts install zip and git-ftp and push. They do not run PHPUnit. The SonarQube pipe is commented out.

QA-to-production checklists: `scope/qa_to_prod_deployments/`. Local setup: `scope/project-local-setup-onboarding.md`. Sample virtual host: `edioverdrive.local` pointing at `public/`.

`CI_ENVIRONMENT` in the sample `env` is `development`. Production cron-key checks and `DBDebug` depend on the server `.env`, which is not in Git.

## Other background entry points

Spark commands under `app/Commands/` are operator tools: populate orders/invoices reporting, `MonitorSystemUsage`, Makro test commands, `BoxerFixtureAudit`, `JabStepsVerification`, health-digest and Niew report previews, `CleanupMakroTestOrders`, plus debug commands.

There is no queue-worker package. Mail is stored and sent by cron methods `getAllUnsentEmails` and `sendEmailsViaCron`.

## Security notes for reviewers

Observations from the tree, not a pentest.

1. `CronJobs::SECURITY_KEY` is committed. The value is omitted. If it is still the production key, the repository is enough to call `/cronJobs` on a reachable host.
2. `writable/keys/` contains `private.pem`, `public.pem`, and `generate_keys.php`. Confirm whether Git should store them.
3. `writable/front_end_exports/` contains generated PDFs. Some folder names look like email addresses. Contents were not copied here.
4. CSRF is off globally.
5. Auto-routing exposes controller methods that are not listed in `Routes.php`.
6. Routes `slack`, `organisations`, `store-api`, and `cronJobs` skip the session filter. Store API and organisation login check a JWT in the controller. Slack’s own check was not fully read. Treat that route as **Requires Confirmation**.
7. The committed `env` file has empty secret fields and a placeholder `encryption.key`. Real keys belong only in an uncommitted `.env`.

## Development and tests

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

`composer test` runs PHPUnit. Unit tests cover password policy, 2FA, credential expiry, Shoprite fixtures, spreadsheets, health, Boxer pack size, invoice reprocess names, cron errors, fault-finder mail, the document browser, dependency versions, and a billing zero-price preview.

`tests/feature/PublicRoutesSmokeTest.php` requests `/` and `/home` when a migrated test database exists; otherwise it skips.

`tools/post_upgrade_verify.sh` runs PHPUnit, a PHP syntax scan, Composer audit, `spark list`, spreadsheet smoke, curl, and optional Playwright.

## Conflicts inside the repository

| Topic | Code on this commit | Docs that disagree |
|---|---|---|
| PHP floor | `composer.json` requires `^8.4` | README says PHP 8.3 or newer. Changelog says the constraint was lowered to `^8.3`. |
| Buyer-seller matrix | Commit message and changelog discuss Boxer with Niew | Flow-map README (2026-07-06) lists Boxer sellers as Godrej and DGB only |
| Order-response export | Stub notice in `Orders` | DGB to Makro ingest is a real, separate path |
| Pipeline title | FTP push only | Step name says Build and Test |

## Unknown / requires confirmation

- Live production company ids, transports, and `clients` rows.
- Whether managed servers run PHP 8.3 or 8.4.
- Password-rotation age in days, and the QA 2FA grace length.
- Whether role 7, the Car controllers, Labels, and the `storefront` branch are still in use.
- Whether the committed cron key and PEM files are still live.
- Confluence space `AO` and Jira project `SI`, linked from the README, were not fetched.
