# Business rules — label_system

Evidence date: 2026-10-10. Source: `master` at `c8b07d50`.

Rules are **Implemented** unless marked otherwise. Display names that the code expects from `label_status.name` are called out separately because the rows were not read from a database.

Nothing in the repository states these as approved business policy. They are the rules the application enforces.

## Access

1. A user without `isLoggedIn` cannot use the app, except the public exceptions in `Filters.php` (`/`, register, organisations API, tracking pixel, 404, cron, identity-unknown).
2. The first URL segment must match a `user_menu.url`, and the session role must have a `user_access` row for that menu. Otherwise the user is blocked.
3. Designers (role 5) see and open only labels where `user_designer_id` is themselves.
4. Producers (role 4) see labels whose `producer` equals a SAWIS `tblMember.strMember` linked by active `sysUserCompanyGroup` rows, excluding SAWIS security groups 5 and 14, and also labels they designed.
5. Label committee (role 3) sees only status `2` labels in their `user_groups` meeting groups. The list adds a placeholder group `99999` when they have none, so the list is empty.
6. Roles 1, 2, 7, and 9 see every active label inside the date window.
7. Upload, resubmit, and dispensation are allowed for roles 1, 7, 5, 6, 4, and 9 only.
8. Role 6 is described in an upload comment as Members/Farmers. Role 8 is described as a SAWIS user who cannot use the label system. Neither has a list or form branch. **Requires Confirmation** what they should see.
9. Designers' history hides log actions `comment`, `reply`, `agree`, `comment-agree`, and `reply-agree`, and submissions `Comment deleted` and `Reply deleted`.
10. Committee members only receive the admin form when the label's meeting group is one of theirs. There is no server-side denial if it is not. The redirect is commented out.

## What may be submitted

11. A standard upload file must have extension `pdf` and MIME `application/pdf`.
12. That PDF must be a single page. Multi-page files are rejected on this path and have a separate upload.
13. Landscape pages are rotated to portrait before the JPEG is written.
14. A password-protected PDF is treated as a failed conversion.
15. Dispensation files must be PDF, DOC, or DOCX (extension and MIME).
16. Posted mutations that call `labelFormsTamperProtection` must echo the session user id and label id. `createLabel` does not call that check.
17. Member-form required fields, when the current user created the label: SAWIS producer, year (max length 4), cultivar, and production area (`woo`). Alcohol is a number, minimum 0, step 0.10, and is not marked required in the member form. Bottle size is required.

## Lifecycle

18. A new label is stored with status `0`. The model comment calls this In Progress.
19. Completing the submission sets status `1`.
20. Assigning a meeting group sets status `2`.
21. Rejection sets status `4`, stores the rejecting user in `approved_by`, and queues email type `4`.
22. Approval with `labelHasProvisions != 1` sets status `5` ("approved as is") and queues email type `5`.
23. Approval with provisions sets status `6`, stores `provisions`, and queues email type `6` with subject suffix " with provision".
24. Contesting a rejection sets status `7` and note type `3`. This method does not queue an email.
25. A dispensation request sets status `11`, note type `6`, and file type `99`.
26. Filter buckets also include status `3` with submitted and pending, `8`, `9`, and `10` as resubmit, and `12` beside contest inside `LabelModel::getLabelsWhere`. No method that was read assigns 3, 8, 9, 10, or 12. How a label enters those states is **Unknown / Requires Confirmation** (manual status update can set any id).
27. Lists and analytics ignore `active = 0`. Delete is a soft delete.
28. Lists ignore labels created at or before `2024-04-28 00:00:00`. **Inferred** as a go-live cutoff. The reason is not documented.
29. The default list window is the last 14 days. The user can replace it. The new range is kept in the session.

## Lock and edit

30. Admin form sets `labelLockdown` when `status_name` is exactly `Approved` or `Approved with provision`.
31. Member form also locks when `status_name` is `Pending`.
32. While lockdown is set, resubmit, field update, file delete, approve, reject, contest, and comment stop with "Label can not be changed after it's been approved". Contest and comment use that same approved-wording even when the member form locked a Pending label.
33. Those status names must match `label_status.name`. They are not the numeric ids. If the live names differ, lockdown does not run. **Requires Confirmation** against production data.
34. Soft-deleted labels are not in the active list. `deleteLabel` itself does not check lockdown.

## Comments and committee

35. A comment is `label_note.type = 1`. A reply is also type `1` and points at `comment_id`. A contest note is type `3`. A dispensation note is type `6`. The form's decision note query uses type `0`.
36. The admin UI refuses to open the approval modal until every meeting-group member has commented. The message is "Not all members have commented on this label yet." The approve and reject model methods do not repeat this check.
37. A label whose meeting group has no members shows "This label is not in a meeting group that has any members." and the page script sets `labelReadyForApproval = 0`.
38. Users offered for assignment exclude roles 4, 5, and 6.

## Timing

39. Application timezone is Africa/Johannesburg.
40. A submission is marked late only when it is in the current ISO week and the submission is Tuesday at or after 14:00, or any time Wednesday. Status `0` is never late. From Thursday 14:00 through Sunday the helper returns no mark, regardless of the submission. The admin form suppresses the sentence unless status is outside `{0,2,3,4,5,6}`. Late marking does not change status or block the save.
41. Weekly analytics email runs on Monday at or after 07:00, once per week, for the previous Monday–Sunday. `?force=1` works only when `ENVIRONMENT` is not `production`.
42. Other cron jobs run when `cronjob_actions.recurrence` minutes have passed since the latest log. Recurrence `0` never runs. The dispatcher is `GET /cronJobs` with no login.

## Email

43. Decision mail is not sent inside the request. A `sent_queue` row is inserted with `processed = 0`.
44. The template is chosen by numeric type. Missing template: no row is inserted.
45. The body may contain `{userName}`, `{LabelNumber}`, `{Note}`, and `{labelURL}`. A tracking pixel is appended. The pixel URL is public.
46. CC list is a field on the label and is copied onto the queue row.
47. Dispensation cron notifies while any active label is still status `11`. It does not move the label out of that status in the method that was read.

## Meeting minutes

48. Approve and reject insert or reuse one `meeting_minutes_groups` row per meeting group per calendar day, then insert a note for the label.
49. Generated minutes can be saved as a draft, rendered to PDF, emailed, and replaced or accompanied by an uploaded signed document.
50. Public holidays in `public_holidays` are consulted when the minutes header date is calculated.

## Data the form treats as read-only for members

51. Designer name and SAWIS agent are read-only on the member form. Approver, status, the latest type-0 note, and the created date are read-only. Updated date and updated-by are editable inputs on the member form. Whether the save path honours those two inputs was not traced field by field. **Requires Confirmation** if producers must not change the audit timestamps.

## Starter rules that are still active

52. Seeded role name is Developer only (`UserRole` seeder). Other role names are operational data.
53. Organisations API is outside the login filter and documents a bearer token from `POST organisations/login`. That API is not part of the label workflow.
54. CSRF protection is disabled in `Filters.php`.

## Conflicts

- `Labels::index` comment calls roles 1, 7, 2, and 9 "Developers, Staff, WCA Board and Admin". `MenuModel` names them Developer, Sawis Admin, SAWIS Staff, and WCA Board, in a different id order. The queries agree on who sees all labels. The labels disagree.
- Member lockdown includes Pending. Admin lockdown does not.
- Member form rewrites status to `pending` when email is unprocessed, after lockdown has already been decided, and uses a different capitalisation than the Pending check.
- README says the product features are authentication, authorisation, registration, and menu management. The label rules above are not in the README.
