# Business rules — unity-skeleton

Evidence date: 2026-09-30. Revision: `master` `7b94f92`.

No business-rules document existed in the repository or in `silicon_ark/unity-skeleton/` before this file. Rules below are what the code enforces. Each rule is marked **Implemented** or **Inferred**. None of these were found as a signed policy. Where the code looks accidental, that is stated.

## Identity and access

### BR-001 — Guest vs signed-in routes

**Implemented.** A route with `auth: []` is for guests only (`FuseUtils.hasPermission`). Login, register, `/registerTest`, and OTP use that. Every other generated route allows `superuser` or `user`. A guest sent to a protected path is redirected to `/login` and the attempted path is kept as `redirectUrl`. A person whose `role` is non-empty and who opens a guest route is redirected to `redirectUrl` or `/`.

Evidence: `src/@fuse/FuseUtils.js`, `src/@unity/UnityAuthorization/UnityAuthorization.js`, `src/app/configs/routesConfig.js`.

### BR-002 — Cognito is the sign-in authority

**Implemented.** The login screen calls `Auth.signIn`. It does not call the mock `GET /api/auth`. The access token stored for later requests is the Cognito access token (`signInUserSession.accessToken.jwtToken`).

Evidence: `LoginSaga.js`. The mock login remains in `src/@fake-db/api/Auth.js` and is not used by this saga.

### BR-003 — Every completed sign-in is a superuser

**Implemented.** After `GET /unity/uuid/{username}`, the client sets `role` to the string `superuser` when a row exists and when it does not. The API fields `strUser`, `strEmail`, and `strCognitoUuid` are copied. No role field from the API is read.

**Requires Confirmation** whether every Cognito user should be a superuser. The code does not consult groups or privileges before assigning the role.

Evidence: `LoginSaga.js` (`asyncUserObjRequest`).

### BR-004 — Empty lookup still enters the app

**Implemented** on the login path only. A response whose parsed body has no element `[0]` still returns a user object: display name `newUser`, email the literal `strEmail`, uuid the literal `strCognitoUuid`, role `superuser`.

The session-restore path does not do this. It destructures `[0]` and, on throw, stores the error as the user payload.

Evidence: `LoginSaga.js`, `InitAuthSaga.js`.

### BR-005 — Unconfirmed Cognito users confirm before they get a role

**Implemented.** `UserNotConfirmedException` does not set user data. The app opens `/otp` with the email in history state and asks for a confirmation pin. Other Cognito errors surface `e.message`.

Evidence: `LoginSaga.js`.

### BR-006 — Token expiry is the `exp` claim

**Implemented.** `isAuthTokenValid` returns false when `jwtDecode(token).exp` is less than `Date.now()/1000`. There is no clock-skew allowance in this function. An expired token clears `jwt_access_token`.

Evidence: `InitAuthSaga.js`.

### BR-007 — Privileges do not gate routes

**Implemented.** `UnityAuthorization` calls `hasPermission` on `route.auth` only. Privilege strings on navigation items are not passed into that check.

`hasPrivilege` itself (**Implemented** in `FuseUtils.js`):

- role `superuser` → true, privilege list ignored
- privilege null or undefined → true
- otherwise the user's `privileges` array must contain an entry whose `description` equals the string, or any string when the requirement is an array

Nav components hide an item only when permission **and** privilege both fail. Items in `navigationConfig.js` and `moduleDB` have no `auth` property, so permission passes and the item stays visible.

**Inferred** from those two conditions: the privilege catalogue is editable data, and it does not currently change what a `superuser` can open. Whether that is the intended rule is **Requires Confirmation**.

### BR-008 — Module list is not filtered by the caller

**Implemented.** `POST /api/getAllowedModules` returns the whole `moduleDB`. The posted `uuid` and `role` are ignored.

Evidence: `src/@fake-db/api/Modules.js`, `InjectModules.js`.

### BR-009 — Superuser group seed

**Implemented.** `GroupDB` starts as one group: id `0000000000000000000000000001`, type `superuser`, privileges `"*"` (the string star, not a list).

Evidence: `src/@fake-db/seeds/Groups.js`.

### BR-010 — Screen and module privilege names

**Implemented** as seed data, not as a check that runs on every route:

| id | type | systemPrivilege | description |
|---|---|---|---|
| 001 | screen | true | `viewThemeSettingsScreen` |
| 002 | screen | true | `viewUsersScreen` |
| 003 | screen | true | `viewModulesScreen` |
| 004 | screen | true | `viewGroupsScreen` |
| 005 | screen | true | `viewPrivilegeScreen` |
| 009 | module | false | `viewSALES` |
| 010 | module | true | `viewTicketingModule` |
| 011 | module | true | `viewExampleModule` |

`viewRolesScreen` is required by the Manage nav parent and is not in this seed. `viewProfile` is set on the privileges route config and is dropped by `FuseUtils.setRoutes`, and it is not in this seed.

Adding a module inserts a new privilege whose description is `module.privilege` (`src/@fake-db/api/Modules.js`).

The users table shows a row when the viewer role is `superuser`, or when `privileges` contains `{displayName}View`. The comparison uses `find(...) !== {}`, and `Array.find` returns the object or `undefined`, never `{}`. **Inferred:** the non-superuser branch is true whenever `find` returns an object, and the `!== {}` clause does not mean what a reader might expect. Cognito login does not put a `privileges` array on the user, but BR-003 makes the viewer a superuser, so the first clause passes.

## Registration and confirmation

### BR-011 — Sign-up identity is the email

**Implemented.** `Auth.signUp` uses `username: email`, the password, and `attributes: {}`. Display name and group arguments on the saga are not sent.

Evidence: `RegisterSaga.js`.

### BR-012 — Password rules on the forms

**Implemented.** Login and register fields are required and must be at least 4 characters. Register collects `validatePassword` and does not require it to equal `password`. Cognito may still reject the password; that policy is not represented in this repository. **Unknown / Requires Confirmation** what the user pool requires.

### BR-013 — Failed sign-up still reports success

**Implemented.** `asyncRequest` catches `Auth.signUp` errors, logs them, and does not throw. The saga then always shows "We sent a code to your email!" and dispatches `REGISTER_SUCCESS`. Navigation to `/otp` happens only inside the successful `try`.

### BR-014 — Confirmation outcome

**Implemented.** `Auth.confirmSignUp` must return the string `SUCCESS`. Any other result, including a swallowed exception (`undefined`), shows "Wrong/Expired OTP please resend or contact your admin" and stays off the login redirect.

### BR-015 — Mock register is a different rule and is unwired

**Implemented** in `src/@fake-db/api/Auth.js`, **not** called by `RegisterSaga`. If it were called it would reject a duplicate email ("The email is already in use"), reject an empty display name, create role `user`, copy `group.privileges`, and add a privilege `{displayName}View`. Cognito sign-up does none of that.

## Sales

These rules are **Implemented** as form shape and mock endpoints. They are **Inferred** as business policy only in the sense that the field names exist. No separate policy document was found.

### BR-016 — A sales client is the schema in `NewUserSchema.js`

Fields include client, company number, CloudHealth linkage, AWS account, address, contact, country, opportunity source and date, AWS sales rep, AWS account manager, APN listing / stage / dates, Silicon billing status, migration and assessment dates, credit redemption fields, CSAT, case study, margin, and notes. Nested `creditTypes` ship with two default rows titled "Active Credit" and "POA Credit". Boolean operational flags (`cloudHealthReports`, `creditGranted`, `sla`, and others) default to false. `creditGranted` is commented "not mapped yet".

`companyNumber` is assigned twice in the same object. The later assignment wins.

### BR-017 — Sales data stays in the browser

**Implemented.** Sales sagas post only to `/api/...` mock routes, which read and write `localStorage` (`UnityLS`). They do not call the `unity` API. Seed runs only when the storage key is missing, so later code changes to the seed do not refresh an existing browser.

### BR-018 — Credit types and sales types are lists of their own

**Implemented.** Adding a credit type posts `/api/addCreditType` and `/api/updateClientCreditTypes`. Adding a sales type posts `/api/addSalesType` and `/api/updateClientSalesTypes`. Separate screens exist at `/sales/credit` and `/sales/type`.

## Tickets

### BR-019 — A new ticket has eight fields

**Implemented.** Name, Description, Board, Status, Priority, Department, Client, AssignedTo (`AddTicketModal.js`). The form state starts as empty strings.

### BR-020 — Boards are a fixed list in the client

**Implemented.** Maintenance Requests, Project Tickets, Service Requests, with hard-coded ids in `AddTicketModal.js`. The comment calls this a mock data dump. Boards are not loaded from the API.

### BR-021 — Ticket writes are local

**Implemented.** Add, update, delete, tasks, and documents use `/api/*` mock handlers in `src/@fake-db/api/Tickets.js`. `MAX_ATTACHMENT_SIZE` is not applied.

### BR-022 — Only the list tab is a feature

**Implemented.** Tabs named assigned, completed, Tab 4, and Tab 5 render empty markup. They do not filter tickets.

## Profile and settings

### BR-023 — The profile page is not the signed-in person

**Implemented.** The header is the string "John Doe" and a fixed avatar file. It does not read `auth.user.data.displayName` or `strEmail`.

### BR-024 — Home is the profile route

**Implemented.** `path: "/"` exact redirects to `/profile`.

## Data lifetime

### BR-025 — Mock tables are not reset on each load

**Implemented.** `seed()` writes a table only when `localStorage.getItem(name)` is null. Clearing the rule requires clearing site storage. redux-persist does not store `auth` or `SalesModule`; the mock tables are separate keys (`authDB`, `moduleDB`, `PrivilegesDB`, `GroupDB`, ticket and sales keys).

### BR-026 — 401 on axios means invalid token

**Implemented** as an interceptor in `InitAuthSaga.js`: status 401 shows "Invalid access_token" and calls `logoutUser()` without dispatching it. **Inferred:** the Redux user may remain until another dispatched logout runs. Mock handlers return 200 for most writes, so this interceptor mainly matters for pass-through requests.

## Explicit non-rules

The following were searched for and not found as enforced behaviour:

- No server-side row security in this repository (there is no server).
- No rate limit.
- No captcha on login or register.
- No password-reset transaction.
- No check that a sales client or ticket belongs to the signed-in uuid.
- No mapping from Cognito groups to `viewSALES` or other privileges.
