# Business rules — dms_archive (WCA Archive)

Evidence date: 2026-10-17.

Source: `git@bitbucket.org:overdrive/dms_archive.git`, `main` at `2bcf278`.

Rules below are copied from behaviour in code, seeds, and in-repo task notes. A rule marked **Inferred** is a reasonable reading, not a signed-off policy. Do not treat code as a decision to change the policy.

## BR-001 — Role names that exist

**Rule.** The application recognises these `user_role.role_name` values: Developer, System Admin, Admin, Company Admin, Company User, System User, Viewer.

**Provenance.** **Implemented** in `UserModel::getAssignableRoleIDs` and `useraccess_helper.php`. **Documented** in `scope/Task_role_dropdown_rules.md`.

**Conflict.** `UserRole` seeder does not insert `Developer`. It can insert `System Admin` and `Viewer` twice on an empty table because the "already exists" check uses different names from the inserted names, and it reads existing names only once before the batch. **Requires Confirmation** which roles production actually has.

## BR-002 — Who may assign which role

**Rule.**

| Actor role | May assign |
|---|---|
| Developer | Admin, Viewer, System User |
| Admin | System Admin, System User, Viewer |
| System Admin | System Admin, System User, Viewer |
| System User | System User, Viewer |
| Viewer | none |
| Company Admin, Company User, any other name | none (default branch of the switch) |

Developer dropdowns list every role row. Every other dropdown lists only assignable roles, and also hides Company Admin, Company User, and Admin. Roles are hidden in the UI. They are not deleted from `user_role`.

**Provenance.** **Documented** in `scope/Task_role_dropdown_rules.md`. **Implemented** in `UserModel`. The task file's matrix header says Developer can assign Admin, Viewer, and System User, and the code matches. The task file also says "Developer sees all", which the dropdown method does, even for roles Developer cannot assign (Company Admin, Company User, System Admin, Developer). That is a small difference between "sees" and "may assign".

## BR-003 — Who may manage roles and the user admin menu

**Rule.** Only Developer and System Admin may manage roles (role CRUD and the menu matrix): `can_manage_user_roles`. Access to the Users menu is a row in `user_access` for the menu whose `url` is `users`: `can_access_users_menu`.

**Provenance.** **Inferred** as policy from helper comments that state the rule in those words. **Implemented** as the functions.

## BR-004 — Menu grant

**Rule.** After login, a request must match a menu the user's role can access (`Authorization` filter), except the public and special paths listed in `app/Config/Filters.php` (`home`, ChatGPT, RAG, tracking, cron, deploy, slack, auth pages, organisations, store-api).

**Provenance.** **Implemented**. The exception list is wider than the document screens. Whether RAG and ChatGPT should be available to every logged-in role is **Unknown / Requires Confirmation**.

## BR-005 — Download

**Rule.** `can_download_documents` is true only for System Admin. That is the org-wide right.

A specific document may still be downloaded when:

- the role is System Admin, or
- the document's category scope is `vault` and the user owns the folder or has an active share on it (or a descendant via `can_view_vault_folder`), or
- the user is a Viewer and `UserDocumentModel::checkDocumentAccess` allows that document.

Everyone else is denied for that document.

**Provenance.** **Implemented** in `can_download_document`. **Inferred** that "System Admin only" in the helper docblock describes the blanket right, not the only download path. The two functions would conflict if read as one rule. The specific-document function is the one that encodes the exceptions.

## BR-006 — Edit, delete, share to a Viewer

**Rule.**

- A Viewer cannot edit a document.
- A non-Viewer can edit a document when `can_modify_vault_document` is true. For organisation scope that function returns true. For vault scope it returns true only for the folder owner.
- Delete is allowed for System Admin and Developer only (`can_delete_documents`).
- A Viewer cannot share a document to a Viewer.
- For a vault document, only the folder owner may share it to a Viewer. Shared vault users are read-only.
- For an organisation document, any non-Viewer who reaches the endpoint may share it (`can_share_document_to_viewer` returns true).

**Provenance.** **Implemented**. The helper comments state this directly.

## BR-007 — Folders and vault access

**Rule.**

- Viewer cannot open the folders tab or the personal vault (`can_access_document_folders_tab` / `can_access_vault`).
- Developer and System Admin may manage document folders. Any other role may manage them only when that role has `user_access` on the menu whose `url` is `categories`.
- A vault folder can be managed only by `owner_user_id`, and only when `scope` is `vault` and `active` is 1.
- Read access to a vault folder includes the owner's descendant tree and every active share's descendant tree.
- Upload or move into a personal vault folder that the user does not own is rejected. Message in `Documents.php`: "Documents can only be uploaded or moved into your own Personal Vault folders."
- Admin category maintenance is scoped so vault folders are not treated as organisation categories (commit `180f4c1`).

**Provenance.** **Implemented**. Ownership message is **Implemented** copy.

## BR-008 — RAG does not index the vault

**Rule.** A document whose category scope is vault is not indexed and is removed from the vector store when it moves into the vault. Organisation documents may be indexed.

**Provenance.** **Implemented** in `document_helper.php` (`isDocumentInVaultScope`, `shouldIndexDocumentInRAG`). **Inferred** reason: vault content is personal and must not appear in shared semantic search. The code does not state that reason.

## BR-009 — Viewer time window

**Rule.** If `UserModel::hasTimeRestriction` is true for the logged-in user, document routes and mailbox routes are refused outside the allowed window. The user is redirected to `blocked` with "Access denied. You are outside your allowed access time window."

`docs/README.md` describes three assignment modes: a start/end datetime, hours counted from login, and permanent access. Permanent access is the absence of a restricting row.

The filter does not check the role name. Any user with a time restriction is subject to it. **Inferred** from `docs/README.md` that the product intent is Viewer-only. **Implemented** behaviour applies to whoever has the flag.

**Provenance.** Mixed, as above.

## BR-010 — Document file rules

**Rule.** Uploads must use extension pdf, jpg, jpeg, or png. Application size limit is 100 MB. PHP `upload_max_filesize` and `post_max_size` are reported to the user when the server rejects the body first. Title length is 3 to 255 characters. Original file name is truncated to 255 characters and must be at least 3 characters.

**Provenance.** **Implemented** in `Documents::upload`. The 100 MB figure is code, not a sentence in `docs/README.md`. Commit `08cbc4b` is titled "upload limitation changes". The approved business number is **Requires Confirmation** if 100 MB was a technical cap rather than a policy.

## BR-011 — Seeded document types

**Rule.** The archive starts with three types: Legislation, Labels, Minutes. Users may add further tabs, including a year.

**Provenance.** Types are **Documented** by the seeder descriptions. Extra tabs are **Implemented**.

## BR-012 — Bulk email send rights and worker

**Rule.**

- `can_send_bulk_email_campaigns` is false only when the role name is System User. Viewer is not excluded by this function.
- A campaign is eligible to queue when `is_active` is 1, status is `draft` or `scheduled`, and `date_to_send` is at or before now.
- One worker tick sends at most 50 recipient jobs (`batchSize`), hard-capped at 500 if a caller passes a limit.
- A job may be attempted 3 times (`maxAttempts`).
- A job left in `processing` for 15 minutes is treated as stale.
- Queue ETA assumes the cron runs every 5 minutes (`cronIntervalMinutes`). That value is a display assumption in config, not proof that the server crontab is every 5 minutes.
- Bulk sends skip per-message BCC and keep the SMTP connection open for the batch.
- The attachment is cleared after each send.

**Provenance.** **Implemented** in `app/Config/BulkEmail.php` and `SendBulkEmailCampaigns`. The System User exclusion is **Inferred** as a product rule from the helper comment "System User excluded". Whether Viewer, Admin, and Company User should send campaigns is **Requires Confirmation**.

## BR-013 — Email log retention shape

**Rule.** A campaign log row does not store the full rendered HTML and the attachments for every recipient. Preview is built from the template and substitutions.

**Provenance.** **Implemented** (commit `ec1b940`). **Inferred** motive: database size. Not written as a retention policy.

## BR-014 — Tracking links are public

**Rule.** Open pixels, click redirects, unsubscribe, and custom pixels do not require a session. They are rate-limited by `EmailTrackingThrottle`.

**Provenance.** **Implemented**.

## BR-015 — Cron window

**Rule.** `GET cronJobs` does nothing on Saturday and Sunday, and does nothing before 08:00 or from 23:00 onward, unless the query flag `override` is set. A comment in `CronJobs.php` attributes the 08:00–23:00 window to "Wessels". In `production`, the query parameter `key` must equal the source constant `add your security key here`. Outside production the key is not checked.

**Provenance.** **Implemented**. The weekday window is an explicit code rule. The key value being a placeholder is **Requires Confirmation** before anyone relies on it as a control.

## BR-016 — Deploy webhook

**Rule.** `/deploy/trigger` and `/deploy/complete` are not session-authenticated. They require the shared `deployment.webhookToken` and pass `DeployWebhookThrottle`. The trigger starts a host script with `exec` and tells the caller a finish email will follow.

**Provenance.** **Implemented**.

## BR-017 — Mailbox

**Rule.** Inbound mail is not received by the web request. A scheduled `php spark mailbox:poll` reads IMAP and creates or appends tickets. Outbound reply and compose go through the mailbox SMTP settings. HTML from mail is sanitised before display.

**Provenance.** **Documented** cron example in `docs/README.md` (every 5 minutes). **Implemented** command and sanitiser. The 5-minute schedule is an example, not a crontab in the repo.

## BR-018 — 2FA default

**Rule.** Login consults setting name `2FA`. The seed `TwoFactorAuthSetting` sets the value to true and `docs/README.md` says 2FA is enabled by default. `getSettingValue('2FA', false)` uses false when the setting row is missing. So a database that never ran the seed behaves as 2FA off.

**Provenance.** **Documented** default is on. **Implemented** fallback is off. **Requires Confirmation** for production.

## BR-019 — Inactive and blocked users

**Rule.** Blocked IP rows and inactive users are consulted on the login path (`BlockedUsersModel`, user `active` flags). System users can be activated or deactivated per company (`system_users/changeUserActive`, `changeUserActiveCompany`).

**Provenance.** **Implemented** presence. The full condition (which column, which message) was not extracted line by line. **Requires Confirmation** if a written policy exists outside the repo.

## BR-020 — Legacy retail rules are not enforced here

**Rule.** Routes for products, orders, label status, fault finder, and company vendor numbers do not have controller classes in this tree. No business rule from those screens can be executed by this code.

**Provenance.** **Implemented** absence. Views under `app/Views/edioverdrive/` and `scope/SAWIS_Label_Phase2_v1.2_202505.pdf` show the archive still carries SAWIS / label material as documents or leftovers. Whether label workflow rules still apply inside the DMS (as documents of type Labels) rather than as the missing LabelStatus module is **Unknown / Requires Confirmation**.

## Rules that must not be invented from names

- No pricing, retention period, or legal-hold rule was found.
- No rule states that Legislation documents are immutable.
- No rule states a maximum number of vault shares.
- AWS, Salesforce, and SMS helpers do not by themselves establish a business process.
