# Business rules — greenfields_web

Evidence date: 2026-10-13. Source: `main` at `c8fce1f`.

Rules are copied from code, seeders, or comments in the repository. **Inferred** means the code behaves this way and no document states it as policy. No rule was added to fill a gap.

## Identity

| ID | Rule | Provenance |
|---|---|---|
| BR-001 | The Git and Sonar names are greenfields_web. The login page title and the mail footer use Transactflow. The navbar logo is Admin Overdrive. System settings say "Label System Settings". The deploy script says "SAWIS: Label". | **Documented** in those files. They disagree. Which name is authoritative is **Unknown / Requires Confirmation** |
| BR-002 | `composer.json` homepage is `https://www.overdrive.co.za`. Mail footer sends people to `https://transactflow.io` and `info@transactflow.io`. | **Documented** and **Implemented** |

## Access

| ID | Rule | Provenance |
|---|---|---|
| BR-003 | A user with `users.active` 0 cannot sign in or complete password reset. The flash text is "Your access has been revoked!. Please contact your administrator." | **Implemented** |
| BR-004 | An unknown email and a wrong password produce the same flash: "Your ID or Password is Wrong!" | **Implemented** |
| BR-005 | Passwords are checked with `password_verify`. The Users seeder stores `password_hash` of a short numeric literal for `admin@overdrive.co.za`. | **Implemented** |
| BR-006 | A confirmed 2FA code (`email_authentication_codes.status` 1) skips a new code when `updated_at` is less than 1 day old. The comparison uses whole days (`DateInterval::days`). The comment example says "e.g., 10 days"; the variable is `1`. | **Implemented**. The comment and the value disagree |
| BR-007 | A new 2FA code is a random integer from 100000 to 999999 and is emailed in the body "Your 2FA code is: {code}". | **Implemented** |
| BR-008 | Login history status 1 is a failed login, 2 is success, 3 is logout, 4 is a 2FA failure, 5 is an invalid reset token, 6 is a reset request (including a request for an email that does not exist). | **Implemented** |
| BR-009 | If the request IP is returned by `BlockedUsersModel::getBlockedIP`, `Welcome` sets `userBlocked` and returns from the constructor. The model table is `blockedusers`. The migration creates `blocked_users_list`. | **Implemented**. Table-name mismatch |
| BR-010 | Password reset always flashes that a link was sent. For an unknown email the flash is an error and no mail is sent. For a known email the flash is success and a mail is sent. | **Implemented** |
| BR-011 | A reset token is 64 hex characters, stored on `password_reset` with `active` 1, and has no timestamp check. Submitting a matching new password pair sets `active` 0. The two password fields must be equal. No minimum length is enforced in `Welcome::resetPassword`. | **Implemented** |
| BR-012 | Registrations are not done on this application (`Welcome::register` is empty). | **Documented** in that comment and **Implemented** |
| BR-013 | Seeded roles are Developer (1), Admin (2), Company Admin (3), Company User (4). | **Documented** in `UserRole` seeder |
| BR-014 | Developer and Admin receive `companyAccessIDs` `all` and both company types 1 and 2. Company Admin receives only linked company ids and `companyStoresAccessIDs` `all`. Company User receives neither. | **Implemented**. Comment calls roles 1 and 2 "Silicon internal roles" |
| BR-015 | The first URL segment must be a `user_menu.url` the role can access, unless the path is on the filter except list. Otherwise the user is sent to `/` or `blocked`. | **Implemented** |
| BR-016 | `home` does not require a menu grant. It does require a session. | **Implemented** |
| BR-017 | `cronJobs`, `slack`, `tracking-pixel`, `organisations`, and `store-api` do not require a session. | **Implemented** |

`Common.php` `statusDisplay` maps 1 Submitted, 2 Approved, 3 Rejected. That map is separate from login-history statuses. It is **Implemented** as a helper. No caller was required to state this rule; the helper itself defines the labels.

`contentTypeDisplay` maps 1 Label, 2 Supporting Document, 3 Front Label, 4 Back Label, 5 Neck Label, 6 Approval Document. **Implemented** helper. This matches the SAWIS label wording, not the EDI order screens.

## Companies, buyers, and sellers

| ID | Rule | Provenance |
|---|---|---|
| BR-018 | `companies.type_desc` 1 is a buyer. `getAllOrders` only selects buyers. | **Implemented**. Comment in `CronJobs::getAllOrders` |
| BR-019 | `companies.type_desc` 2 is a seller. Export, invoice, move-exported, and client-report jobs select sellers. `CompanyModel::getCompaniesSellers` uses 2 and `getCompaniesBuyers` uses 1. | **Implemented** |
| BR-020 | A comment says `type_desc` 3 is both buyer and seller. Queries that use `type_desc = 2` do not include 3. | **Inferred** from the comment. No seeder inserts type 3 |
| BR-021 | Vendor numbers belong to a company and are issued by another company (`vendor_numbers.company_id`, `issued_by_company_id`). | **Implemented** in `CompanyModel` |
| BR-022 | Transport methods for a company run only when `company_transport_methods.active` is 1, in `order` ascending. | **Implemented** |
| BR-023 | The only seeded transport name is `Folder`. Code also accepts names `API` and `Email`. | **Implemented**. Email does no work (**Planned**). API invoices do no work (**Planned**) |
| BR-024 | An API order endpoint equal to `https://externalservicesqa.shopriteholdings.co.za/b2bservice/api/VendorOrder` is Shoprite REST. Every other API order endpoint is treated as SOAP (Pick n Pay). | **Implemented** |
| BR-025 | Pick n Pay SOAP stays on the UAT/test WSDL unless `api_endpoint` is exactly the production `pullservice.svc` URL. Comment: "always default to QA just incase". | **Implemented** and **Documented** in that comment |
| BR-026 | Shoprite order confirmation and Pick n Pay invoice generation run after a successful parse. SSL peer verification is disabled for both HTTP clients. | **Implemented** |

## Documents and faults

| ID | Rule | Provenance |
|---|---|---|
| BR-027 | Folder transport processes `method_identifier` `orders` or `invoices` only. Any other identifier is an error result and does not move a file. | **Implemented** |
| BR-028 | Duplicate Shoprite order ids in one response (`orderIdentification.entityIdentification`) are skipped after the first. | **Implemented** |
| BR-029 | A Shoprite response without `orderMessage.StandardBusinessDocumentHeader` is a Notice, not a saved order. | **Implemented** |
| BR-030 | Seeded fault names are `gln_not_found` and `bc_not_found` at severity 3, and `min_order_qty`, `min_order_val`, `product_price_fault`, and `invoice_price_fault` at severity 1. Each seeder row mails the same two Overdrive addresses. | **Documented** in `FaultType` seeder. The Users seeder does not call `FaultType` |
| BR-031 | Invoice calculation faults allow a caller-supplied difference (`$diffAllowed`). GLN faults use `checkGlnFound`. Range faults use `isInRangeCheck`. | **Implemented** |
| BR-032 | Cron processes at most `cronjob_actions.items_process_total` companies per run, or 10 when that column is empty. | **Implemented** |
| BR-033 | A cron action with recurrence 0 does not run unless the request has `override=true`. Recurrence is minutes. | **Implemented** |
| BR-034 | Cron does not run on Saturday or Sunday, before 08:00, or from 23:00 onward. A comment records an earlier close of 17:00. | **Implemented** |
| BR-035 | When `CI_ENVIRONMENT` is `production`, cron exits unless query `key` equals the hardcoded constant. Other environments do not check the key. The committed environment is `development`. | **Implemented** |
| BR-036 | If query `action` is set, only the cron row whose `action` equals that value runs. | **Implemented** |
| BR-037 | Scheduled queue mail sends when `sent_queue.processed` is 0, `send_date` is not null, and `send_date` is now or in the past. Limit is the job's `items_process_total` or 10. Oldest id first. | **Implemented** |
| BR-038 | Cron log rows are stamped `created_by` 9999 and `updated_by` 9999. | **Implemented** |

## Mail

| ID | Rule | Provenance |
|---|---|---|
| BR-039 | Mail is from `notifications@transactflow.io`, display name "Transactflow Notifications" plus `base_url()`, via SMTP host `smtp.transactflow.io`, port 465, crypto ssl. The password is hardcoded in `app/Config/Email.php` and is not recorded here. | **Implemented** |
| BR-040 | If `base_url()` does not contain `transactflow.io`, BCC is only `wessels@overdrive.co.za`. | **Implemented** |
| BR-041 | If `base_url()` contains `transactflow.io`, BCC is `wessels@overdrive.co.za` and `francois@overdrive.co.za`. Type `internalReport` also BCCs `jaco@overdrive.co.za`, `saskia@overdrive.co.za`, and `gareth@overdrive.co.za`, except on `qa.transactflow.io`, where Jaco and Gareth are removed. Type `externalReport` adds only Saskia. | **Implemented** |
| BR-042 | The HTML greeting is "Hi {capitalised local-part}." unless a type is passed, in which case it is "Hi EDI Team." The footer says not to reply. | **Implemented** |
| BR-043 | After a successful `sendMail`, Slack is notified except when type is `internalReport`, `externalReport`, or `invoiceExport`. A comment dated 12122024 says this exclusion still needs testing. | **Implemented**. The "needs testing" note is **Documented** in the comment |
| BR-044 | Attachments are added only when the file exists. A missing file is printed with `print_rr` and skipped. | **Implemented** |

## Dashboard leftovers

| ID | Rule | Provenance |
|---|---|---|
| BR-045 | `Home::index` treats roles 1, 7, 2, and 3 as staff who are not filtered to their own user id, and role 5 as a designer filtered to their user id. Roles 5 and 7 are not seeded. The parent/child status list the filter would apply to is an empty array. | **Implemented** leftover. Effect on the dashboard is **Inferred** to be none, because the list is empty |
| BR-046 | Session key `labelID` is set to an empty string at login. | **Implemented** |

## What was not found

No rule in this tree states a price, a tax rate, a credit limit, a document retention period, or a customer-specific Greenfields trading rule. Those items are **Unknown / Requires Confirmation**.
