# Technical documentation — por (Purchase Order Requisition)

Evidence date: 2026-10-06.

Source: `git@bitbucket.org:overdrive/por.git`, `master` at `6fabdc090d2a3693f369f9fe50dea47990109349` (2026-06-23, `#88_po system Key account Fix #xx_Contact information save Fix`). Default branch inspected: `master`. The clone was depth 1, so parent history was not listed.

The scan-host path `~/projects/sonar_scan/repos/por` was not on the machine that produced this document. Secret values are omitted.

## Architecture

Server-rendered PHP on IIS. One shared ODBC connection per request. HTML is concatenated in page scripts and classes. State is a PHP session plus SQL Server.

```text
index.php -> login.php
                 |
                 v
         $_SESSION['USER']  (mstUser + mstUserGroupPOR)
                 |
     +-----------+-----------+------------+
     |           |           |            |
requisition   receipts    vendors     admin pages
Form + list   Form + list Form + list imports, reports, settings
     |           |           |            |
     +-----------+-----------+------------+
                 |
         include/system.inc.php
         SecureConfig -> PorOdbc -> SQL Server
                 |
         sysCronJob  (written here, not consumed)
         include/documents/  (uploads)
```

`ajax.functions.php` is a separate endpoint on the same bootstrap. It returns XML and does not render the shell.

## Repository layout

| Path | What it is |
|---|---|
| `index.php` | Redirect to `login.php` |
| `login.php`, `logout.php` | Sign-in and sign-out |
| `requisition.php`, `requisitionForm.php` | Live purchase-order list and form |
| `receipts.php`, `receiptsForm.php` | Live receipt list and form |
| `vendors.php`, `vendorForm.php` | Live vendor list and form |
| `import.UGAccess.php`, `import.UGGLCodes.php` | Excel imports |
| `report.php`, `settingsForm.php` | Reports and `sysQuery` editor |
| `ajax.functions.php` | XML lookups |
| `include/system.inc.php` | Bootstrap: errors, timezone, ODBC, session |
| `include/SecureConfig.php` | `.env` and connection parsing |
| `include/system.functions.inc.php` | Login, filters, UG access |
| `include/*.cls.php` | Domain classes |
| `include/database/` | `PorOdbc` and `SqlObject` actually included |
| `include/requisition.query.filters.inc.php` | List filters and page size |
| `include/requisition.exports.inc.php` | AP export and CSV helper |
| `include/layout.*` | Header, menu, full page, login layout |
| `database/` | Second copy of the ODBC classes. Not included by bootstrap |
| `ldap/` | Third-party adLDAP. Not bound by `loginUser` |
| `PHPExcel/` | Legacy library. Reports do not load it |
| `vendor/` | Composer install, including PhpSpreadsheet |
| `sql/recommended_performance_indexes.sql` | Optional index script. Not applied by the app |
| `Sessions/` | Directory name in the tree. Runtime sessions are configured under the IIS document root, not necessarily this folder |
| `web.config` | IIS FastCGI PHP handlers and default document `index.php` |
| `composer.json` | `phpoffice/phpspreadsheet` `^4.3` |
| `README.md`, `CHANGELOG.md`, `.env.example` | Setup and the unreleased security note |
| `sonar-project.properties` | Sonar project key `por` |

Dated and `*.maanie.php` / `*.pj.php` files are leftovers. See `features.md`.

## Runtime

- Language: PHP. `SecureConfig` comments say PHP 7.0 or newer (no typed properties). `web.config` registers FastCGI for `C:\php8.4.8\php-cgi.exe`, `C:\PHPv8.2\php-cgi.exe`, `C:\PHPv8.4\php-cgi.exe`, `C:\php-8.4.8\php-cgi.exe`, and `C:\PHP\php-cgi.exe`. **Unknown** which handler the site uses.
- ODBC extension required (`odbc_connect`). README names the IIS pool `rgbc_por_qa` as an example to restart.
- Time zone: `Africa/Johannesburg`.
- `display_errors` is `1`. `error_reporting` is `E_ALL` without notices and warnings.
- Session GC lifetime `21600`. Save path is `{DOCUMENT_ROOT}\rgbc_por\Sessions`.
- UI: HTML tables, jQuery 1.9.1 and jQuery UI 1.9.1 from Google, Select2 4.0.3 from cdnjs, local CSS under `include/style/`.
- Cache headers on the header include: `no-cache`. Content type `text/html; charset=utf-8`.

There is no framework, no router, and no automated test suite in the tree.

## Configuration

Prefer the split variables (README and `.env.example`). If all four are set, they win over the single connection string.

| Variable | Role |
|---|---|
| `RGBC_POR_ODBC_DSN` | ODBC data source. Example `rgbc_php` |
| `RGBC_POR_ODBC_DATABASE` | Second connect / catalog. Example `Superbowl`. Comment: legacy used the name Superbowl |
| `RGBC_POR_ODBC_USER` | Example `rgbc_php` |
| `RGBC_POR_ODBC_PASSWORD` | Required non-empty when split vars are used. Quotes required when the value contains `!` or `;` |
| `RGBC_POR_CONNECTION_STRING` | Alternative. Keys Data Source, Initial Catalog, User ID, Password, with aliases server, database, uid, pwd |
| `RGBC_POR_DEV_LOGIN_OVERRIDE` | Optional impersonation |
| `RGBC_POR_LOGIN_DEBUG` | Optional diagnostic stop on failed login |

`.env` is read from the directory above `include/` (the site root). If the file is missing, process environment is used. README says production should set the variables on the IIS pool or machine and should not keep `.env` on disk.

`CHANGELOG.md` says credentials used to be hardcoded in `include/system.inc.php` and that those passwords should be rotated. The current `system.inc.php` calls `SecureConfig::getOdbcSettings()` only.

## Authentication

1. IIS is expected to set `LOGON_USER`. `web.config` has no authentication or authorization section.
2. `loginUser` maps that string onto `mstUser.strDomainLogin` and requires `mstUserGroupPOR`.
3. The session stores the whole joined row as `$_SESSION['USER']`.
4. Full pages redirect when that session key is missing.
5. `checkLogin()` exists and is called from `challengeUgAccess` only.
6. Email-link entry points include `ldap/adLDAP.php` and then call `loginUser`. They do not construct `adLDAP`.

Login debug can print the SQL and a sample of domain logins. It is gated by `RGBC_POR_LOGIN_DEBUG`.

## Data

There is no schema migration in the repository. Tables and views below are those named in SQL strings. Column lists are incomplete; they are the columns this PHP reads or writes.

### POR tables

| Object | Used for |
|---|---|
| `porRequisition` | PO header: status, PO number, `intPONumber`, user, managers, region, vendor, dates, quote, contacts, captured-by, authorised-by |
| `porItem` | Lines: GL segments, description, item number, quantity, cost, total |
| `porReceipt` | Receipt header: number, invoice number, status, date, requisition id |
| `porReceiptItem` | Receipt quantities and costs per PO line |
| `porDocument` | Uploaded file name, type, user. `refRequisitionID` is also used for vendor documents |
| `porVendor` | Onboarding header and status |
| `porVendorSalesDepartment`, `porVendorAccountsDepartment` | Contact rows for a vendor |
| `porUGAccess` | Level limits and authoriser tokens per user group |
| `porUGGL` | GL codes allocated to a user group |
| `stgUGAccess`, `stgUGGL` | Import staging. Import truncates the live table |

### Shared or external objects (not created here)

| Object | Used for |
|---|---|
| `mstUser`, `mstUserGroupPOR` | Identity and `strUserGroup`, `blnSpecial`, managers, email, region |
| `mstUsageLog` | Login audit (`strSystem = POR`) |
| `mstVendor` | PO vendor dropdown and exports. Separate from `porVendor` |
| `mstBranch` | Region descriptions (`strRegionDesc2`) |
| `mstGLCode` | Full GL validation |
| `mstPaymentTerms` | Payment term dropdown |
| `fsrRequestType` | Request type dropdown |
| `dwdCustomer` | Ship-to and bill-to |
| `sysQuery` | Settings, including `POR_FISCAL_PERIOD` |
| `sysCronJob` | Outbound queue |
| `viePOR_UGAccess` | Approval challenge |
| `viePORequsitionManager` | Manager display names (spelling is `Requsition` in SQL) |
| `vieGLCode`, `vieBrandGLCode`, `vieKeyAccountGLCode` | GL dropdowns |

`sql/recommended_performance_indexes.sql` proposes nonclustered indexes on `porItem.refRequisitionID`, `porItem (BrandGLCode, refRequisitionID)`, `porRequisition.dtDate`, `porRequisition.strPONumber`, and `mstGLCode.FullGLCode`. The header says to review with a DBA before applying. **Unknown** whether those indexes exist in the database.

## Status values

Purchase order, written by this app: `Draft`, `Pending`, `Approved`, `Declined`, `Cancelled`, `Completed`, `Complete balance no longer applicable`.

Purchase order, read but not written here: `Partially Receipted`.

Receipt, written here: `New`, `Submitted`, `Cancelled`.

Receipt, read but not written here: `Processed`.

Vendor, written here: `Draft`, `Pending`, `Approved`, `Declined`, `Finance Approved`, `Complete`, `InComplete`, `Active`, `Done`.

Form-only label: `FISCAL PERIOD LOCKED` (not written to `porRequisition`).

New unsaved forms display status `New` before the first insert.

## APIs

There is no JSON REST API.

`ajax.functions.php` switches on `type`:

| type | Result |
|---|---|
| `ajaxSearchVendorXML` | Vendor options, excluding names starting with `ESN` |
| `ajaxSearchShipToAccountXML` | `dwdCustomer` search in a region |
| `ajaxLoadRegionBillToAccountsXML` | Bill-to, promotional accounts |
| `ajaxLoadRegionShipToAccountsXML` | Ship-to for a region |
| `ajaxReloadProductsXML` | Products for a brand |
| `jsReloadRegionGLCodes`, `jsReloadBrandGLCodes`, `jsReloadKeyAccountGLCodes`, `jsReloadPEGLCodes`, `jsReloadTMEGLCodes` | Dependent GL options. Active rows, or the value already selected |
| `jsReloadGLDescription` | Description for the chosen combination |
| `jsCheckReceiptItemQty` | Returns 1 or 0 |

The default body is an empty XML data wrapper. Content type is `text/xml` unless `header` is posted, in which case that string is sent as the content type. Output is echoed after `ob_clean()`.

Page actions are HTML form posts named `Action` (`Save`, `Submit`, `Approve`, and the labels listed in `prd.md`).

## Background work

No queue worker, cron script, or Windows service is in this repository.

The app inserts `sysCronJob` with a compact timestamp `yyyyMMddHHmm` and status `New`. Cancel updates prior rows for the same param to `Removed`. Re-receipt sets an existing `POR_3Receipted` row back to `New`.

**Unknown / Requires Confirmation:** process name, schedule, and how it sets `Partially Receipted` and `Processed`.

## Files and storage

Uploads use `move_uploaded_file` into `include/documents/`. Quote names start with the PO number. Invoice names start with the receipt number. Spaces in the original name become underscores. Single quotes are stripped. An existing file with the same name is deleted first.

`porDocument` stores the file name and type, not the bytes.

## Security notes (observed, not a test)

- SQL is built by concatenating request values in many statements. `requisitionSqlEscape` is used on some filters. It is not universal.
- `display_errors` is enabled.
- Import replaces a whole configuration table.
- Approval buttons are hidden in HTML. The status-changing methods do not re-check the user-group challenge.
- `ajax.functions.php` has no session redirect.
- Login debug prints SQL and sample account names when the env flag is set.
- Historical secrets are called out in `CHANGELOG.md` as something to rotate. They are not repeated in Silicon Ark.
- Sonar exclusions skip `vendor/`, `PHPExcel/`, `ldap/`, `Excel/`, and editor folders. Several ODBC and `SqlObject` rules are ignored in `sonar-project.properties`.

## Deployment and environments

**Documented** in the README: copy `.env.example` to `.env`, set the ODBC values, enable `php_odbc`, restart the app pool. Production should use machine or pool environment variables. QA should use the same PHP build as production if production already runs POR.

**Inferred** host names from code: session folder `rgbc_por`, pool name `rgbc_por_qa` in the README, footer `[Dev]` when `SERVER_NAME` is `sqlsrv02`. An older footer file checks `sql-server` and is commented unused.

No pipeline file, Docker file, or appspec is in the repository.

**Unknown:** production URL, which catalog is live, and whether the recommended indexes have been applied.

## Development and testing

- Composer for PhpSpreadsheet. Reports load `./vendor/autoload.php`.
- `.editorconfig` is present.
- Sonar project key `por`.
- No PHPUnit, no CI definition, no seed data.
- `test_folder_share_read.php` is a one-link manual page, not a test harness.

## Monitoring

`mstUsageLog` records a successful login. There is no application log file, metrics endpoint, or health check in the tree. ODBC failures surface as PHP errors because `display_errors` is on.

## Document map

| Question | Where |
|---|---|
| What the product is for and how the workflows run | `prd.md` |
| What is live, leftover, or outside the repo | `features.md` |
| Rules a tester can check | `business-rules.md` |
| Scope and what is not decided | `project-plan.md` |
