# Business rules — edi_system (SAWIS Label System)

Evidence date: 2026-10-16.

Source: `git@bitbucket.org:overdrive/edi_system.git`, `master` at `75a67b5`.

Rules below are taken from code, views, migrations, and the README. Each rule is marked **Documented** or **Inferred** (from implementation). Where two parts of the code disagree, the conflict is stated. No rule was added because it "should" be true.

## Identity

**Inferred.** Operators using the UI are working in a product titled SAWIS Label System. The README calls the same tree Admin Overdrive. The repository name `edi_system` does not appear in the UI.

**Unknown / Requires Confirmation.** What "edi" means for this repository. No EDI business process is implemented.

## Access

**Inferred — BR-A1.** A request needs a session (`isLoggedIn` true) unless the path is `/`, `getLogin`, `register`, `organisations` or a child of it, `tracking-pixel`, `show404`, or `404`.

**Inferred — BR-A2.** A session is not enough. The first URL segment must exist as `user_menu.url`, and `user_access` must grant that menu to the session role. Otherwise the user is sent to `/` (unknown menu) or `blocked` (known menu, no grant).

**Inferred — BR-A3.** These paths skip the menu check: `/`, `getLogin`, `register`, `blocked`, `home`, `Welcome/*`, `organisations` and children, `chatGPT/*`, `tracking-pixel`, `show404`, `404`.

**Inferred — BR-A4.** `home` is visible to every logged-in user, including a role that has no menu grants.

**Documented and Inferred — BR-A5.** Seeded access rows all use role 1. The README says the app has authorization. It does not list the SAWIS role names.

**Inferred — BR-A6.** Role meanings used by the application:

| Id | Rule the code applies |
|---|---|
| 1 | Developer. Full label list, admin form, status screen actions that check role 1, developer section in `layouts/main.php` |
| 2 | SAWIS staff. Full label list and admin form. Not the role-1-only status/template blocks |
| 3 | Board. Label list filtered to status id 7. Admin form. Counted with staff on the dashboard |
| 5 | Designer. Only labels whose `user_designer_id` is that user. Member form. SAWIS navbar suppressed |
| 6 | May upload and resubmit. No list or form branch |
| 7 | SAWIS admin. Full label list and admin form. Grouped with role 1 on several list buttons |
| 0 | Stored when the SAWIS security group name is not one of the mapped names |

**Inferred — BR-A7.** SAWIS security group names map as follows (`Welcome::getHasLoginFromSawisOnline`): `Admin Dev` → 1, `Sawis Admin` → 7, `Sawis User` → 2, `Sawis Inspector` → 2, `Designer` → 5, any other name → 0. The comment on role 0 says the person cannot use the system and that the role must be updated in the database by hand.

**Inferred — BR-A8.** Self-registration always stores role 1. It does not consult SAWIS.

**Inferred — BR-A9.** After a successful SAWIS hash, the local password is not checked. The `password_verify` line is commented out.

**Inferred — BR-A10.** Meeting-group membership queries only users whose role is 1, 2, 3, or 7. Designers (5) and role 6 are not in that list.

**Inferred — BR-A11.** CSRF and honeypot filters are not applied. A logged-in browser session can post without a CSRF token.

**Documented — BR-A12.** README setup tells an operator to seed `users` before use. The seed inserts username `jaco@overdrive.co.za` with role 1 and a hardcoded password. The password value is omitted here.

## Label visibility

**Inferred — BR-L1.** Designer list and designer form add `where user_designer_id = current user`. If that user opens another designer's id, the label payload is empty and the member view still renders.

**Inferred — BR-L2.** Roles 1, 2, and 7 load every label on the list.

**Inferred — BR-L3.** Role 3 loads only `label.status = 7`. The view's default status names call one status "Contested". The code does not look up the name; it filters the number 7. If production id 7 is not Contested, the board sees the wrong queue.

**Inferred — BR-L4.** Roles without a branch (including 4 and 6) do not set list data. That is an implementation gap, not a written "deny" rule.

## Label fields

**Inferred — BR-L5.** On create, the label number is the autoincrement `id`. Status is stored as `1`. The designer id and `cc_list` come from the post. A log line "Uploaded label files" is written.

**Inferred — BR-L6.** Create accepts PDF only. Other extensions are skipped. If every file is skipped, the label row may already have been inserted only when at least one file reached `labelFiles`. If the extension check fails for all files, `labelFiles` is missing and the model will error when it iterates. The controller still told the user to upload a PDF only by filtering inside the loop.

**Inferred — BR-L7.** File type is chosen by the input name: `formFileSupportedPDF` → 2 Supporting Document, `formFileLabelPDFFront` → 3 Front Label, `formFileLabelPDFBack` → 4 Back Label, `formFileLabelPDFNeck` → 5 Neck Label, any other name → 1 Label. `contentTypeDisplay()` also knows type 6 as Approval Document, which create does not assign.

**Inferred — BR-L8.** Alcohol percentage is a required number input, minimum 0, step 0.10, on both label forms. The column is `DECIMAL(10,2)` after migration `ModifyAlcoholPercentage`.

**Inferred — BR-L9.** Warning is treated as a yes/no field when a note is built (`1` shown as Yes, `0` as No). The column is an integer.

**Inferred — BR-L10.** Trademark, bottle size, year, and CC list are written on update. Barcode is not written; the assignment is commented out.

**Inferred — BR-L11.** SAWIS agent is written only when the post contains `sawisAgent`. The member form posts the agent name into a read-only input of that name, so a member save can write a name into the integer `agent_id` column. **Requires confirmation** against the live form script (the input is `readonly`, not `disabled`, so browsers still submit it).

**Inferred — BR-L12.** Admin agent options are every row in `users`, not a filtered staff list.

**Inferred — BR-L13.** Approver display name is joined from `users` on `approved_by`. Approve and reject set `approved_by` to the acting user id, including on rejection.

## Status transitions

**Inferred — BR-L14.** There is no state machine. Any of the methods below write the new id directly. They do not check the previous status.

| Action | New status id | Email template type | Log action stored |
|---|---|---|---|
| Create | 1 | none | `create` |
| Approve, no provisions | 5 | 5 | `approve` |
| Approve, `labelHasProvisions == 1` | 6 | 6 | `approve` |
| Reject | 4 | 4 | `reject` |
| Contest | 7 | none | `reject` (text says the rejection was contested) |
| Manual change | posted id | none | `update` |
| Resubmit file | unchanged | none | `resubmit` |
| Edit fields | unchanged | none | `update` |

**Inferred — BR-L15.** Dashboard "pending" is status id 1, 2, or 3. "Rejected" is 4. "Approved" is 5. "Approved with provision" is 6. "Contested" is 7.

**Conflict — BR-L16.** `statusDisplay()` in `app/Common.php` maps 1 → Submitted, 2 → Approved, 3 → Rejected, anything else → "Error 0". That map does not match BR-L14 or BR-L15. Which helper is shown on a given screen depends on the view. The label list uses the joined `label_status.name`, not `statusDisplay()`.

**Inferred — BR-L17.** The status admin UI refuses edits and deletes when the name is one of: Submitted, Pending, Rejected, Approved, Contested, Resubmit. Other names can be edited. The workflow does not consult this list.

**Inferred — BR-L18.** New statuses can be given a parent and a group. Parent and group are integers. No rule was found that moves a label to a child status automatically.

**Unknown / Requires Confirmation — BR-L19.** The ids in BR-L14 are assumed to be the production primary keys of those meanings. The migrations do not insert the rows. A rebuilt database will not have id 5 meaning Approved unless someone inserts rows in that order.

## Files, provisions, and certificates

**Inferred — BR-L20.** A resubmitted file does not deactivate the previous version. Both can stay `active = 1`. Version display orders by `file_version` descending.

**Inferred — BR-L21.** File approve/reject is per file. It does not approve or reject the label.

**Inferred — BR-L22.** The certificate includes approved files (`file_approved = 1`) except type 6, then saves itself as a new type 6 version. It is generated on approve and on reject. It is not generated on contest.

**Inferred — BR-L23.** Certificate checkboxes follow status at generation time: 5 approved, 4 rejected, 6 approved with provision. Any other status leaves all three checks empty.

**Inferred — BR-L24.** Provisions are a comma-separated string on `label.provisions`, copied from the approval post. Provision names shown in the modal come from `label_provision`. No seeder fills that table.

**Inferred — BR-L25.** Upload stores files under `uploads/` using a random prefix and the original file name with spaces turned into underscores. The directory is not limited to a private disk in code. **Requires confirmation** whether the web server blocks direct download of those PDFs.

## Email

**Inferred — BR-E1.** Approve and reject enqueue mail. They do not send it. Contest does not enqueue mail.

**Inferred — BR-E2.** The template row is chosen by `label_email_template.type` equal to the status id. If no row matches, the code logs "No email template found." and still risks reading `->body` before that empty check (the body is read first). A missing template is an error path, not a silent skip.

**Inferred — BR-E3.** Placeholder tokens are `{userName}`, `{LabelNumber}`, `{Note}`, `{labelURL}`. Unknown tokens are left as written.

**Inferred — BR-E4.** The label URL in the body is `labels/labelform?id={id}`. Auto-routing on a case-sensitive system must be confirmed; the controller method is `labelForm`.

**Inferred — BR-E5.** `processed` is written as 0 on insert, 1 after manual send, and 2 when the tracking pixel is requested by a browser that is not logged in. The email tab treats any truthy `processed` as "Sent", so an open (2) displays as Sent.

**Inferred — BR-E6.** A logged-in viewer of the pixel does not update the queue. An open from a user who is also signed into the label system is not recorded.

**Inferred — BR-E7.** `send_date` can be edited. Nothing in this repository sends mail because that date has arrived.

**Inferred — BR-E8.** Manual send does not test whether `sendMail` succeeded. The queue is marked processed and a success note is written whenever the row existed.

**Inferred — BR-E9.** Outgoing display name is "Admin Overdrive", not SAWIS, and the from-address is `admin@overdriveqa.co.za`.

## Meeting groups

**Inferred — BR-G1.** A group has a name, a free-text type, and an active flag. Reassignment lists only `active = 1`.

**Inferred — BR-G2.** Reassignment does not write `label_log` or a note.

**Inferred — BR-G3.** Creating a group, renaming it, and changing active state are controller actions. Role 1 is checked for some of the page data in `LabelUserGroups::index`. The POST actions rely on the menu filter rather than a second role check inside every method. **Requires confirmation** of which menu URL (`labels`) is granted to which roles, because the first segment of `labels/groups` is `labels`.

## Tamper check

**Inferred — BR-T1.** Update, resubmit, delete, approve, reject, contest, file-approval, and manual status change must post the session `userID` and session `labelID`. A mismatch stops the request.

**Inferred — BR-T2.** Create does not use that check. The call is commented out.

**Inferred — BR-T3.** Reassign, send-now, reschedule, and previous-version lookup do not call the tamper helper.

## Registration and passwords

**Inferred — BR-P1.** Local passwords are hashed with PHP `password_hash` (bcrypt default). SAWIS comparison is SHA-256 of the stored salt concatenated with the typed password, compared to `sysPassword.hash` on the newest password id.

**Inferred — BR-P2.** The login form links to a static forgot-password page that is not in this repository. No reset token or email was found.

**Inferred — BR-P3.** Login validation requires the email field only. Empty password is possible on the validator and then fails the hash or `password_verify`.

## Cars, master file, and other starter rules

These are **Inferred** from the modules that share the codebase. They are not written up as SAWIS label policy.

**Inferred — BR-C1.** Car import reads a spreadsheet and writes a car plus cost category A, B, and C rows.

**Inferred — BR-C2.** `CarApi` keeps a session id across named steps (range, model, affordability, maintenance, recommendation, apply, credit). No SAWIS role check was found inside those methods beyond the global login and menu filters. The first URL segment is `carapi`, which is not in the menu seeder, so a fresh database denies the API at the filter.

**Inferred — BR-M1.** Master-file import has two entry points, Overland and ModelliQ. Display taste notes are cut to 200 characters.

**Inferred — BR-O1.** An organisation API client authenticates with `name` plus `api_key`, receives a JWT, and must send that bearer token on later calls. Token lifetime is 6000 seconds from issue. The signing secret in code defaults to `secret`.

## Conflicts that need a person

1. Status ids in `LabelModel` and `Home` versus names in `statusDisplay()` and versus the unlocked status table.
2. Public registration creates a Developer.
3. Role 3 and role 6 are enforced in screens and not assigned by login.
4. Create-label columns versus `NOT NULL` migration columns.
5. Mail is queued in product language ("email sent" notes exist only on the manual path) while approve itself only inserts a queue row.
6. Repository name and README name versus the SAWIS label behaviour.
