# PRD — siliconscan

Evidence date: 2026-10-03. Source: `git@bitbucket.org:overdrive/siliconscan.git`, `master` at `56c90dc0` (2024-01-31, "latest code base , copy of label system").

There is no prior PRD in Silicon Ark. The repository README describes an "Admin Overdrive Starter". The running screens and the database name `labelsystem` implement a SAWIS wine-label workflow. Requirements below come from that README and from the code. Provenance is marked on each item.

Whether operators use this repository, or a different repository that this commit was copied from, is **Unknown / Requires Confirmation**.

## Product purpose

People submit wine label PDFs. Staff review them, approve them, approve them with provisions, or reject them. A designer can contest a rejection. Approval and rejection send an email and write a Wine and Spirit Board certificate PDF.

Provenance: **Implemented** (`Labels.php`, `LabelModel.php`, `Email.php`). The README does not describe labels. It describes authentication, authorization, registration, and menu management. Provenance of that starter description: **Documented** (`README.md`).

The SonarQube project key is `overdrive_label_system_AYr5qv21RY8iIL6fFGD4`. Provenance: **Documented** (`sonar-project.properties`).

## Users and roles

Role ids are hardcoded. The only seeded role name is Developer for id 1. Other names come from comments in controllers. Those comments do not all use the same word for the same id.

| Id | Name used in code | What the label screens do | Provenance |
|---|---|---|---|
| 1 | Developer in the seeder. Comments also say Dev Admin, Admin, and Developers | Sees every label. Opens the admin label form. May create and resubmit files. Status, meeting-group, and email-template screens render only for this id | Seeder **Documented**. Behaviour **Implemented**. The extra names are **Inferred** from comments |
| 2 | Sawis Staff in `LabelUserGroups`. The label-list comment says Staff or Admin | Sees every label and the admin form. Listed with ids 1 and 3 as meeting-group users | **Inferred** from comments. List behaviour **Implemented** |
| 3 | Board / Sawis Board | Label list is limited to `label.status = 7` (contested in the dashboard). Opens the admin form | **Inferred** name. Filter **Implemented** |
| 4 | Not referenced | No branch | **Unknown** |
| 5 | Designers | Sees only labels whose `user_designer_id` is their user id. Opens `labelFormMember`. Hidden from the SAWIS outbound menu | **Implemented**. Name is a comment |
| 6 | Members [Farmers] in a comment on create and resubmit | Those two actions allow this id. `Labels::index` has no branch for it, so the list data is unset | **Inferred** name. Gap **Implemented** |
| 7 | Grouped with Developer, Staff, and Admin in list comments | Sees every label and the admin form. Not included in the meeting-group user query (`whereIn` 1, 2, 3) | **Implemented** behaviour. Display name **Unknown** |

Self-registration always stores `inputRole` 1. Provenance: **Implemented** (`Welcome::registration`).

The users seeder inserts one account: full name Developer Tester, username `jaco@overdrive.co.za`, role 1. The password is a short numeric string hashed in `app/Database/Seeds/Users.php`. It is not repeated here. Provenance: **Implemented**. Whether that account exists in any shared database is **Unknown / Requires Confirmation**.

## Main workflows

### W1 — Sign in

Trigger: a person opens `/`.

- If the session already has `isLoggedIn`, the app redirects to `home`.
- Otherwise it shows `common/login`.
- Posting `GetLogin` or posting to `/` with `inputEmail` looks up `users.username`.
- The password is checked with `password_verify` against the stored hash.
- Success sets session keys `username`, `role`, `isLoggedIn`, `userID`, and an empty `labelID`, then redirects to `home`.
- Failure sets a flash message "Your ID or Password is Wrong" and redirects to `/`.

`inputEmail` is required. The password field is not in the validation rule. Provenance: **Implemented** (`Welcome::index`).

Logout is `welcome/logout`, which destroys the session.

### W2 — Register

Trigger: GET `register`, then POST `register`.

- Email must be unique on `users.username`.
- Password is required and must match the confirmation.
- The new user is stored with role 1.
- Success shows the login view with a flash message. It does not start a session.

Provenance: **Implemented**. The README lists user registration as a feature. Provenance of that sentence: **Documented**.

### W3 — Open the dashboard

Trigger: GET `home` after login.

Counts use `LabelModel::getLabelsWhere`:

| Tile | Query |
|---|---|
| Total submissions | designer filter only |
| Pending approval | status in (1, 2, 3) |
| Approved | status = 5 |
| Approved with provision | status = 6 |
| Rejected | status = 4 |
| Contested | status = 7 |

Roles 1, 7, 2, and 3 count labels whose `user_designer_id` is not null. Role 5 counts only that user's id. Other roles use `IS NULL`, which does not match a normal designer id. Provenance: **Implemented** (`Home::index`).

The same page still queries `app_session` for session, credit, and outcome charts. The credit percentage is forced to 0. Provenance: **Implemented**.

### W4 — Submit label files

Trigger: `Labels::createLabel`.

- Allowed roles: 1, 7, 5, 6. Anyone else gets "You are not allowed to do this!"
- At least one uploaded file is required.
- Extension must be `pdf` (case-insensitive).
- The file is stored under `uploads/` with a random prefix.
- File input names map to types: `formFileSupportedPDF` = 2, `formFileLabelPDFFront` = 3, `formFileLabelPDFBack` = 4, `formFileLabelPDFNeck` = 5, anything else = 1.
- A `label` row is inserted with status `1`, `user_designer_id` and `created_by` set from posted `userID`.
- A `label_log` row is written with action `create`.

Tamper protection is commented out on this method. A TODO says it breaks the function. Provenance: **Implemented**.

Wine fields (barcode, alcohol, warning, bottle size, year, cultivar, WSB number, production area) are not set on create. They are updated later. Provenance: **Implemented** (`createLabelFiles` versus `updateLabelData`).

### W5 — Edit label details

Trigger: `Labels::updateLabelData`.

`labelFormsTamperProtection` requires the posted `userID` and `labelID` to match the session. On mismatch it returns JSON and stops.

Saved columns: `barcode`, `alcohol_percentage`, `warning`, `bottle_size`, `year`, `updated_by`. A note lists fields whose `prev-*` values changed. Provenance: **Implemented**.

### W6 — Resubmit one file

Trigger: `Labels::resubmitLabelFile`.

Same role gate and PDF rule as create. The new `file` row uses posted `labelFileSelectedType` and `file_version` = selected version + 1. A note says the type was resubmitted. The previous file is not set inactive. A TODO in the model says that choice is open. Provenance: **Implemented**.

### W7 — Approve, reject, or contest

Trigger: the matching method on `Labels`, which calls `LabelModel`.

Tamper protection runs first.

| Action | Status written | Email | PDF |
|---|---|---|---|
| Approve, `labelHasProvisions` not 1 | 5, comment in code: approved as is | type `5`, subject "Label #id has been approved" | `labelApprovalPDF` |
| Approve, `labelHasProvisions` is 1 | 6, stores `provisions` | type `6`, subject includes "with provision" | `labelApprovalPDF` |
| Reject | 4, stores the rejection note | type `4` | `labelApprovalPDF` |
| Contest | 7, stores the contest note | none | none |

Each of those writes a `label_note` and a `label_log`. Approve and reject set `approved_by` to the posted user id. Contest does not. Provenance: **Implemented**.

The email type is the status number. `label_email_template.type` is joined to `label_status.id` on the template screen. Whether status ids 4, 5, and 6 exist as `label_status` rows is **Unknown / Requires Confirmation**. There is no seed for `label_status`.

### W8 — Certificate PDF

Trigger: a successful approve or reject.

`labelApprovalPDF` builds an mPDF document titled as The Wine and Spirit Board, covering Wine of Origin, IPW, and estate brandy. It writes `uploads/approvalcerts/labelNo{id}Approvalcert{timestamp}.pdf` and inserts a `file` row with type 6.

Checkbox logic in that method:

| `label.status` | Checkbox the code sets | Comment in the switch |
|---|---|---|
| 1 | Approved | "Approved" |
| 4 | Rejected | "Rejected" |
| 6 | Approved, provided that | "Approved with provision" |
| 5 | none | no case |

Approve-as-is stores status 5, so the approved checkbox is not ticked. Status 1 is what create stores, and the PDF treats 1 as the approved tick. Provenance: **Implemented**. This disagrees with the dashboard, which treats 5 as approved and 1 as pending. See business rule BR-STATUS.

Provision ticks are the numbers found in the comma-separated `label.provisions` value. The sentences are hardcoded for ids 1 through 8 (blend, sugar class, export, export to the USA, field of vision, 2 mm lettering, bag-in-box filling date, can filling date). Provenance: **Implemented** in the HTML template.

The certificate names Chanelle Samuels, Administration (Labels), and hardcodes SAWIS and WSB contact lines. Provenance: **Implemented** template text. Whether that is the current signatory is **Unknown / Requires Confirmation**.

The PDF is modification-protected with a hardcoded owner password. The password is not repeated here.

### W9 — Manual status and per-file decision

- `manualLabelSubmissionStatusUpdate` sets `label.status` to the posted `labelStatusID`, writes a note, and logs action `update`. It does not send mail.
- `labelFileApprovalStatus` sets `file.file_approved` from posted `fileApproved` (1 approved, otherwise rejected) and logs it.

Provenance: **Implemented**. No extra role check was found inside these methods beyond the global filters and tamper protection.

### W10 — Meeting groups

Role 1 can open `labels/groups`, create a group, rename it, toggle `active`, and assign users. The user list on that screen is roles 1, 2, and 3.

`reassignToMeetingGroup` batch-updates `label.meeting_group_id` from a comma-separated id list. Provenance: **Implemented**. No role check was found inside `reassignToMeetingGroup` itself.

### W11 — Email and tracking

`Email::sendEmailNotification` loads `label_email_template` by `type`, replaces `{userName}`, `{LabelNumber}`, `{Note}`, and `{labelURL}`, appends a SAWIS logo image and a 1×1 pixel, sends SMTP mail, and inserts `sent_queue` with `processed` 1.

The pixel URL is `tracking-pixel?code=sub{labelId}trId{uniqueCode}`. If the viewer is not logged in, `EmailPixelTracking::index` stores the code and returns a transparent PNG. A logged-in viewer still receives the PNG and is not logged. Provenance: **Implemented**.

If no template row matches, the code writes "No email template found." to `log.txt` and does not send. Provenance: **Implemented**.

### W12 — Organisations API

Documented in comments in `Routes.php` as form calls on `/organisations`. The controller reads JSON.

- `POST` body is not how login works. `Organisations::login` reads JSON `name` and `api_key`, looks up `organisations`, and returns a JWT. Expiry is `time() + 6000` seconds. The HMAC secret is the literal word `secret`.
- List, show, create, update, and delete require `Authorization: Bearer`. The list payload key is `incident_type`.

Session filters skip `organisations` and `organisations/*`. Provenance: **Implemented**. The route comment that says form POST login is **Documented** and disagrees with the controller.

## Functional requirements

### FR-001 — Session gate

The system redirects a request to `/` when `isLoggedIn` is not true, except `/`, `getLogin`, `register`, `organisations`, `organisations/*`, and `tracking-pixel`.

Evidence: `app/Config/Filters.php`, `Authentication.php`.

Implementation: Implemented.

Confidence: High.

### FR-002 — Menu gate

After login, the first URL segment must match `user_menu.url`. The role must have a `user_access` row for that menu. Missing menu redirects to `/`. Missing access redirects to `blocked`.

Exceptions: `/`, `getLogin`, `register`, `blocked`, `home`, `Welcome/*`, `organisations`, `organisations/*`, `chatGPT/*`, `tracking-pixel`.

Evidence: `Authorization.php`, `Filters.php`.

Implementation: Implemented.

Confidence: High.

The seeded menu urls are `home`, `users`, `menuManagement`, `crudGenerator`, `edit`, `list`, `files`, `media`. There is no seeded url `labels`. A role cannot open `/labels/...` until a menu row with url `labels` exists and is granted. Provenance: **Implemented** seed versus filter. Whether a deployed database already has that row is **Unknown / Requires Confirmation**.

### FR-003 — Label list visibility

Role 5 sees own labels. Roles 1, 7, and 2 see all labels. Role 3 sees status 7 only. Other roles do not get a defined row set.

Evidence: `Labels::index`.

Implementation: Implemented.

Confidence: High.

### FR-004 — PDF-only label files

Create and resubmit accept extension `pdf` only.

Evidence: `Labels::createLabel`, `Labels::resubmitLabelFile`.

Implementation: Implemented.

Confidence: High.

### FR-005 — Approval mail and certificate

Successful approve or reject sends one email to the designer's `users.username` and writes a certificate PDF.

Evidence: `LabelModel::approveLabel`, `rejectLabel`, `Labels::labelApprovalPDF`.

Implementation: Implemented. Certificate checkboxes do not follow status 5. See W8.

Confidence: High for the send and the file write. Low for "the PDF matches the decision" because of the status switch.

### FR-006 — Starter admin

A signed-in user with menu access can manage users, roles, and menu permissions, and can open menu management and the CRUD generator.

Evidence: `Users.php`, `Routes.php`, `README.md`.

Implementation: Implemented. README provenance: Documented.

Confidence: High.

### FR-007 — Public pixel

`GET tracking-pixel` does not require a session.

Evidence: `Filters.php`, `EmailPixelTracking.php`.

Implementation: Implemented.

Confidence: High.

## Integrations

| System | Purpose | Trigger | Direction | Auth | Failure |
|---|---|---|---|---|---|
| MySQL `labelsystem` | All product data | Every request that hits a model | App to database | `env` user `root`, empty password in the sample file | CodeIgniter database errors. Not custom-handled in the label methods |
| SMTP `smtp.overdriveqa.co.za:587` | Label mail | Approve, reject | App to SMTP | Username in `Email.php`. Password is committed there and is not copied into this PRD | `sendMail` always calls `printDebugger`. Return value of `send()` is ignored |
| SAWIS Online | Industry screens (members, farms, harvest, blends, and others) | User clicks the header menu | Browser to the other site | Not in this repo | Link only. Disabled item: Pending Member Changes |
| OpenAI | Chat page | GET `chatGPT/sent` | App to OpenAI | `OPENAI_API_KEY` is commented out in `env` | **Unknown** beyond the helper existing |
| SonarQube | Static analysis | Push to `master` | Pipeline to SonarQube | `SONAR_TOKEN` and `SONAR_HOST_URL` repository variables | Pipeline file has no other step |
| Salesforce | Named in a helper | Commented call in `Home::index` | None active | Commented | Not called |
| Africa's Talking | Named in a helper | Commented call in `Home::index` | None active | Commented sandbox block. The key is not copied here | Not called |

SAWIS host selection (`sawis-menu.php`): base URL containing `localhost` links to `http://sawis.local`; containing `qa` links to `https://qa.sawisonline.co.za`; otherwise `https://sawisonline.co.za`. Provenance: **Implemented**.

## Constraints

- PHP 8.0 and the extensions listed in the README.
- Label uploads are PDF.
- Session driver in `env` is the database handler, table `sessions`, match IP true.
- CSRF and honeypot filters are present and commented out in `Filters.php`.
- `setAutoRoute(true)` is on, so a controller method can be reached by convention when the menu gate allows the first segment.
- `app.baseURL` is empty in config. Email links and asset URLs depend on it being set.

## Behaviour that conflicts

| Topic | One place | Another place |
|---|---|---|
| Status 1 | Dashboard pending; create sets 1; `statusDisplay` says Submitted | PDF switch comment and tick say Approved |
| Status 2 and 3 | Dashboard pending | `statusDisplay` says Approved and Rejected. `LabelModel` never writes 2 or 3 |
| Status 5 | Dashboard and `approveLabel` say approved | PDF has no case for 5 |
| Role 1 name | Seeder: Developer | Comments: Admin or Dev Admin |
| Organisations login | Route comment: form POST | Controller: JSON `name` and `api_key` |
| Product name | Repo and Sonar key: label system / siliconscan | README and composer: Admin Overdrive |

These are recorded again in `business-rules.md`. They need a human decision before anyone treats one side as the rule.

## Unknown / Requires Confirmation

- Production URL, and whether this git repo is what production runs.
- Live contents of `user_role`, `label_status`, `label_provision`, and `user_menu`.
- Whether role 6 (member) is a real user, given the list screen does not handle that id.
- Whether car, master file, site health, ChatGPT, Adminer source under `app/Views/projects/`, and the Selenium runner are still required.
- Who signs the certificate in production. The template hardcodes one name.
- Whether committed secrets have been rotated. This PRD does not repeat them.
