# PRD — edioverdrive (Transactflow)

Evidence date: 2026-10-16. Source: `https://bitbucket.org/overdrive/edioverdrive.git`, `main` at `a33b2b78` (2026-10-06).

There is no prior PRD in Silicon Ark. Requirements below come from `README.md`, `CHANGELOG.md`, `STORE_API_DOCUMENTATION.md`, `scope/`, and the code that implements them. Provenance is marked on each item.

Naming: the Git repository is **edioverdrive**. The running product is called **Transactflow** (`composer.json` description, mail from-name `Transactflow Notifications`, hosts `admin.transactflow.io` and `qa.transactflow.io`). The README title is still **Admin Overdrive Starter**.

## Product purpose

Transactflow sits between retailers (buyers) and suppliers (sellers). It collects purchase orders, invoices, order responses, and claims, checks and stores them, and delivers them in the format and channel each partner expects.

Provenance: **Documented** (`scope/company_flow_maps/System_Functionality_Overview_For_Business_Article.md`).

The goal stated in that article is to avoid re-keying documents and to shorten the gap between purchase, supply, invoicing, and reconciliation.

## Users and roles

Seeded roles (`app/Database/Seeds/UserRole.php`), in insert order. Numeric ids below assume a fresh autoincrement starting at 1. Live databases may have more rows. Provenance of the four names: **Documented** (seeder). Provenance of the numeric ids: **Inferred**.

| Id (inferred) | Role | What the code lets them do | Provenance |
|---|---|---|---|
| 1 | Developer | Seeded first. Stale-user deactivation skips `role <> 1`, so Developer accounts are not auto-deactivated. Source-file view on orders, invoices, and order responses allows role 1. | Implemented |
| 2 | Admin | Source-file view allows role 2. Label-email admin checks include role 2. | Implemented |
| 3 | Company Admin | Sidebar and `Authorization` filter currently hide and block `orderResponses` for role 3. Comment says this is temporary until buyer/supplier scoping is ready. | Implemented (temporary comment) |
| 4 | Company User | Seeded. Company-scoped order lists use session `companyAccessIDs`. | Implemented |

Views also treat role ids **1, 7, 2, and 3** as admin on label email templates (`app/Views/label/TabsContent/labelEmailTab.php`). Role **7 is not in the seeder**. What role 7 is called in production is **Unknown / Requires Confirmation**.

Menu visibility is data-driven: `user_access` rows per role, loaded by `menu_helper` and enforced by `App\Filters\Authorization` against the first URL segment. A URL with no `menu` row redirects to `/`. A menu row the role cannot access redirects to `/blocked`.

Session login is `isLoggedIn` (`App\Filters\Authentication`): missing `session('isLoggedIn')` redirects to `/`.

Company users see orders limited by `companyAccessIDs`. The value `all` means no company filter (`Orders::getOrderCompanies`). Provenance: **Implemented**.

## Main workflows

### W1 — Sign in, 2FA, and password reset

Trigger: a person opens `/` or, during maintenance, `/silicon-staff-login`.

Implemented behaviour (`Welcome`, `Users`, `TwoFactorService`, `PasswordPolicy`):

- Login posts to `GetLogin`.
- Failed password writes login history status **1**. Successful login writes status **2**. Failed 2FA writes status **4**. Logout writes status **3** (`LoginHistoryModel`).
- After a correct password, email 2FA is the default. The user can switch to an authenticator app (TOTP) from Profile → Two-Factor Authentication. TOTP secrets are stored encrypted (`scope/Task_2FA_TOTP.md`, `robthree/twofactorauth`).
- QA can skip a new OTP when the last successful 2FA is inside the model window (`Welcome` comments: `is2FARequired`, `check2faEmailConfirmationDate`). The exact window length is in `UserModel` and was not re-measured in this pass. **Requires confirmation** if a tester needs the number of hours.
- Pending 2FA session lives 900 seconds (`Welcome::PENDING_2FA_TTL`). After that the pending session is cleared.
- Five failed logins or failed 2FA attempts from the same email and IP within 5 minutes insert a blocked-user row. Successful logins do not count. Provenance: **Implemented** (`LoginHistoryModel::checkFailures`). The attempts limit is a literal `5` with a TODO comment.
- Forgot-password and reset-password routes are public. A reset link is sent only when the reset token is stored (`CHANGELOG.md`).
- New and changed passwords must pass `PasswordPolicy`: at least 12 characters, uppercase, lowercase, a number, a non-alphanumeric character, and at most 72 bytes.

Maintenance mode (`MaintenanceMode` filter): when maintenance is on, `/` shows the maintenance page. Staff entry excepted paths are `silicon-staff-login`, `getLogin`, `2fa-validation`, `confirmCode`, and `cronJobs`.

### W2 — Receive a purchase order

Trigger: CronEngine calls `CronJobs::getAllOrders`, which calls `GetXMLData` for active order transports. A storefront may also `POST /store-api/createOrder`.

Documented path (`scope/Policy_New_Client_Integration_Go_Live.md`):

1. The transport row identifies the company, `method_identifier` (`orders`), transport type (Folder, API, FTP, Email, ShopFlow), and `docs_type` (examples named in the policy: `shopritexml`, `boxerxml`, `metroxml`, `tafDefaultOrdersJson`).
2. The matching parser maps the file or message into order header, party, and line fields.
3. `saveOrderToDb` stores the order.

Duplicate rule (implemented, also stated in the policy and changelog): an inbound order is a duplicate when the same `order_number` already exists for the same `seller_company_gln`. JAB orders also match `branch_number`. The same PO number may exist for two sellers (Boxer reuses PO numbers). An empty GTIN element is stored as an empty string so the line still inserts.

Store API path (documented and implemented): organisation `POST /organisations/login` with `name` and `api_key` returns a JWT. `exp` is `time() + 6000` (100 minutes). `createOrder` requires `Authorization: Bearer` and a JSON `order` object, then writes a flat file (`StoreApiController::createOrderFlatFile`). It does not insert straight into `orders` in the method that was read. When that file is later picked up by the folder/cron parser is **Requires Confirmation** against the transport row for that seller.

### W3 — Deliver the order to the supplier

Trigger: `CronJobs::exportAllOrders` for seller companies (`type_desc` 2).

The exporter is company-specific (pipe-delimited, XML, CSV such as TAF). UI exports of orders and invoices are written under `writable/front_end_exports/{user}/` so cron does not treat them as inbound files. Download of those files requires a logged-in session (`CHANGELOG.md`).

If today’s orders have been available to the seller for at least one hour and are still not downloaded or processed, an admin alert is queued. Duplicate alerts for that subject are cooled down for 30 minutes (`CronJobs` constants). Provenance: **Implemented**.

### W4 — Receive a supplier invoice and send it to the buyer

Trigger: `CronJobs::getAllInvoices` then `CronJobs::exportAllInvoices`.

Documented rules (go-live policy; `scope/Invoice_Flow_Seller_To_Buyer_Reference.md` is the generic model):

- Folder invoice transports are **read from disk**. For the documented DGB pattern, the supplier places files on the server. Transactflow does not pull those files from a remote API. A different pattern, FTP with `docs_type = transferIn`, downloads into a staging folder first.
- Outbound invoices are grouped by `invoice_name`. The export transport is the seller row whose `company_identifier` **exactly** equals that `invoice_name` (case and punctuation). Examples named in the policy: `PICK 'N PAY`, `Shoprite`, `BOXER SUPERSTORES RSA`, `Metro`, `Dischem`, `Makro`, `M2North`.
- No matching transport yields “No transport method to export invoices for [seller name]”.

Reprocess (`markInvoiceForReprocessing`): if `invoice_name` is empty or `Unknown`, it is filled from the store GLN. Existing EDI labels, including `BOXER SUPERSTORES RSA`, `BOXER SWAZILAND`, and `SHOPRITE LESOTHO (PTY) LT`, are left unchanged. Unknown plus a Boxer store becomes `BOXER SUPERSTORES RSA`. Provenance: **Implemented** and **Documented** (`scope/specs/2026-09-22-invoice-reprocess-unknown-invoice-name.md`).

### W5 — Order response

Two different mechanisms exist. They must not be collapsed into one.

1. **Inbound partner response, then hub callback.** Example documented as stable: DGB CSV (`docs_type` `dgbmakroorderresponsexml`, `method_identifier` `orders_responses`) is parsed and sent to Makro M2North as CONFIRMED, REJECTED, or MIXED via `MakroM2NorthSoapService`. `getAllOrders` does not run this stream. `CronJobs::getAllOrderResponses` does.
2. **Generic outbound export.** `CronJobs::exportAllOrderResponses` calls `Orders::exportOrderResponsesViaCron`, which returns a Notice that no order-response export pipeline is configured. Provenance: **Implemented** placeholder.

Makro order **ingest** sends hub status RECEIVED when `sendOrderResponseToMakro` is true, including when the save result is duplicate or empty. Ingest does not send hub REJECTED. If the RECEIVED SOAP call fails, the error is kept and a hub-ack failure flag is set. Provenance: **Documented** in the changelog and **Implemented**.

Company Admin (role 3) cannot open the order-responses UI until scoping work lands. Developer and Admin (roles 1 and 2) can open source files there.

### W6 — Claims

Claims are a separate stream (`claims` tables, `Claims` controller, `method_identifier` claims). The business article says they are isolated so claim rules do not collide with orders and invoices. Provenance of isolation: **Documented**. The per-partner claim parsers that are actually enabled depend on transport rows in the database. **Requires Confirmation** per company.

### W7 — Fault finding and customer error mail

`FaultFinder` can scan orders, invoices, and products. Fault notification mail goes to the company `primary_email`, `cc_list`, and the admin team inbox (setting name `TAF Team Admin Contact Email`). Provenance: **Implemented** (`CHANGELOG.md`, `tests/unit/FaultFinderNotificationMailListTest.php`).

Customer-facing cron error text includes the document number and omits internal record ids. Admin mail includes ids. Provenance: **Implemented** (`CronJobs::formatErrorMessageForCustomer` / `ForAdmin`).

### W8 — Reporting and downloads

Signed-in users with menu access can open orders reporting, invoice reporting, chart reports (orders, invoices, claims, daily and monthly), uninvoiced orders, unmatched invoices, sales, and BI tabs (`app/Views/reports/`).

Download Reports invoice buyer filter resolves a short company name (for example `Boxer`) through the company GLN and active store GLNs, then falls back to `invoice_name`. Provenance: **Implemented** (`InvoicesModel::applyInvoiceBuyerFilter`).

Download Links is a maintained list of files (`download_links_list`). UI exports are per user under `writable/front_end_exports/`.

### W9 — Documents browser

`GET /documents` lists directories and files and can view or download one file (`Documents` controller, `DocumentBrowserService`). Menu migration `2026-06-12-120000_AddDocumentsMenu.php` adds the menu and admin role access. Which directories a company user can open versus an admin is **Requires Confirmation** beyond “admin role access” on that migration.

### W10 — Scheduled operations

See business rule BR-CRON in `business-rules.md`. Operators can also run one job from System Settings (`POST system_settings/runCronJob`), including an inactive job, bypassing the clock window. That path requires a logged-in granted session.

### W11 — New client go-live

Trigger: a project manager starts a new buyer or seller integration.

The mandatory checklist is `scope/Policy_New_Client_Integration_Go_Live.md` (company, stores, vendor numbers, `clients`, transports, exact `invoice_name` / `company_identifier`, user access, QA evidence). Provenance: **Documented** policy. The application does not appear to block go-live in code if the checklist is skipped. Provenance of “no software gate”: **Inferred** (no go-live workflow controller was found).

## Functional requirements

### Identity and access

FR-001 — Login. A user with a valid username and password starts a session. Invalid credentials are recorded as login-history status 1 and do not start a session. Evidence: `Welcome`, `Authentication` filter. Implementation: Implemented. Confidence: High.

FR-002 — Second factor. After a valid password, the user must complete email OTP or TOTP unless the QA grace rule in `UserModel` says 2FA is not required. Evidence: `Welcome`, `TwoFactorService`, `scope/Task_2FA_TOTP.md`. Implementation: Implemented. Confidence: High for the two methods. Confidence: Medium for the grace-window length.

FR-003 — Lockout. The fifth failed login or failed 2FA for the same email and IP inside 5 minutes blocks that pair. Successful logins do not increment the counter. Evidence: `LoginHistoryModel::checkFailures`. Implementation: Implemented. Confidence: High.

FR-004 — Password policy. Passwords shorter than 12 characters, missing a required character class, empty, or longer than 72 bytes are rejected with one message that does not echo the password. Evidence: `app/Libraries/PasswordPolicy.php`, `tests/unit/PasswordPolicyTest.php`. Implementation: Implemented. Confidence: High.

FR-005 — Menu authorisation. A signed-in user may open a first-segment URL only when that segment exists as a menu and the user’s role has `user_access` for it. Evidence: `Authorization` filter. Implementation: Implemented. Confidence: High.

FR-006 — Company scope. When `companyAccessIDs` is not `all`, order queries are limited to that user’s companies and stores. Evidence: `Orders::getOrderCompanies`. Implementation: Implemented. Confidence: Medium (the same pattern should be confirmed on every list; this pass confirmed orders).

FR-007 — Order responses hidden from Company Admin. Role 3 is redirected to `/blocked` for segment `orderResponses` and the menu item is omitted. Evidence: `Authorization.php`, `sidebar.php`. Implementation: Implemented. Confidence: High. Product permanence: **Requires Confirmation** (code comment says temporary).

FR-008 — Stale accounts. A monthly cron deactivates active users other than role 1 whose last successful login (status 2) is older than 6 months, or who have no successful login and were created more than 6 months ago. Admin mail lists who was deactivated. Evidence: `CronJobs::deactivateStaleUsers`, migration `2026-05-12-120000`. Implementation: Implemented. Confidence: High. Whether the cron row is still `active = 1` in production: **Requires Confirmation**.

### Documents

FR-010 — Ingest orders. The cron engine ingests active order transports and stores non-duplicate orders with lines. Evidence: `getAllOrders`, `GetXMLData::saveOrderToDb`, go-live policy. Implementation: Implemented. Confidence: High.

FR-011 — Duplicate orders. Same `order_number` and `seller_company_gln` is a duplicate. JAB also uses `branch_number`. Evidence: changelog and go-live policy. Implementation: Implemented. Confidence: High.

FR-012 — Export orders to the seller format configured on that company’s order transport. Evidence: `exportAllOrders`. Implementation: Implemented, format depends on `docs_type`. Confidence: High at the framework level. Per-company format: see that company’s flow map.

FR-013 — Ingest invoices from the seller’s configured inbound channel and export them only when `invoice_name` exactly matches the seller transport’s `company_identifier`. Evidence: go-live policy section 1.2. Implementation: Implemented. Confidence: High.

FR-014 — Invoice reprocess must not overwrite a real EDI buyer label with a GLN-derived name. Evidence: spec `2026-09-22-invoice-reprocess-unknown-invoice-name.md`. Implementation: Implemented. Confidence: High.

FR-015 — Order-response ingest for configured `orders_responses` transports, including DGB to Makro. Evidence: `getAllOrderResponses`, changelog. Implementation: Implemented for the documented DGB/Makro path. Confidence: High.

FR-016 — Generic order-response export cron reports that no pipeline is configured. Evidence: `Orders::exportOrderResponsesViaCron`. Implementation: Implemented as a notice, not as an exporter. Confidence: High.

FR-017 — Claims are stored and processed on their own tables and transports when a claims method is active. Evidence: claims migrations and `Claims` controller. Implementation: Implemented as a subsystem. Which partners are live: **Requires Confirmation**.

FR-018 — Store API create order. A valid organisation JWT may submit one JSON order. Missing `order`, invalid header fields, or invalid lines return validation errors. Missing or bad JWT returns 401. Evidence: `STORE_API_DOCUMENTATION.md`, `StoreApiController::createOrder`. Implementation: Implemented. Confidence: High.

FR-019 — Store API get invoices. A valid JWT and an `order_numbers` array return matching invoices. Evidence: `StoreApiController::getInvoices`. Implementation: Implemented. Confidence: High.

### Operations

FR-030 — Cron engine runs due `cronjob_actions` only between 06:00 and 22:59, only when `active = 1`, and only when the `function` column names a real `CronJobs` method. Production requires query `key` to match the in-code security key. Evidence: `CronJobs::index`, `scope/Task_CronJobs_Implementation_Overview.md`. Implementation: Implemented. Confidence: High.

FR-031 — Manual run. A granted user may run one cron row from System Settings, including an inactive row, outside the clock window. Evidence: changelog, `SystemSettings::runCronJob`. Implementation: Implemented. Confidence: High.

FR-032 — Heartbeat. Primary heartbeat and backup heartbeat write distinct cron-log types and email when the engine or the primary ping is stale, except the expected overnight gap. Evidence: `scope/Task_CronEngine_Heartbeat.md`. Implementation: Implemented. Confidence: High.

FR-033 — Cron health digest. `notifyCronHealthDigest` emails an inline summary plus an HTML attachment of document-level errors and warnings. Evidence: changelog, `CronHealthDigestAttachmentService`. Implementation: Implemented. Confidence: High.

FR-034 — Credential expiry warning. `notifyTransportCredentialExpiry` emails when transport credential expiry dates are due. Evidence: migration `2026-07-01-100100` and `CronJobs::notifyTransportCredentialExpiry`. Implementation: Implemented. Confidence: Medium (the exact “due” comparison was not re-read line by line).

FR-035 — Unknown names. `notifyReportingUnknownNames` scans orders and invoices for unknown party names and emails admins. Recurrence value `540` means twice daily (about 06:00 and 15:00). Evidence: changelog, migration `2026-08-13-090000`. Implementation: Implemented. Confidence: High.

FR-036 — Fault notifications include the client’s primary and cc addresses plus the admin team inbox. Evidence: `FaultFinderNotificationMailListTest`. Implementation: Implemented. Confidence: High.

FR-037 — Maintenance. When the maintenance setting is on, public `/` shows maintenance. Staff can still log in via `/silicon-staff-login`. Cron remains reachable. Evidence: `Filters.php`, migration `2026-04-19-120000_AddMaintenanceSettingToSettingsTable.php`. Implementation: Implemented. Confidence: High.

### Master data

FR-040 — A buyer is `companies.type_desc = 1`. A seller is `type_desc = 2`. A trading pair is an active `clients` row. Evidence: flow-map README. Implementation: Implemented as data model. Confidence: High.

FR-041 — Where a buyer sends a supplier code instead of a GLN, `vendor_numbers` maps that code to the seller, issued by the buyer. Missing or wrong rows block or mis-route the order. Evidence: go-live policy section 2.1. Provenance: **Documented** requirement. Confidence: High as policy.

FR-042 — Products can be listed, added, updated, and scanned for duplicates (`Products` controller). CSV upload can map columns using `csv_import_templates`. Evidence: routes, migration `2026-03-23-100000_CreateCsvImportTemplatesTable.php`. Implementation: Implemented. Confidence: High.

## Integrations

| System | Direction | Purpose | Auth as implemented | Provenance |
|---|---|---|---|---|
| Retailer and hub APIs (REST and SOAP), including Makro M2North and Shoprite | Both | Orders, invoices, order responses | Per-transport endpoint, key, and secret on `company_transport_methods` | Documented and implemented |
| FTP, SFTP, and folder drops | Both | File exchange | Paths and FTP credentials on the transport row | Documented and implemented |
| Storefront callers | Inbound JSON, outbound invoice JSON | `POST /store-api/createOrder`, `POST /store-api/getInvoices` | JWT from `POST /organisations/login` (`name`, `api_key`), 6000 second expiry | Documented and implemented |
| SMTP | Outbound | 2FA, errors, reports, rotation, digest | `.env` `email.SMTP*` | Implemented |
| Slack | Outbound | `Slack::sendMessage`; deploy webhook from `.env` | Webhook URL | Implemented. Route `slack` is outside the login filter |
| OpenAI | Outbound from ChatGPT screen | `chatgpt_helper.php` via cURL | `.env` `openai.apiKey` | Implemented. The `orhanerday/open-ai` package was removed |
| Xneelo crontab and cron-job.org | Inbound HTTP | Wake CronEngine and heartbeats | Production `key` query parameter | Documented |
| Salesforce, Africa’s Talking SMS | None | Helpers are stubs | n/a | Dead code |

Partner-specific endpoint paths, folder names, and credentials stay in `scope/company_flow_maps/` and in the database. They are not copied here.

## Important constraints

- `invoice_name` must match `company_identifier` exactly or the invoice cannot be exported.
- Order duplicate identity is PO plus seller GLN, not PO alone.
- CronEngine does not run from 23:00 through 05:59. Overnight silence is expected.
- A new `CronJobs` method does nothing until a `cronjob_actions` row points at it.
- PHP version the installer will accept is whatever `composer.json` says on that commit (`^8.4` here), even though the README says 8.3.
- Company ids in the July 2026 flow-map index are environment-specific.

## Error and empty cases

| Situation | Behaviour | Provenance |
|---|---|---|
| Bad or missing cron `key` in production | Engine refuses the run | Implemented |
| Outside 06:00–22:59 | Engine does not run jobs (manual System Settings run still can) | Implemented |
| Unknown `docs_type` | Policy says the result is “Unsupported transport method” or the wrong parser | Documented |
| Duplicate order or invoice | Skipped; error text includes the document number | Implemented |
| Empty Boxer GTIN | Stored as empty string and the line is kept | Implemented |
| No invoice transport for that `invoice_name` | Export error naming the seller | Documented |
| Store API without JWT | 401 “Unauthorized Access” | Implemented |
| Store API without `order` or `order_numbers` | Validation error | Implemented |
| 2FA pending longer than 900 seconds | Pending session cleared; user must sign in again | Implemented |
| Fifth failure in 5 minutes | Email and IP blocked | Implemented |
| Folder empty | Cron constant message “The folder is empty” | Implemented |
| Order-response export cron | Notice, not a file | Implemented |

## Non-goals confirmed by the code

- The app does not send SMS. The helper is commented out.
- The app does not call Salesforce. The helper is empty.
- AWS SDK was removed (`CHANGELOG.md`: no app usage).
- There is no in-app gate that enforces the go-live checklist.
