# Business rules — edioverdrive (Transactflow)

Evidence date: 2026-10-16. Source: `main` at `a33b2b78` (2026-10-06).

Rules are taken from policy docs or read from code. Each rule is marked **Documented** or **Inferred** (or both). Nothing here is a proposed new rule.

Company-specific formats (column indexes, SOAP operations, folder names) stay in `scope/company_flow_maps/`. Those files are the operational source of truth for one company’s transports. This file holds rules that apply across companies.

## Parties and links

BR-001 — Buyer and seller. **Documented.** In `companies`, `type_desc` `1` is a buyer and `type_desc` `2` is a seller (`scope/company_flow_maps/README.md`).

BR-002 — Trading pair. **Documented.** `clients` must link `buyer_id` and `seller_id`, and `active` must be 1, before supplier scope, reporting, and exports treat the pair as live.

BR-003 — Vendor code. **Documented.** When the payload carries a buyer-assigned supplier code instead of the supplier GLN, `vendor_numbers` maps it: `company_id` is the seller, `issued_by_company_id` is the buyer, `vendor_number` is numeric, `active` is 1. A wrong row blocks the save or assigns the PO to the wrong seller (go-live policy section 2.1).

BR-004 — Company ids are per database. **Documented.** The flow-map index says company ids and transport ids depend on the database named in `.env`. The published index is a QA snapshot from 2026-07-06. Do not treat those ids as production ids without a query.

## Orders

BR-010 — Duplicate purchase order. **Documented and implemented.** `GetXMLData::saveOrderToDb` treats an order as a duplicate only when the same `order_number` already exists for the same `seller_company_gln`. JAB also matches `branch_number`. A buyer may reuse one PO number across sellers. A hit for seller A must not block seller B.

BR-011 — Empty GTIN. **Documented and implemented.** An empty GTIN element becomes an empty string on `products.gtin`, not a skipped line. A real barcode string is stored unchanged.

BR-012 — Inbound channel is data. **Documented.** Which parser runs is selected by `company_transport_methods.method_identifier` and `docs_type`, not by a hardcoded company name in the cron loop. An unknown `docs_type` does not silently use a default retailer format (policy wording: unsupported transport method, or the wrong parser).

BR-013 — Order export is per seller. **Inferred** from `exportAllOrders`, which loads companies of type 2 (seller), matching `exportAllInvoices`. **Documented** examples of seller formats include TAF CSV and XML. The exact file for a seller is the active order transport, not a global template.

BR-014 — UI exports are not inbound files. **Documented.** Files written under `writable/front_end_exports/` must not be processed as new EDI input. Cron inbound folders are separate (`temp_export` and company payload folders).

BR-015 — Undownloaded orders. **Implemented.** 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. The same alert subject is not sent again for 30 minutes.

BR-016 — Makro ingest acknowledgement. **Documented and implemented.** When Makro M2North ingest is configured to send a hub response, the hub status on receive is RECEIVED, including when the local save is a duplicate or has no order data. Ingest does not send REJECTED. A failed RECEIVED call is recorded and does not erase the local error.

## Invoices

BR-020 — Exact buyer label. **Documented.** Outbound invoice transport is looked up by seller company id plus `company_identifier` equal to `invoices.invoice_name`. Case and punctuation matter. `Pick n Pay` is not the same string as `PICK 'N PAY`. No match produces “No transport method to export invoices for [seller name]”.

BR-021 — Who delivers folder invoices. **Documented.** For Folder plus `dgbinvoice` (DGB pattern, and the same pattern for other suppliers), the supplier places files on the server. The cron only reads files already on disk. It does not pull those invoices from a remote API. FTP download runs only when the transport type is FTP and `docs_type` is `transferIn`.

BR-022 — Reprocess must not clobber a real label. **Documented and implemented.** `markInvoiceForReprocessing` may set `invoice_name` from the store GLN only when the current value is empty or `Unknown`. These labels are left as stored: `BOXER SUPERSTORES RSA`, `BOXER SWAZILAND`, `SHOPRITE LESOTHO (PTY) LT`. Unknown plus a Boxer store becomes `BOXER SUPERSTORES RSA` (same alias as the DGB/Namaqua import).

BR-023 — Buyer filter uses GLNs. **Implemented.** A report filter given a short company name (example: `Boxer`) matches the company GLN and active store GLNs, then falls back to `invoice_name`. This is why a filter of `Boxer` can find rows labelled `BOXER SUPERSTORES RSA`.

## Order responses and claims

BR-030 — Order responses are not orders. **Documented.** `GetXMLData::index` excludes `method_identifier` `orders_responses`, so `getAllOrders` does not run the DGB response CSV. `getAllOrderResponses` is the entry for that identifier.

BR-031 — DGB response status vocabulary. **Documented.** The Makro hub map uses CONFIRMED, REJECTED, and MIXED, with a per-line map. CSV status-column drift may be auto-detected and logged. An empty seller-product-code column can fall back to the next column so a single-line REJECTED file still processes. Details: `scope/company_flow_maps/9_makro.md` and `12_dgb_sa.md`.

BR-032 — Generic response export is not live. **Implemented.** `exportOrderResponsesViaCron` returns a Notice that no order-response export pipeline is configured. Do not expect every seller’s responses to be exported by this cron.

BR-033 — Claims stay on their own stream. **Documented** (business article): claims are isolated from order and invoice rules. **Implemented** as separate tables (`claims`, claim items, claim item meta). Which partners have an active claims transport is data, not a universal switch.

BR-034 — Company Admin cannot open order responses yet. **Implemented**, with a temporary comment. Role 3 is omitted from the sidebar entry and redirected to `/blocked`. The comment says `user_access` for role 3 may remain. Whether this is still the desired rule is **Requires Confirmation**.

## Identity

BR-040 — Password shape. **Implemented.** At least 12 characters; at least one lowercase, one uppercase, one number, and one character that is not a letter or number; at most 72 bytes (bcrypt cap). One public error message. Unicode letters count. Evidence: `PasswordPolicy`.

BR-041 — Lockout. **Implemented.** Status 1 (login failed) and status 4 (2FA failed) count. Status 2 (success) does not. Five matching failures for the same email and IP in five minutes create a block row. The limit is a code literal.

BR-042 — 2FA methods. **Documented and implemented.** Email code is the default. TOTP is optional and can be switched back to email. Pending 2FA expires after 900 seconds.

BR-043 — QA 2FA grace. **Implemented** (comments in `Welcome`). On QA, a recent successful 2FA can skip a new code. The length of that window was not extracted in this pass. **Requires Confirmation** before writing a test number.

BR-044 — Stale users. **Implemented.** Once a month (recurrence `43200` on the migration’s insert), deactivate `active = 1` users whose role is not 1, when the last successful login is older than 6 months, or when they have never logged in successfully and `created_at` is older than 6 months. `updated_by` is set to 9999. An admin email lists the accounts. Developers (role 1) are exempt.

BR-045 — Menu rights. **Implemented.** Access is by role and menu id (`user_access`). The first URL segment must match a menu. Missing menu redirects home. Missing grant redirects to `/blocked`.

BR-046 — Company data scope. **Implemented** on order company lookup. Session `companyAccessIDs` of `all` sees every company. Any other value limits the query to those companies and their stores. The go-live policy says supplier users must only see orders their organisation is allowed to access.

BR-047 — Password storage. **Documented** phase 1. Hashes are dual-written to `user_passwords.password_hash` and legacy `users.password`. Reads prefer `user_passwords` via COALESCE. Profile JSON and system-user lists strip the password field. Rotation email clears the rotation-required flag and stamps `dtPasswordRotated` on reset. The number of days that sets rotation required is **Requires Confirmation**.

## Schedule and notifications

BR-050 — Engine hours. **Documented and implemented.** CronEngine and heartbeat run only from 06:00 until 22:59. From 23:00 to 05:59 the engine does not run. A missed-schedule mail is not sent at 06:00 merely because the last engine row is from the night before.

BR-051 — Who wakes the engine. **Documented.** Production and QA RUN ALL is an Xneelo user crontab every 5 minutes (`GET /cronJobs` with `override=false` and `key`). Primary heartbeat is the same host every 12 minutes (`action=heartbeat`). Backup heartbeat is cron-job.org only (`action=heartbeat_backup`). Do not also run a 5-minute RUN ALL on cron-job.org. If the Xneelo crontab is down, documents stop; the backup heartbeat only emails.

BR-052 — Production key. **Implemented.** In production the `key` query value must match `CronJobs::SECURITY_KEY`. The value is hardcoded. It is intentionally not copied into this document. Manual System Settings runs do not use this URL.

BR-053 — Job catalogue. **Documented.** Adding a PHP method is not enough. `cronjob_actions.function` must match the method name, `active` must be 1, and recurrence must be due, unless `note` contains `bypass` or the request has `override=true`.

BR-054 — Recurrence units. **Documented.** Stored in minutes: 0 never; 1, 5, 10, 15, 30; 60 hourly; 540 twice daily (about 06:00 and 15:00, elapsed 9 hours); 1440 once per calendar day; 10080 weekly; 43200 monthly; 525600 yearly.

BR-055 — Manual run. **Documented and implemented.** Cron Settings play button runs one row by id, including inactive rows, and skips the clock window and the recurrence check. It still requires a logged-in user who passes `isGranted`.

BR-056 — Health digest evening slot. **Implemented.** From 21:00 until the end of working hours, `notifyCronHealthDigest` may run once even if its normal recurrence would miss the overnight gap. The digest window is 3 hours. The attachment caps at the newest 40 document-level errors per cron.

BR-057 — Two error audiences. **Implemented.** Customer error-log text has the document number and not the internal id. Admin mail includes record and invoice ids.

BR-058 — Fault mail recipients. **Implemented.** Fault finder queues the company primary email, the company cc list, and the admin team contact (setting name `TAF Team Admin Contact Email`).

BR-059 — Admin team inbox. **Implemented.** Several cron mails use that same settings row. If the setting is empty, who still receives mail is **Requires Confirmation**.

## Store API

BR-060 — Organisation login. **Documented and implemented.** `POST /organisations/login` with JSON `name` and `api_key` returns a bearer token when both match an organisation row. Unknown pair returns “No user found”. Token expiry is 6000 seconds. The route is outside the session login filter; the controller checks the JWT.

BR-061 — Create order. **Documented and implemented.** `POST /store-api/createOrder` requires the bearer token and an `order` object. Header and line validation run before a flat file is written. The documented QA base URL is `https://qa.transactflow.io/store-api/`.

BR-062 — Get invoices. **Implemented.** `POST /store-api/getInvoices` requires the bearer token and `order_numbers` as an array.

## Go-live (policy, not enforced in code)

BR-070 — Checklist before production. **Documented.** `scope/Policy_New_Client_Integration_Go_Live.md` is mandatory for the admin team: company, GLNs or codes, stores, vendor numbers, `clients`, transports, credential test on the target environment, `invoice_name` consistency, and user access. **Inferred:** the PHP application does not refuse to enable a transport when the checklist is incomplete. Enabling `active = 1` on a transport is enough for the cron to pick it up.

## Rules that look like product rules but are not confirmed

- Commercial price, zero-price billing preview, and who is invoiced for the platform. Tables and a billing email exist (`clients_billing`, `sendCompaniesBillingReport`, `ClientsBillingZeroPricePreviewTest`). The commercial rule is **Unknown / Requires Confirmation**.
- Role 7 as an administrator. One view treats it like an admin. The seeder does not define it.
- Car / cost categories and Labels / meeting groups. Code and some menus exist. No business rule document was found for them.
