# Business rules — formulys_simpleflow

Evidence date: 2026-10-14.

Source: `git@bitbucket.org:overdrive/formulys_simpleflow.git`, `master` at `07cd01b` (2024-06-21).

No business-rules document exists in the source repository. Every rule below is taken from a condition in the React source or the pipeline file. Rules marked **Inferred** are a reading of that condition, not a written policy. Form.io may enforce further rules on the server. Those rules are not in this repo and are **Unknown / Requires Confirmation**.

## Identity

### BR-001 — A session is a Form.io JWT

**Implemented.**

Login POSTs `{ data: { email, password } }` to `simflowlogin/submission`. The client stores the `x-jwt-token` response header in `localStorage` under `token` and sends that header on later Form.io calls.

There is no refresh flow, expiry check, or idle timeout in this repo. A failed `GET /current` leaves the stored token in place.

### BR-002 — Unsigned visitors are sent to `/login`

**Implemented.**

`App` navigates to `/login` whenever the auth store `user` is null. Logout clears `user`, which trips the same rule.

### BR-003 — The signed-in company is the company id on the user

**Implemented.**

After login or `GET /current`, the client loads `simflowclientdetail/submission/{user.company._id}`. It does not ask the user to pick a company at login.

If `user.company._id` is missing, that request URL is malformed. The catch logs "failed:". Provenance of the happy path: **Implemented**. Provenance of the missing-id outcome: **Inferred** from the string concatenation.

### BR-004 — The client does not check `isActive` at login

**Implemented** as an absence.

`isActive` is shown on company and user tables and can be edited on the user form. `LoginForm` and `authStore.login` never read it. Whether an inactive user can still obtain a JWT is decided by Form.io. **Unknown / Requires Confirmation.**

## Roles

Two role names are literals in the UI. Every other name is whatever Form.io returns from `simflowuserroles`.

| Rule | Condition in source | Provenance |
|---|---|---|
| BR-005 Settings link | `roleName === "Simflow Admin"` | **Implemented** |
| BR-006 Management link | Simflow Admin or Project Manager | **Implemented** |
| BR-007 Companies and Users tabs | Same as BR-006 | **Implemented** |
| BR-008 Emails tab | Simflow Admin only | **Implemented** |
| BR-009 Add company | Simflow Admin only | **Implemented** |
| BR-010 Rename company | Form.io field `businessName` disabled when role is not Simflow Admin | **Implemented** (`69afe58`, `fe3c6c9`) |
| BR-011 See every company and every user | Simflow Admin lists are unfiltered. Other roles that reach the screen add a `businessName` filter for the signed-in company | **Implemented** |
| BR-012 Assign Simflow Admin | That role is removed from the dropdown unless the editor is a Simflow Admin | **Implemented** |
| BR-013 Add or edit a user | Any role that can see the Users tab, including Project Manager. The plus control is not admin-only | **Implemented** |
| BR-014 Open `/management` or `/settings` by URL | No route guard. A signed-in user who is not a manager mounts the page. Management renders an empty tab list. Settings still shows Coming Soon | **Implemented** |

**Inferred** from BR-011 and BR-012: a Project Manager is a company-scoped administrator and cannot create another Simflow Admin or another company. The code does not say this in a comment. Form.io could still accept a hand-built request. That server rule is **Unknown**.

### BR-015 — Process visibility is `roleAccess`, with an admin exception that is not consistent

**Implemented**, and the copies disagree.

Process Hub, and the dashboard's recent-items fetch: a process is kept when `roleAccess` has an entry whose `roleName` equals the user, **or** the user is Simflow Admin. The admin clause is inside `Array.some`, so a process with an empty `roleAccess` is hidden from Simflow Admin as well.

`submissionStore.getAllSubmissions`: a process is kept only when `roleAccess` contains the user's role. There is no Simflow Admin exception.

The dashboard card grid and the task list use the store. The recent column and the Process Hub use the local filter.

**Inferred** from commit `fe3c6c9` ("allow simfow admins to see all"): the intended rule is that Simflow Admin sees every process. The store was not updated to match. Treat the store behaviour as what the dashboard grid and task list actually do.

### BR-016 — The Process Hub hides tasks

**Implemented.**

A process with `isTask` set is excluded from the hub even when BR-015 would show it. Those processes are the ones the task list tries to show.

## Tasks and cards

### BR-017 — An open task has no `simflowCardStatus`

**Implemented.**

The task list and the nav badge treat a submission as an open task when `isTask` is set and `data.simflowCardStatus` is falsy. Any truthy status removes it from the task list and the badge. The client never writes `simflowCardStatus` itself. The process form in Form.io would have to set it. **Unknown / Requires Confirmation** which form fields do that.

### BR-018 — Cards without `simflowCardName` are not listed on the dashboard

**Implemented.**

The dashboard filter returns false when the name is missing. The task-list filter does not use that guard. If `isTask` is set and status is falsy and the name is missing, it calls `.toLowerCase()` and throws.

### BR-019 — Card colour follows status and task

**Implemented** in `DashboardProcessCard.jsx`.

| Condition | Colour |
|---|---|
| `simflowCardStatus` truthy | `#00bf63` |
| status falsy and `isTask` set | `#ff9900` |
| otherwise | `#00bf63` (the initial value; the first `if` assigns the same green) |

No legend in the UI explains the colours. Meaning of green versus orange beyond the conditions above is **Inferred** as "done or not a task" versus "open task".

### BR-020 — Search is a case-insensitive substring

**Implemented** on the dashboard, task list, company table, and user table.

| Screen | Fields |
|---|---|
| Dashboard and recent column | `simflowCardName` |
| Task list | `simflowCardName` |
| Companies | `businessName` or `contactPersonEmail` |
| Users | `firstName`, `lastName`, `email`, or `company.data.businessName` |

An empty search matches every row that has the required field, because `includes("")` is true.

Company search throws if `businessName` is set and `contactPersonEmail` is null. User search throws if `firstName` is set and `company.data` is missing. Provenance: **Implemented** code path. Not runtime-tested.

### BR-021 — Lists stop at 500 rows

**Implemented.**

`fetchSubmissions` always sends `limit=500`. There is no page control. Rows past that limit are not shown. Provenance of "the business accepts a 500 cap": **Inferred**. The cap itself is **Implemented**.

### BR-022 — Recent submissions are the 15 newest by `modified`, for Simflow Admin

**Implemented.**

Sort comparator `compareDates` orders `modified` descending. The code then sets `arr.length = 15`. When more than 15 rows exist, the tail is dropped. When fewer than 15 exist, the array grows with empty slots. The later `.filter` assumes each slot has `.data`. Empty slots throw. Provenance of the padding effect: **Inferred** from assigning `length`. Not runtime-tested.

The recent rows are not links. The chevron has no handler. Only the main cards open a form.

## Profile and user records

### BR-023 — A person cannot change their own role or email on My Profile

**Implemented.**

Those inputs are `disabled`. The PUT still spreads the whole `user` object into `data`, then overwrites `firstName`, `lastName`, and `password`. Role and email therefore stay whatever was already on `user`, unless Form.io strips unknown keys. What Form.io persists from that spread is **Unknown / Requires Confirmation**.

### BR-024 — My Profile requires a new password

**Implemented.**

Both password fields are `required`, and they must be equal or the client shows "Passwords must match" and does not PUT. There is no minimum length, complexity rule, or "keep current password" option on this form.

### BR-025 — An administrator can save a user without a password

**Implemented** (`22a9d07`).

On the management form the password state starts as boolean `false`. The PUT/POST includes `password` only when that state is not `false`. The input is not `required`.

If the editor types in the field, the state becomes a string and is sent, including an empty string after the field is cleared. Provenance: **Inferred** from `onChange` always calling `setPassword`. Not runtime-tested.

### BR-026 — User create and update send a reduced user

**Implemented.**

The management payload is `firstName`, `lastName`, `email`, `userRole: { roleName }`, `company` (the full company submission chosen in the dropdown), `isActive`, and maybe `password`. It does not send the spread used by My Profile.

Required on the form: first name, last name, email (`type="email"`). Company and role are selects. `isActive` is a checkbox that toggles with `!isActive`, not with `event.target.checked`.

Non-admins only receive companies whose `businessName` equals their own, and cannot pick Simflow Admin.

### BR-027 — Editing yourself from user management reloads the session and does not close via `onSubmitDone`

**Implemented.**

When `userID === user._id`, the handler calls `autoLogin` and skips `props.onSubmitDone()`. The modal-close and list-reload path runs only for a different user.

### BR-028 — There is no delete

**Implemented** as an absence.

No user, company, submission, bounce, or block delete control exists on `master`. Commit `997a2a3` records an investigation of user delete. It did not add one.

## Company branding

### BR-029 — Process cards use the company palette when those fields are set

**Implemented.**

Fallbacks: primary `#6e7982`, secondary `#a6a6a6`, tertiary `#ff9900`. The nav logo uses `logoUrl` when set, otherwise the bundled logo. Login does not use `logoUrl`.

## Email

### BR-030 — Only a Simflow Admin sees suppressions, and only for one sender

**Implemented.**

The Emails tab requests SendGrid bounces and blocks with query `from_email="joshua@overdrive.co.za"` and `limit` 10. The address is a literal in `EmailManagement.jsx`. The page does not offer another sender, a date range, or a delete.

**Inferred:** the screen was built to inspect mail from that sender, not as a general mailbox. No comment states the policy.

## Deployment

### BR-031 — A `master` pipeline builds, uploads to the QA FTP variables, then scans

**Implemented** in `bitbucket-pipelines.yml`.

Upload uses `FTP_USERNAME_QA`, `FTP_PASSWORD_QA`, `FTP_HOST_QA`, `FTP_SITE_ROOT_QA`. No production variable names appear. The echo line names `FTP_HOST` and `FTP_SITE_ROOT`, which the upload does not use.

**Inferred** from the `_QA` suffix: this pipeline publishes a QA site. No document in the repo says that, and no production job exists here.

The build step cannot succeed while `src/App.jsx` contains the trailing `1` on the Messages import.

## Rules that are not in this repository

- Who may call each Form.io form (project-level roles and submission permissions).
- Password policy inside the `simflowlogin` and `simflowclientusers` forms.
- Which `simflowCardStatus` values mean completed, rejected, or in progress.
- The list of business processes and which roles may start them. That list is `businessProcesses1` on each company submission.
- Whether inactive users or inactive companies are blocked.
- Any relationship to the role enum in `formulys_simpleflow_backend` (`tenantUser`, `tenantAdmin`, `platformAdmin`). Those strings do not appear in this front end.
