# Technical documentation — ipw-php

Evidence date: 2026-10-11. Source: `git@bitbucket.org:overdrive/ipw-php.git`, `main` at `054883d9` (2026-09-28, “auditor score alignment”).

History inspected: the latest 80 commits (2026-08-27 through 2026-09-28). The onboarding note named `eda1a748` (2026-09-23). `main` is one commit ahead of that note.

This file describes the CodeIgniter 4 tree. `knowledgeBase/Overview.md` describes an older `livesite/` and `system/` layout that is not the tree that was checked out.

## Stack

| Item | What the repository shows |
|---|---|
| Language | PHP. `composer.json` requires `^8.0`. `README.md` says 8.3 is required. Bitbucket Pipelines uses the image `php:8.3-cli` for the scoring test step |
| Framework | CodeIgniter 4 (`codeigniter4/framework` `^4.0`). Front controller pattern via `spark` and `public/` |
| Package name | `codeigniter4/adminstarter`. Description field still says `Transactflow`. Homepage `https://www.overdrive.co.za` |
| Application database | MySQLi. The `env` template sets host `localhost`, database `adminoverdrivev2`, user `root`, empty password. Those are starter defaults, not the production inventory |
| BIS database | SQL Server reached by ODBC (`odbc:Driver={SQL Server};...;Database=bislive`) or by the standalone HTTP API. `BisService` forces the HTTP API path |
| PDF | `mpdf/mpdf` `^8.1` |
| Spreadsheets | `phpoffice/phpspreadsheet` `^5.0`. README still says `^1.25` |
| CSV | `league/csv` `^9.16` |
| AWS | `aws/aws-sdk-php` `^3.254` is required. Call sites were not inventoried |
| OpenAI client library | `orhanerday/open-ai` `^3.5`. The helper that was read uses raw cURL, not that library |
| Tests | PHPUnit 9. Suites `App` and `Scoring` in `phpunit.xml.dist` |
| CI | `bitbucket-pipelines.yml`: on `main` and on every pull request, run the Scoring PHPUnit step, then SonarQube scan `sonarsource/sonarqube-scan:3.0.2` |
| Web server notes | README documents an Apache virtual host with `DocumentRoot` at `public/`. Pipeline memory for Docker is 3072 MB |

**Requires Confirmation:** the PHP version and database name actually used on `ipw.co.za` and on staging.

## Repository layout

```text
app/                 Controllers, Models, Services, Config, Filters, Helpers, Database, Views, Commands
public/              Web root, compiled assets, uploaded audit files under assets/content/audit/
writable/            CI4 logs, cache, session files when file sessions are used
tests/               PHPUnit, plus e2e-nav-audit
standalone-api/      BIS HTTP API with no CodeIgniter dependency
scope/               Client and developer scope, parity audits, task notes
knowledgeBase/       Mixed: some pages match the CI4 app, some describe the legacy site
docs/                testing notes and a SQL baseline
spark                CLI entry
env                  Environment template (copy to .env; .env is not the template)
composer.json
bitbucket-pipelines.yml
phpunit.xml.dist
sonar-project.properties
db_schema.sql
```

Controllers worth knowing first:

| Area | Controller |
|---|---|
| Sign-in and registration | `Welcome` |
| Public pages | `Pages`, `Content`, `LegacyRedirect` |
| Users, groups, SAWIS numbers | `Users`, `SystemUsers`, `Api\User` |
| Evaluations | `MemberEvaluations`, `EvaluationForms`, `EvaluationFormTemplate` |
| Audits, reports, compliance, probation, certificates, documents, lab, SO2 | `Audit`, `Reports`, `Compliance`, `Probation`, `Certificates`, `DocumentManagement`, `LabAnalysis`, `So2Reports` |
| Settings and mail | `SystemSettings`, `Email`, `EmailPixelTracking` |
| Developer utilities | `Developers\CRUDGenerator`, `MenuManagement`, `DbTableDataView` |

Domain services live in `app/Services/`. Scoring policy is configuration, not a service: `ScoringRules`, `CellarTypes`, `LegacyScoringMatrix`, `SprayProgrammeScores`.

## Request pipeline

`app/Config/Filters.php` global order:

Before the controller:

1. `maintenanceMode`
2. `language`
3. `isLoggedIn` except `PublicRoutes` patterns
4. `routePolicy`
5. `isGranted` except public routes plus `authorizationExcept`
6. `hiddenStaffPages`
7. `requestTiming`

After the controller:

- Debug toolbar, except chat GPT, document download, and evaluation attachment routes
- `noSearchEngineIndex`
- `noCacheHeaders`
- `requestTiming`

CSRF and honeypot aliases exist. Both are commented out of the global before-list. That is the current configuration, not an oversight to “fix” inside documentation.

`Routes.php` sets `setAutoRoute(true)` and a 404 override of `LegacyRedirect::notFound`. The comment in that file says the override must be a `[class, method]` array. A `"Class::method"` string is ignored by the installed CI4 version.

Public route patterns are listed in `app/Config/PublicRoutes.php` and must stay aligned with `Routes.php`. Staff bookmarks `certified` and `certified-farms` are intentionally absent from the public list.

## Authentication and authorisation

- Session filter: `app/Filters/Authentication.php`.
- Menu filter: `app/Filters/Authorization.php`.
- Route policy: `app/Filters/RoutePolicy.php` with matrices in `RoutePolicy.php` and `RoutePolicyGrants.php`.
- Coarse roles: `user_role` via `Database/Seeds/UserRole.php`. Ids already in the database are preserved; the seed only inserts missing names.
- Fine roles: `rbac_roles` referenced by name (System Developer, IPW Administrator, IPW Data Capturer, IPW Auditor, IPW Lead Auditor).
- Member object scope: selected group in session. Helpers `is_member_user()`, `get_selected_member_group()`, `member_any_privilege_allowed()`.
- User class 2 is the member class (`ScoringRules::MEMBER_USER_CLASS_ID`).

MFA routes are commented out.

`app/Libraries/JwtLibrary.php` exists. It was not traced to a public API authentication scheme in this review. **Unknown** whether JWT is used by any current client.

## Sessions, cookies, and maintenance

The `env` template includes the usual CI4 keys: `app.sessionDriver`, cookie name, CSRF names, `app.maintenanceMode`, `app.routePolicyMode`, `encryption.key`. Values in production are not in git. The template’s database password is empty and must not be assumed for any shared environment.

Maintenance mode is both a filter and `App\Services\MaintenanceService`, with `MaintenanceSessionEviction`.

## Persistence

- Models under `app/Models/` map the operational tables (`group_eval_form`, `group_eval_form_item`, `group_eval_form_appendix`, `group_audit`, `group_probation`, `sawis_compliance`, users, RBAC, documents, control products, and others).
- Migrations under `app/Database/Migrations/` are how schema changes are intended to ship. `MIGRATION_DEPLOYMENT_GUIDE.md` (2025-12-18) says a server whose database was not created by CI4 migrations needs a one-time `php spark migrate:sync --force` before `php spark migrate`, otherwise CI4 will try to create tables that already exist.
- `db_schema.sql` is a schema snapshot in the repository root. It was not diffed against the migrations in this review. If they disagree, the applied production database is the authority, and both files need a human check.
- `group_probation.group_eval_form_id` is unique. There is no foreign key; the probation note says legacy data was not rewritten to add one.
- Farm Table B uses `eval_form_part` flag added by `2026-09-09-090000_AddUnlocksFullTotalToEvalFormPart.php`. Base total is `eval_form.total_score`. Full total is `total_score_w`.
- Appendix 2B section column: `2026-07-22-073200_AddAppendixSectionToGroupEvalFormAppendix.php`.

CLI commands in `app/Commands/`:

| Command class | Role |
|---|---|
| `ProcessProbation` | `probation:process`, supports `--dry-run` |
| `SyncSawisFarms` | Pull farm data from BIS into the local store |
| `BackfillPassCompliance`, `BackfillSubmissionScoreRollup`, `BackfillTableBRollup` | Score/compliance repair. Table B backfill does not change status |
| `SyncMigrations`, `RunMigration` | Migration tracking repair |
| `RbacPolicyAudit` | Report grant drift |
| `CheckCellarBottlerCompliance`, `CheckLocalCellarBottler` | Cellar/bottler checks |
| `EnsureNavMatrixUsers` | Test-user setup |
| `MonitorSystemUsage` | Usage monitor |
| `TestBisConnection` | BIS connectivity |

## BIS integration

Two implementations:

1. `app/Services/BisService.php` — used by the CI4 app. The constructor sets `$this->useApi = true` unconditionally (the staging check is commented). HTTP base URL comes from `BIS_API_URL`, defaulting to a SAWIS host. API key comes from `BIS_API_KEY`. The class still contains an ODBC PDO setup using `BIS_CONNECTION_STRING`, `BIS_USERNAME`, and `BIS_PASSWORD`, with defaults in source.
2. `standalone-api/` — a small PHP front controller (`index.php`, `BisService.php`, `config.php`) that talks ODBC to BIS and authenticates callers with a static API key (`X-API-Key` or `Authorization: Bearer`). `standalone-api/README.md` describes the endpoints. `TROUBLESHOOTING.md` is beside it.

`BIS_INTEGRATION_README.md` is a query catalogue for the old `system/BIS.php` and `livesite/` pages (member profile farm vs cellar, `FARM_MASTER`, `IPWSO2`). Use it to understand which BIS tables the business reads. Do not treat its file paths as the CI4 design, and do not copy the credentials printed in that file into tickets or new docs.

SSL verification for the BIS API defaults to on when `ENVIRONMENT` is `production`, and off otherwise, overridable with `BIS_API_SSL_VERIFY` and `BIS_API_CA_BUNDLE`.

**Security note for operators:** default BIS credentials and a default API key are present in source, and `chatgpt_helper.php` contains a hardcoded OpenAI bearer token. A commented Africa’s Talking sandbox key is also in `africastalking_helper.php`. This knowledge base deliberately does not repeat those values. They should be rotated, removed from the tree, and supplied only through environment configuration. Whether the checked-in values are still accepted by the remote services is **Unknown / Requires Confirmation**.

## Mail, SMS, and other outbound calls

| Path | Behaviour |
|---|---|
| `app/Helpers/mail_helper.php` | `sendMail()` builds an HTML body and sends through CI4’s email service. `env` lists `SMTP_HOST`, `SMTP_USERNAME`, `SMTP_PASSWORD`, and `SENDGRID_API_KEY` |
| `app/Helpers/chatgpt_helper.php` | POST `https://api.openai.com/v1/completions`, model `text-davinci-003`, SSL peer verification disabled in the helper. Route `chatGPT` |
| `app/Helpers/slack_helper.php` | Loaded for every request via `BaseController`. Not fully traced |
| `app/Helpers/salesforce_helper.php` | `salesforceConnect()` is an empty stub; SDK calls are commented |
| `app/Helpers/africastalking_helper.php` | `smsMessage()` is entirely commented. SMS is not sent |
| `EmailPixelTracking` | `GET tracking-pixel` |

No WhatsApp client was found under `app/`.

## Files and public storage

Uploaded audit documents are under the web root: `public/assets/content/audit/<year>/<month>/`. Download goes through `DocumentManagement::download` and `document_file_stream_response()`. Because the files are under `public/`, anyone who can guess or learn a direct asset URL may be able to fetch them without the UUID controller. Whether the web server blocks direct access to that folder was not verified. **Requires Confirmation.**

`FileServer` also serves legacy images and `assets/content/pdfs/`. Several `content/*` prefixes are public in `PublicRoutes.php`, including `content/audit/*`. That public pattern is broader than the authenticated UUID download. **Requires Confirmation** that this does not expose member uploads.

## Front end

The README says Bootstrap 5 is bundled in `public/assets/js/app.js` and that a second Bootstrap copy must not be loaded from a CDN. Primary colour used for audit export actions is `--bs-primary: #6b8e23`.

Views are server-rendered CI4 views with DataTables-style POST parameters on submission lists (`draw`, `start`, `length`, `search`).

## Testing and quality

Pipeline on `main` and pull requests:

1. PHP 8.3 image, install `zip`, `mysqli`, `intl`, `gd`, `mbstring`.
2. `composer install`.
3. `php -l app/Controllers/MemberEvaluations.php`.
4. `vendor/bin/phpunit --testsuite Scoring`.
5. SonarQube scan using `SONAR_HOST_URL` and `SONAR_TOKEN` repository variables.

The `App` PHPUnit suite is not the pipeline gate. Local full-suite status is **Unknown**.

Browser and user-test write-ups live in `BROWSER_TEST_RESULTS.md`, `FRONTEND_TESTING_PLAN.md`, `USER_TESTING_GUIDE.md`, and `scope/`. They are evidence of past test plans, not of a run on 2026-10-11.

`docs/testsprite_run.md` records TestSprite notes inside the application repo.

## Deployment

`MIGRATION_DEPLOYMENT_GUIDE.md` is the only deployment procedure checked into the repo. It tells operators, after a merge:

1. Pull `main` (or the staging branch) on the server.
2. First time on an existing database: `php spark migrate:sync --force`.
3. Then `php spark migrate` (the guide’s example of `migrate:run AddPassingScoreFieldsDE` is specific to the December 2025 change).
4. `php spark migrate:status`.

It does not name the host, the process manager, or how `public/` is published. Staging hostname `staging.ipw.co.za` appears in `BisService::isStagingEnvironment`. The public site name `ipw.co.za` appears in scope comparison docs. How code is copied to those hosts (git-ftp is hinted by `.git-ftp-ignore`, or another path) is **Unknown / Requires Confirmation**.

Probation’s daily run is an operational Task Scheduler entry, not a row in `bitbucket-pipelines.yml`.

## Local setup as documented

From `README.md`:

1. Copy `env` to `.env` and set `app.baseURL` and database settings.
2. `composer install`.
3. `php spark db:create`, `php spark migrate`, `php spark db:seed users`, `php spark key:generate`.
4. `php spark serve`, or an Apache vhost aimed at `public/`.

`db:create` against a copy of production is not what the migration guide says to do. Use `migrate:sync` on an existing database.

## Configuration keys (names only)

From the `env` template: `CI_ENVIRONMENT`, `CI_DEVELOPER`, `OPENAI_API_KEY`, `SENDGRID_API_KEY`, `GOOGLE_MAP_API_KEY`, `SMTP_HOST`, `SMTP_USERNAME`, `SMTP_PASSWORD`, `app.baseURL`, `app.forceGlobalSecureRequests`, `app.maintenanceMode`, `app.routePolicyMode`, session and cookie keys, CSRF keys, CSP keys, `BIS_API_URL`, `BIS_API_KEY`, `BIS_CONNECTION_STRING`, `BIS_USERNAME`, `BIS_PASSWORD`, `BIS_API_CONNECT_TIMEOUT`, `BIS_API_SSL_VERIFY`, `BIS_API_CA_BUNDLE`, `database.default.*`, `encryption.key`, honeypot keys, logger threshold.

`GOOGLE_MAP_API_KEY` is named. A map feature was not traced. **Unknown** whether maps are still used.

## Related reading inside the source repo

Prefer these when they agree with code:

- `README.md` (feature bullets and document-storage notes)
- `knowledgeBase/ProbationManagement.md`
- `knowledgeBase/RBAC_Implementation.md` (role names; confirm ids on a real database)
- `scope/IPW_Audit_Platform_Scope.md` (January 2025 plan, not a statement of 2026 completion)
- `MIGRATION_DEPLOYMENT_GUIDE.md`
- `standalone-api/README.md`

Treat these as historical:

- `overview.md`
- `knowledgeBase/Overview.md`
- `knowledgeBase/RolesAndInteractions.md`
- `knowledgeBase/BusinessRules.md`
- `knowledgeBase/Certification.md`
- `BIS_INTEGRATION_README.md` (query catalogue only)
