# Business rules — bi_tool_frontend_phase6

Evidence date: 2026-10-19. Source: `phase_6` at `82d7ef04`.

Rules below are what the frontend enforces or displays. The API may enforce more. Where the only source is a code comment or a client-side reconstruction, the rule is marked **Inferred** or **Requires confirmation**.

The identifiers in this file (BR-01 onward) belong to this catalogue. They are not the old capture label "BR-15". That label meant a size-level total check. `userFacingMessages.js` no longer shows it to users. The check itself is BR-24 and BR-28 here.

No separate business-rules document existed in Silicon Ark for this project. `scope/User_Flow_Scope.md` and `CHANGELOG.md` are the written sources. They are cited when they match, and contradicted when they do not.

## Identity and session

**BR-01 — Disclaimer before a session.** **Implemented.** `POST /loginAuth` success does not write `sawisAuth`. Accept on the disclaimer does (`LoginForm.jsx`).

**BR-02 — Session cookie is not HttpOnly.** **Implemented.** `js-cookie` sets `sawisAuth` with `path=/` and `sameSite: Lax`. A copy is stored in `localStorage` under `sawis_auth_v1`.

**BR-03 — Expiry is HTTP 401.** **Implemented.** `checkUnauthorizedAndLogout` ignores 403 and does not scan the body for "denied". Changelog (2026-08) matches. A comment in `filterStore.js` still says 401/403; the function it calls does not.

**BR-04 — Logout clears browser storage.** **Implemented.** Explicit logout and 401 both call `clearClientSession`, which expires JS-visible cookies and clears `localStorage` and `sessionStorage`. Capture drafts are removed because they live in `localStorage`.

**BR-05 — Disclaimer acceptance cookie lasts 30 days.** **Implemented.** `disclaimerAccepted=true`. This cookie is not the session. Clearing it is a side effect of logout because logout expires every cookie name it can see.

**BR-06 — Login payload encryption depends on the API host.** **Implemented.** Empty `REACT_APP_API_BASE_URL` or a localhost base sends plain JSON `{ email, password, typeOfLogin: 1 }`. Any other base sends `{ payload }` from AES-256-CBC. The key is an environment variable, not hard-coded. **Requires confirmation** that production sets the same key as the backend.

**BR-07 — reCAPTCHA can be bypassed by env.** **Implemented.** `REACT_APP_DISABLE_RECAPTCHA=true` (set in `.env.development`) uses token `test-bypass`. **Requires confirmation** that production builds do not set this.

**BR-08 — Mock login.** **Implemented** only when `REACT_APP_MOCK_AUTH=true`. The stand-in user is role 1 (DAFF) and cannot capture. `.env.development` sets the flag false.

## Who may see what

**BR-09 — Anonymous report access.** **Implemented.** Home, export reports, grape production, and vineyard status have no auth guard. Domestic Sales and Admin do.

**BR-10 — Value and currency need a login.** **Implemented** in the export UI: the Value prompt tab exists only when logged out; the volume tab grows a value icon when logged in; several table calls move under `/auth`. **Inferred:** the API returns currency fields only on the auth routes. This repo cannot prove the payload.

**BR-11 — Price range is a logged-in export tool, capped at five tables in the UI.** **Implemented** copy and button state in `PricesRanges.jsx`. Calls go to `/auth/getPriceRangeData` and `/auth/setPriceRangeFilterHistory`.

**BR-12 — Admin portal is `admin_role` > 0.** **Implemented.** Role 0 and missing role do not see the Admin link (`canAccessAdminPortal`).

**BR-13 — DAFF is tools only.** **Implemented.** Role 1: `canManageDomesticUsers` false, `canAccessAdminSubmissions` false, `canAccessAdminTools` true. Guards redirect `/admin/users` and `/admin/submissions` to the default path, which is tools.

**BR-14 — Super User is not a mapper operator.** **Implemented.** Role 3: tools access false, user management true, submissions true, default path `submissions`. `/admin/tools` redirects away.

**BR-15 — Dev and SU Admin share the wide admin menu.** **Implemented.** Roles 2 and 4 can open tools, users, and submissions. Default landing is tools because `getAdminDefaultPath` prefers tools.

**BR-16 — Live permission flags win.** **Implemented.** If `user.domestic.permissions` exists, `can_view_reports`, `can_capture`, `can_approve`, `can_manage_users`, and `can_assign_super_users` are read from it. Role numbers are the fallback only when that object is missing.

**BR-17 — Reconstructed permissions when `/me` is missing.** **Inferred / requires confirmation.** `reconstructDomestic` sets `can_capture` for a linked producer or roles 3 and 4, `can_approve` for 3 and 4 only, `can_manage_users` for 2, 3, and 4, `can_assign_super_users` for 2 and 4. That `can_approve` set is narrower than `canApproveDomestic`'s fallback (2, 3, 4). Which one the business wants for role 2 when `/me` 404s is not written down outside the code.

**BR-18 — Domestic reports page is login-gated, not permission-gated.** **Implemented.** Any session can open `/domestic/reports`. `canViewDomesticReports` is unused. The report HTTP calls themselves send no token.

**BR-19 — Capture requires a link, with a Super Admin exception.** **Implemented.** `can_capture` or, if no permissions object, `is_domestic_producer` or role 3 or 4. Role 4 may type any PRODKODE (`canTypeAnyProducerCode`). Others without codes see the unlinked message and are told to sign out and in after a Super User links them.

**BR-20 — Producer code `"0"` means none.** **Implemented.** `parseProducerCodes` drops empty tokens and `"0"`.

## SAWIS 7 return

**BR-21 — A return is opened by producer code, period, and country.** **Documented** in README. **Implemented** as the POST body of `openOrGetSubmission`. Country is omitted when falsy. Server-side uniqueness is **Unknown**.

**BR-22 — Period choices are the last 12 months from today.** **Documented** in `CHANGELOG.md` (not the selected period, and not a hard-coded month). Confidence in the date arithmetic: medium; the changelog is the statement, the control lives in the capture panel.

**BR-23 — Wine classes.** **Implemented** in `WINE_CLASSES`: still and sparkling are grids; fortified and wine component are lumps. Headings from the API are mapped with Afrikaans and English fragments (`wynkomponent`, `wine component`, `gefortif`, `fortif`, `vonkel`, `sparkling`). Anything else maps to still. **Inferred** that those spellings cover the lookup data SAWIS sends.

**BR-24 — Grand total is the sum of `wine_class_totals`.** **Implemented** in `volumesToSavedTotalsPayload`. Comments say the server also requires size-level subtotals to equal `grand_total`, and each size's litres to equal the sum of `TOTAAL` for that TH+HOUGRO. The server check is **Documented in a client comment**, not visible as server code here.

**BR-25 — Lumps still need a back-page total.** **Implemented.** Fortified uses HOUGRO `TOTAL`, wine component uses `WCTOTAL`, TH `T`. A cultivar and price class are chosen from lookups (`resolveLumpCulPk`, price class fallback `"O"`, cultivar fallback `"7"`). **Inferred** that code `"7"` and price class `"O"` are acceptable to the API when names do not match.

**BR-26 — Litres cannot be negative.** **Implemented.** Inputs clamp with `Math.max(0, …)`.

**BR-27 — Draft save is advisory.** **Implemented** and **Documented** in the changelog. Client mismatch, lump sync failure after a successful PUT, and wine-type mismatch do not roll back a successful totals save. They warn.

**BR-28 — Submit is strict.** **Implemented.** Submit stays blocked until there is at least one volume, over-allocated count is 0, needs-breakdown count is 0, front grid total equals back grand total, and the strip matches Step 1 per class. The user can still save a draft. The message prefix is "You can still save a draft. Submit is blocked until: ".

**BR-29 — Submit does not set status in the client.** **Implemented.** `POST …/submit` has no body. **Inferred** that the API moves the return to Submitted. **Requires confirmation.**

**BR-30 — Producer cannot edit submitted, approved, or synced returns.** **Implemented** (`statusLocked`). `forceEditable` can override; callers of that prop were not fully traced. **Requires confirmation** if any screen sets it for producers.

**BR-31 — Cancelled is not in the producer read-only list.** **Implemented** as an omission. Changelog and `scope/User_Flow_Scope.md` say cancel locks editing. The admin review page treats cancelled and missed as `editLocked`. The producer form does not. **Requires confirmation** which surface is authoritative. The API may still reject writes.

**BR-32 — Status words.** **Implemented** display: `open`, `new`, `draft`, and blank show as Open. Tones also exist for submitted, approved, synced, cancelled / canceled, and "never submitted" (same tone as cancelled). Missed is used by reopen logic and has no dedicated tone (falls through to open tone).

**BR-33 — Who may cancel.** **Implemented.** Roles 2, 3, and 4, and only when the loaded status is open (including new/draft) or submitted. Request has no body.

**BR-34 — Who may reopen.** **Implemented.** Roles 2 and 4, when status is cancelled, canceled, or missed. Role 3 cannot. Request has no body. User-facing name in the scope doc is "Open up".

**BR-35 — Approve sends a SAWIS 5 confirmation flag.** **Implemented** body `{ sawis5_confirmed: true, note? }`. The product language on the menu is "SAWIS 7 Submissions". **Unknown / Requires Confirmation** what the reviewer is confirming.

**BR-36 — Unlock notifies the producer.** **Implemented** client always sends `notify_producer: true` and `reason: "sawis5_mismatch"`. The free-text reason is `unlock_reason`. **Inferred** that the API emails the producer. Not proven in this repo.

**BR-37 — Approve is offered on open, submitted, and approved.** **Implemented** `canAct` in `SubmissionReviewPage.jsx`. Unlock's button is disabled when status is open or synced (separate from `canAct`).

**BR-38 — Review totals are compared, not auto-fixed.** **Documented** in the changelog and **Implemented** as a warning when package/size sums differ from cultivar line sums.

## Users and codes

**BR-39 — Promote makes a Super User.** **Implemented.** PUT role `admin_role: 3`. The Promote control is hidden when the row is already role 3.

**BR-40 — Demote returns a Super User to User.** **Implemented.** PUT `admin_role: 0`. Changelog matches.

**BR-41 — Only roles 2 and 4 assign super users**, unless `can_assign_super_users` is set on the permissions object. **Implemented.**

**BR-42 — The assigner id is the logged-in user.** **Implemented.** The managed row's `domestic_producer` is not sent as the actor. If the session has no numeric id, the client matches the session email to the users list, else sends 0. Sending 0 is the leftover when both lookups fail. Changelog says the bug being fixed was sending 0.

**BR-43 — Linked means flag plus at least one code.** **Implemented** in the users table. Codes are a comma-separated list with no spaces (`joinProducerCodes`).

**BR-44 — Super User code entry is not free text.** **Implemented.** `allowFreeTextCodes` is false when the signed-in role is 3 (`UsersListPage.jsx`). Role 4 on capture is the opposite: free text producer code.

## Mapper tools

**BR-45 — Lookup maintenance is four lists.** **Implemented.** Unknown cultivar matches, unknown country matches, unknown producer matches, MCC designation lookup. Required fields are marked on the form config (for example original country name, type of liquor, prod no, appl name, type product name).

**BR-46 — Import files are spreadsheet or csv.** **Documented** in `scope/User_Flow_Scope.md` (`.xlsx`, `.xls`, `.csv`). The wizard posts to `/admin/mapper_processing_sheet_queue`. This onboarding did not re-read the file-type check line by line. Confidence: medium.

**BR-47 — Process statuses have colours.** **Implemented** map in `src/lib/constants.js`: success/completed, started, uploading/processing, lookup error, error/failed, cancel/cancelled.

## Content and measurement

**BR-48 — Disclaimer text.** **Documented** in the login form and footer. SAWIS accepts no responsibility for accuracy or for actions taken from the site, and disclaims virus-related damage. The full paragraph is in `LoginForm.jsx` and `Layout.js`.

**BR-49 — Analytics.** **Implemented.** GA4 starts on a timer. No consent check is in `index.js`. **Requires confirmation** whether that matches SAWIS policy.

## Rules that must not be invented from names

- The meaning of SAWIS form numbers 5 and 7, beyond the labels in the UI.
- Whether role 2 is a SAWIS developer account or an external role. The label is "Dev".
- Whether DAFF users should see industry reports. The UI lets role 1 use the public report routes because those routes are not role-gated.
- Currency, tax, or pricing formulas. Price range data is whatever `/auth/getPriceRangeData` returns.
- Retention of submissions. Not specified in this frontend.
