# PRD — dms_archive (WCA Archive)

Evidence date: 2026-10-17.

Source: `git@bitbucket.org:overdrive/dms_archive.git`, `main` at `2bcf278` (2026-10-16, "update back to 50").

There was no prior PRD in Silicon Ark. Requirements below are recovered from `docs/README.md`, `docs/PROJECT_RULES.md`, `docs/bulk_email_spec.md`, `scope/`, routes, controllers, models, migrations, seeds, and tests. Provenance is labelled.

Secrets in the committed `env` file (database password, SMTP password, OpenAI key, encryption key) are not reproduced here.

## Product purpose

**Documented.** `composer.json` description: "WCA Archive - Document Management System". Homepage: `https://wineauthority.co.za`. `docs/README.md` titles the project "WCA Archive System Starter" and lists authentication, authorisation, user registration, menu management, and a document management system with OCR, unified search, categories, role-based access, time restrictions, and audit logging.

**Implemented.** After login the user reaches `home`, which exposes dashboard statistics for documents. The primary working areas that have controllers are documents (including personal vault), users, bulk email, mailbox, chat, system settings, monitoring, RAG status, categories, and developer menu tools.

**Inferred.** The product is the operational archive for the organisation at wineauthority.co.za (legislation, labels, and minutes are the seeded document types). That organisational purpose is not written as a requirement.

**Unknown / Requires Confirmation.** What "WCA" stands for. Whether this repository is the production system at wineauthority.co.za or one of several DMS codebases (`wca_dms` in Silicon Ark is a different, thinner Admin Overdrive tree).

## Users and roles

### Seeded role names

`app/Database/Seeds/UserRole.php` attempts to insert: System Admin, Admin, Company Admin, Company User, Viewer, System User.

The seeder's existence checks do not match the names it inserts (it looks for `Developer` before inserting `System Admin`). On an empty `user_role` table the batch can contain duplicate System Admin and Viewer rows and no Developer row. Provenance: **Implemented** seeder behaviour. Whether production data was corrected by hand is **Unknown / Requires Confirmation**.

### Roles the application code recognises

| Role name | Behaviour found in code | Provenance |
|---|---|---|
| Developer | May manage folders, delete documents, and manage roles. Sees every role in dropdowns. May assign Admin, Viewer, and System User. Comment in `UserModel` treats role id 1 as Developer for list visibility. | **Implemented** in `useraccess_helper.php` and `UserModel`. **Not inserted** by `UserRole` seeder. |
| System Admin | Same folder, delete, and role-management rights as Developer. Only role for which `can_download_documents()` is true (org-wide download). May assign System Admin, System User, and Viewer. | **Implemented** |
| Admin | Hidden from non-Developer dropdowns. Assignable only by Developer. Shares the System Admin assignable-role list if someone has this role. | **Implemented** |
| Company Admin | Hidden from non-Developer dropdowns. Not in the assignable-name switch, so nobody's `getAssignableRoleIDs()` returns it. | **Implemented** as hidden legacy |
| Company User | Same as Company Admin. | **Implemented** as hidden legacy |
| System User | May assign System User and Viewer. Cannot send bulk-email campaigns (`can_send_bulk_email_campaigns` returns false only for this name). Cannot delete documents or manage roles. | **Implemented** |
| Viewer | No assignable roles. Cannot use the folders tab or personal vault. Cannot edit documents or share them to another viewer. Document access is through `user_documents` assignments. Download of an org document requires a valid assignment. | **Implemented** |

`scope/Task_role_dropdown_rules.md` (dated 2025-02-11) documents the same matrix. That file is a **Documented Requirement**. The matching methods are `UserModel::getAssignableRoleIDs()` and `getRolesForDropdown()`.

### Other actors

| Actor | Expected behaviour | Provenance |
|---|---|---|
| Person who is not logged in | `Authentication` filter redirects to `/`, except login, reset, 2FA, tracking pixels, `cronJobs`, `deploy/*`, `slack`, `email/test`, organisations, and store-api. | **Implemented** |
| Person whose account is inactive or whose IP is blocked | Login path uses `BlockedUsersModel` and user active flags. Exact message text was not fully traced. | **Implemented** presence; wording **Unknown** |
| Cron caller | `GET cronJobs`. Outside Monday–Friday 08:00–22:59 the script exits unless `override` is set. In production the query `key` must equal the placeholder `add your security key here`. | **Implemented** |
| Deploy pipeline | `GET /deploy/trigger` with `deployment.webhookToken` (query `token` or header `X-Deploy-Token`), throttled. Starts a host script. `deploy/complete` sends the finish email. | **Implemented** |
| Email recipient | Opens `email-tracking/pixel/{token}`, click, and unsubscribe routes without a session. Throttled. | **Implemented** |
| IMAP mailbox | `php spark mailbox:poll` imports messages as tickets. Not a human user of the web UI. | **Implemented** |

## FR-001 — Sign in

**Requirement.** A person opens `/` (`Welcome::index`). POST `GetLogin` submits email and password. If the setting named `2FA` is disabled, login completes. If it is enabled, the system emails a code and the person submits it at `ConfirmCode`. Failed or incomplete login stays on the public pages.

**Who.** Any person with a user row.

**Trigger.** POST `GetLogin`, then POST `ConfirmCode` when 2FA is on.

**Inputs.** Email, password, and when required a confirmation code.

**Outputs.** Session `isLoggedIn`, `userID`, `role`. Redirect into the app, or back to the 2FA form.

**Validation.** Credential check in `Welcome` / `UserModel`. 2FA setting via `getSettingValue('2FA', false)`. Seed `TwoFactorAuthSetting` documents default value `"true"`.

**Errors.** Invalid credentials, blocked user, and a reset-password error that commit `e6e2031` says was fixed. Exact messages were not copied here.

**Provenance.** **Documented** in `docs/README.md` (2FA section). **Implemented** in `Welcome.php` and `app/Config/Filters.php`.

**Confidence.** High for the flow. Medium for the production default of the 2FA setting, because it depends on whether the seed ran.

## FR-002 — Password reset

**Requirement.** `GET forgot-password` collects an address. `POST sendResetPasswordLink` sends a link. `GET reset-password` then `POST resetPassword` sets a new password. These routes are outside the login filter.

**Who.** A person who cannot sign in.

**Provenance.** **Implemented** routes. Commit `e6e2031` ("Fix Reset password error") is evidence the flow was broken and changed. The defect detail is not in the commit subject.

**Confidence.** Medium.

## FR-003 — Home dashboard

**Requirement.** `GET home` shows document statistics. JSON helpers: `home/getDashboardStats`, `getDocumentsByMonth`, `getDocumentsByDay`, `getDocumentsByType`, `getUploadRate`.

**Who.** A logged-in user. `home` is excluded from the menu-grant filter, so any authenticated session can request it.

**Provenance.** **Implemented** routes. What each chart contains was not opened line by line.

**Confidence.** Medium.

## FR-004 — Document library

**Requirement.** A logged-in user opens `documents/`. They can list, search, view, and (when permitted) upload, edit, download, delete, and share documents. Allowed upload extensions are `pdf`, `jpg`, `jpeg`, `png`. Application max size is 100 MB (`100 * 1024 * 1024`), and PHP `upload_max_filesize` / `post_max_size` can reject the file first.

**Who.** Roles that pass the menu grant for the documents menu. Viewers are further limited by assignment and time windows.

**Trigger.** Routes under the `documents` group in `app/Config/Routes.php`.

**Inputs.** File, title (3–255 characters), folder/category, type, year, reference number, tags.

**Outputs.** A `documents` row, a stored file, an audit log entry, and (when the document is not in a personal vault) a RAG index.

**Validation.** Extension and size checks in `Documents::upload`. Folder write checks in the upload/move path. Rate limit on folder mutations: 30 requests / 60 seconds (comment in `Documents.php`).

**Errors.** Permission denied, missing temp file, invalid type, file too large, folder not writable.

**Evidence.** `Documents.php`, `docs/README.md` OCR section, tests `DocumentFoldersTest`, `DocumentsFolderSidebarViewTest`, `CanDownloadDocumentHelperTest`.

**Provenance.** **Documented** feature list in `docs/README.md`. Size, extensions, and rate limit are **Implemented**.

**Confidence.** High.

## FR-005 — Document types and tabs

**Requirement.** Seeded types are Legislation (`legislation`), Labels (`labels`), and Minutes (`minutes`). Users can add and delete their own type tabs, including a year dimension (`user_document_type_tabs`, migrations 2026-05-25 through 2026-05-28).

**Who.** A documents user. Tab preference routes: `documents/tab-preferences`, `documents/add-tab`, `documents/delete-tab/{id}`.

**Provenance.** Types are **Documented** by `DocumentTypes` seeder and `docs/README.md`. Year tabs are **Implemented** in migrations and `DocumentsTabPreferencesTest`.

**Confidence.** High.

## FR-006 — Folders and personal vault

**Requirement.** Organisation folders and a personal vault share `document_categories`. Vault rows use `scope = vault` and `owner_user_id`. A user who is not a Viewer may open the vault. Only the owner may create, rename, move, or deactivate their vault folders, and may share or unshare a folder. A share grants read access to that folder and its descendants (`vault_folder_shares.active = 1`). Shared recipients do not gain write access. Organisation documents stay on role permissions.

Vault documents are excluded from RAG indexing (`shouldIndexDocumentInRAG` / `isDocumentInVaultScope`).

**Who.** Non-Viewer for access. Owner for writes and shares.

**Trigger.** `documents/folders/*` and `documents/vault/*`.

**Evidence.** Migrations `2026-07-29-100000_AddVaultScopeToDocumentCategories.php` and `2026-07-29-100001_CreateVaultFolderSharesTable.php`. Helpers in `useraccess_helper.php`. Tests `VaultPermissionsTest`, `DocumentModelVaultRlsTest`, `VaultAccessHelperTest`.

**Provenance.** **Implemented**. A short doc commit `b1cf9d9` ("docs: personal vault permissions feature") exists in the source repo. The rule text above is taken from the helper comments and tests, not from a separate signed-off business rule.

**Confidence.** High.

## FR-007 — Viewer document sharing and time limits

**Requirement.** A non-Viewer may assign a document to a Viewer (`users/assignDocument`, `users/bulkAssignDocuments`, `documents/shareToViewer/{id}`). Access can be a datetime range, hours from login, or permanent (`user_documents`, `user_access_times`). Revoke endpoints exist. While a user has a time restriction and is outside the window, `TimeAccessFilter` redirects to `blocked` with flash message "Access denied. You are outside your allowed access time window."

The filter runs for every `documents*` request and for the `mailbox` group, not only for the Viewer role. It no-ops when `UserModel::hasTimeRestriction` is false.

**Who.** Sharer: any non-Viewer who may share that document (vault: owner only). Recipient: Viewer.

**Provenance.** **Documented** in `docs/README.md` (2025-01 viewer profile section) and `scope/Task_viewer_user_profile.md`. **Implemented** in `Users.php`, `UserDocumentModel`, `TimeAccessFilter`.

**Confidence.** High for the mechanism. The exact UI labels for the three limit types were taken from `docs/README.md`.

## FR-008 — Search

**Requirement.** `GET documents/search` runs a unified search. `document_helper.php` provides database-side RAG search (`searchDocumentsWithRAG`) over chunked text (chunk size 1000) stored as embeddings. Vault-scoped documents are not indexed.

**Who.** A user who can open documents. Result rows are expected to respect vault RLS in `DocumentModel` (test `DocumentModelVaultRlsTest`).

**Provenance.** **Documented** as "Database + OCR + AI semantic search" in `docs/README.md`. Chunking and vault exclusion are **Implemented**.

**Confidence.** Medium. The ranking blend of SQL versus vector hits was not fully traced.

## FR-009 — Chat and RAG status

**Requirement.** `GET chat` plus `chat/send` and `chat/stream` talk to the model with conversation history. Memories and document contexts are stored and embedded (`chat_memories`, `document_contexts`). `GET rag/status` shows configuration, statistics, and indexed documents. `POST rag/status/reindex` reindexes. `rag/*` is excluded from the menu-grant filter, so any logged-in user can call it.

A separate page `chatGPT` posts to `chatGPT/sent`.

**Provenance.** **Implemented**. `docs/MEMORY_SYSTEM_OPTIMIZATIONS.md` and `docs/CHAT_SECURITY_AUDIT.md` are source-repo notes, not re-audited here.

**Confidence.** Medium.

## FR-010 — Bulk email

**Requirement.** A user with the bulk-email menu can maintain recipients, groups, templates (GrapeJS builder), and campaigns. A campaign has a subject, template, audience (groups and extra recipients), send datetime, optional document or uploaded attachments, and a status. Due campaigns (`is_active = 1`, status `draft` or `scheduled`, `date_to_send` in the past) are queued by `php spark send:bulk-email-campaigns`. Each tick sends up to `Config\BulkEmail::$batchSize` jobs (50 at `2bcf278`). Failed jobs retry up to 3 times. Stuck `processing` rows reset after 15 minutes. The UI can show queue progress and can set a failed campaign back to sending.

Email logs store template id and substitutions for preview rather than the full HTML and attachments (commit `ec1b940`).

Public tracking: pixel, click, unsubscribe, and custom tracking pixels. System User cannot pass `can_send_bulk_email_campaigns`. Other roles, including Viewer, are not rejected by that helper; the menu filter may still hide the screen.

**Who.** Operator with menu access, except the send helper's System User exclusion. Recipient of the email is external.

**Provenance.** **Documented** in `docs/bulk_email_spec.md` (written as a porting spec; it predates the 2026-09-16 queue table and does not list `c_email_campaign_queue`). Queue behaviour is **Implemented** in `BulkEmailCampaignQueueService` and `app/Config/BulkEmail.php`.

**Confidence.** High for the worker. Medium for every UI tab, which was not clicked.

## FR-011 — Transactional email queue

**Requirement.** `sendMail()` can insert into `sent_queue`. `GET cronJobs` runs active `cronjob_actions` in `order`. Documented processors `sendScheduledEmails()` and `getAllUnsentEmails()` send up to `items_process_total` (documented default 10) per run. Schedule window is weekdays 08:00–23:00 unless `override` is present.

**Provenance.** **Documented** in `docs/EMAIL_QUEUE_AND_CRON_SETUP.md`. The weekday window is **Implemented** in `CronJobs::index`. The route line number in that doc (line 19) is stale; the route is now later in `Routes.php`.

**Confidence.** Medium.

## FR-012 — Mailbox

**Requirement.** `mailbox` lists tickets. A user can open a ticket, reply, compose, close, switch view mode, and download an attachment. Inbound mail is polled by `php spark mailbox:poll` (IMAP, suggested every 5 minutes in `docs/README.md`). HTML is sanitised (`MailboxHtmlSanitizer`, HTMLPurifier).

**Who.** A logged-in user who passes menu grant and the time-access filter.

**Provenance.** **Documented** mailbox env block in `docs/README.md` and `docs/mailbox_env.md`. **Implemented** controller, command, and tests (`MailboxAuthTest`, MIME fixtures).

**Confidence.** High for the presence of the workflow. Ticket status names were not enumerated.

## FR-013 — Users and profiles

**Requirement.** User administration: list, create, update, delete, menu permission matrix, role access. Profile (`users/profile`, `users/userprofile`) updates name and contact fields, resets password, shows audit activity, assigned documents, and session timing. User id is kept in session (`users/setSelectedUser`) rather than in the profile URL, as described in `docs/README.md`.

Role assignment follows FR roles table. Viewer cannot assign roles.

**Provenance.** **Documented** in `docs/README.md` and `scope/Task_role_dropdown_rules.md`. **Implemented** in `Users.php` and `UserModel`.

**Confidence.** High.

## FR-014 — System settings

**Requirement.** `system_settings` manages cron jobs, public holidays, email logs, writable log files, sent queue, ChatGPT settings, and (via routes) transport methods and fault types.

Fault-type and transport-method endpoints call `SystemSettings` methods. Whether those methods still have tables and views is **Unknown / Requires Confirmation**. They look inherited from the Admin Overdrive shell.

**Provenance.** Routes are **Implemented**. Completeness of fault types is **Unknown**.

**Confidence.** Medium.

## FR-015 — Monitoring

**Requirement.** `monitoring` shows system usage and alert thresholds. CLI `php spark monitor:system` logs threshold breaches.

**Provenance.** **Implemented** command and routes. Threshold numbers were not extracted.

**Confidence.** Medium.

## FR-016 — Deploy

**Requirement.** On push to `main`, the pipeline installs PHP dependencies, runs SonarQube, uploads changed files by FTPS, then calls the production deploy webhook. The webhook rejects a bad token, throttles, and requires `exec()`. Success text tells the operator that a finish email will be sent.

**Provenance.** **Implemented** in `bitbucket-pipelines.yml` and `DeployWebhook.php`. Test: `DeployWebhookTest`.

**Confidence.** High for the pipeline definition. Whether FTP variables are set in Bitbucket is **Unknown**.

## FR-017 — Scope browser

**Requirement.** `GET scope`, `scope/userGuide`, and `scope/feature/{name}` serve the HTML scope pack under `scope/html/` (DMS, chat, email, menu, monitoring, RAG, reports, settings, users).

**Provenance.** **Implemented** routes and files. Those HTML pages are project narrative, not independently verified against every screen.

**Confidence.** Medium.

## Integrations

| System | Purpose | Direction | Auth | Failure |
|---|---|---|---|---|
| MySQL | All persistent data. Template DB name `extract`, driver MySQLi. | App → DB | Username/password in env | CodeIgniter database exceptions. Not catalogued per action. |
| SMTP | 2FA, password reset, bulk campaigns, sent queue, deploy notices | App → mail host | `email.SMTP*` and `mailbox.smtp*` | Campaign job marked failed and retried up to 3 times. Other mail failure behaviour was not fully traced. |
| OpenAI | Chat, embeddings, template AI assistant | App → API | `OPENAI_API_KEY`. Template model `gpt-4`. | Not fully traced. RAG status screen is the operator view. |
| IMAP | Inbound mailbox | Mail host → app via poll | `mailbox.imap*` | Poll command logs. Ticket import tests cover MIME parsing, not live IMAP errors. |
| Tesseract / Poppler | OCR on upload | Local process | PATH or `TESSERACT_PATH` | `docs/README.md`: log shows `command not found` when binaries are missing. |
| FTP (pipeline) | Deploy files | Bitbucket → host | `FTP_USERNAME`, `FTP_PASSWORD`, `FTP_SERVER` | Step fails the pipeline. |
| Deploy webhook | Run host `deploy.sh` | Pipeline → `https://wineauthority.co.za/deploy/trigger` | `DEPLOY_WEBHOOK_TOKEN` must match `deployment.webhookToken` | Non-200 fails the pipeline step. |
| SonarQube | Static analysis on `main` | Pipeline → Sonar | `SONAR_TOKEN` | Exclusions drop most application PHP from the scan. |
| AWS | SDK is required by Composer | Unknown | Unknown | **Unknown / Requires Confirmation** whether application code calls it. |
| Slack | Route `slack` → `Slack::sendMessage`, excluded from login | App → Slack | Not opened | **Unknown** |
| Africa's Talking / Salesforce | Helpers exist | Unknown | Unknown | **Unknown** whether called. |

## Important constraints

- PHP version documents disagree (`^8.4` vs 8.3 vs pipeline 8.3). See project plan.
- Auto-routing is on (`setAutoRoute(true)`), so undefined explicit routes can still hit a controller method.
- CSRF applies to every POST, with extra explicit `csrf` filters on many bulk-email and document posts.
- Global CSRF is not in the `before` globals; it is in `$methods['post']`.
- Menu grant (`isGranted`) does not cover `home`, `chatGPT/*`, `rag/*`, tracking, cron, deploy, slack, or the public auth routes.
- Sonar analysis excludes almost the whole `app/` tree, so a green scan is not evidence of review coverage.
- `docs/overview.md` must not be treated as requirements. It contradicts `composer.json`.

## Workflows

### Upload a document

1. User is logged in and inside any time window that applies to them.
2. User chooses an organisation folder they may write, or their own vault folder.
3. Upload step 1 stores a temp file. Step 2 (`documents/upload`) checks permission, title, extension, and size.
4. File is stored, `documents` row written, audit log written.
5. If the folder is not vault scope, text is extracted and indexed for RAG.

### Share a vault folder

1. Owner opens the share action for a vault folder they own.
2. `documents/vault/share` writes `vault_folder_shares`.
3. The other user can read that folder and descendants. They cannot edit, move, or re-share.

### Send a campaign

1. Operator builds a template and a campaign with audience and `date_to_send`.
2. Cron or a manual run of `php spark send:bulk-email-campaigns` queues due campaigns, then sends up to 50 recipient jobs.
3. Each job uses the template plus substitutions. Attachments are cleared after the send so memory does not accumulate (commit `207ecde`).
4. Opens and clicks hit the public tracking routes.

### Inbound support mail

1. Cron runs `php spark mailbox:poll`.
2. New IMAP messages become tickets or replies (`MailboxThreadingService`).
3. An agent opens `mailbox/ticket/{id}`, replies, or closes.

## Requirements that could not be confirmed

- Legal expansion of "WCA" and the customer contract this archive serves.
- Production database name, base URL, and whether `wineauthority.co.za` is this repository.
- Whether a Developer role row exists in production.
- Whether AWS, Salesforce, Africa's Talking, Slack, products, orders, and fault finder are still required.
- Whether Viewer should be blocked from bulk email by product rule, or only by menu setup.
- Ranking rules inside unified search.
- The real production cron security key. The source constant is a placeholder.
