# Business rules — bitool_frontend

Evidence date: 2026-10-19. Revision: `master` `937e09cb`.

Rules below are what the client enforces or what the UI copy states. Server-side rules of the BI API are **Unknown** unless the client shows a specific response shape. Each rule is marked **Documented** or **Inferred** when it is not a direct statement in a product spec. There is no separate business-rules document in the source repo.

`phase_6` / `qa` add domestic permissions and `admin_role` values. Those rules are in `silicon_ark/bi_tool_frontend_phase6/business-rules.md`. They are not rules of `master`.

## Access

| ID | Rule | Evidence | Label |
|---|---|---|---|
| BR-001 | Home, Exports, Grape Production, and Vineyard Status open with no account | `AppRoutes.js` has no guard | **Inferred** from the route table. The pages render for `user === null` |
| BR-002 | Currency comparison on volume tables is offered only when a user object exists | `Tables.jsx` banner and heading suffix | **Implemented** UI rule |
| BR-003 | The Reports tab (RSA exports) is rendered only when `user` is set | `Reports.js` | **Implemented** |
| BR-004 | Price-range data is requested only when a token is present. Guests see a login prompt | `PricesRanges.jsx` | **Implemented** |
| BR-005 | The Value tab is rendered only when `user` is absent, and it only prompts for login | `Reports.js`, `LogoutValue.jsx` | **Implemented** |
| BR-006 | Navbar label for a signed-in user is the email substring before `@` | `Navbar.js` `AuthBtn` | **Implemented** |
| BR-007 | Account creation and password reset happen on sawis.co.za, not in this app | Links in `LoginForm.jsx` | **Documented** by the links |

## Sign-in and session

| ID | Rule | Evidence | Label |
|---|---|---|---|
| BR-008 | Sign In stays disabled until reCAPTCHA calls `onChange` with a value, and while a login request is loading | `LoginForm.jsx` | **Implemented** |
| BR-009 | Accept sends `typeOfLogin` `1` with email and password. The reCAPTCHA token is not in the POST body | `LoginForm.jsx`, `authStore.login` | **Implemented**. Server use of `typeOfLogin` is **Unknown** |
| BR-010 | A successful login stores `jwtToken` and `biUser` cookies for path `/` and sets `disclaimerAccepted` for 30 days | `authStore.js`, `LoginForm.jsx` | **Implemented** |
| BR-011 | The disclaimer cookie is not checked on later visits. The app does not block use when it is missing | No reader of `disclaimerAccepted` except removal on logout | **Inferred** |
| BR-012 | Logout clears `disclaimerAccepted`, `jwtToken`, and `biUser` | `authStore.logout` | **Implemented** |
| BR-013 | On load, a session is restored from `jwtToken` and `biUser`. If those are missing and the cookie string contains `:::`, a legacy split is tried | `autoLogin` | **Implemented** |
| BR-014 | Reporting and price-range HTTP 401, while a user is set, logs out once and alerts "Your session has expired, please log in again." | `authSession.js` | **Documented** in `README.md` and `NewKnowledgeBase.md`, **Implemented** |
| BR-015 | HTTP 403 means not permitted and must not clear the session. Network, parse, 4xx/5xx other than 401, and aborted requests must not clear the session | `NewKnowledgeBase.md`, `authSession.test.js` | **Documented** and **Implemented** for the helpers that call `checkUnauthorizedAndLogout` |
| BR-016 | The RSA exports report, grape production calls, and vineyard calls do not use that helper | `GeneratedReportPivotTable.jsx`, `GenericPivotTable.jsx`, the two page files | **Inferred** gap: a 401 on those calls leaves the cookie session in place |
| BR-017 | Changing the token resets the Exports tab to Overview | `Reports.js` `useEffect` on `userToken` | **Implemented** |
| BR-018 | Login details must be kept confidential. When an authorised person leaves a registered user, that user must remove the person from its sign-in profile. SAWIS disclaims liability for misuse | Disclaimer text in `LoginForm.jsx` and `Layout.js` | **Documented** legal copy. No client screen implements user administration on `master` |

The disclaimer also states that SAWIS accepts no responsibility for accuracy, completeness, viruses, or actions taken from the material, and that the site is not a substitute for professional advice. **Documented**.

## Export filters and figures

| ID | Rule | Evidence | Label |
|---|---|---|---|
| BR-019 | Default producer is `Industry` when the API list includes it | `getAvailableFilters` | **Implemented** |
| BR-020 | Default year is the API `LatestYear`. Default ending month is the API `LatestMonth`. Default period is 12 | `getAvailableFilters` | **Implemented** |
| BR-021 | Dropdown edits on the export sidebar apply when the user clicks Apply, except Select all countries, which writes at once | `Sidebar.js`. Commit `30f818e` | **Implemented** |
| BR-022 | Clear sets producer to the first option, year to the first option, ending month to `defaultMonth`, period to 12, and the other filters to empty | `Sidebar.js` reset handler | **Implemented**. This year is the first option, not `LatestYear` |
| BR-023 | If every product type, package type, or cultivar option is selected, the request sends an empty list for that filter. Country sends the selected values as they are | `getReportingData` | **Inferred** meaning of the empty list: "no restriction". Server behaviour **Requires confirmation** |
| BR-024 | Missing producer selection is sent as `["Industry"]` on the main reporting payload | `getReportingData` | **Implemented** |
| BR-025 | Reporting period label is `START_MON YEAR - END_MON YEAR`, with the start month `period` months before the ending month | `calculateReporting_Period` | **Implemented** |
| BR-026 | The first volume table uses `ProducerCompareYearPivotTable` only when the user is signed in and the producer selection is non-empty and does not include `Industry`. Other signed-in users get `CompareYearPivotTable`. Guests get `PivotTable`. Tables 2–5 use `ProducerPivotTable` on that same producer test and do not check `user`; every other selection uses `PivotTable`. An empty array is truthy in JavaScript, so a stored selection of `[]` takes the producer component | `Tables.jsx` | **Implemented** |
| BR-027 | Package subtitle is the one selected package type, otherwise the literal `PACKAGED/BULK` | `Overview.jsx`, `Tables.jsx` | **Implemented** |
| BR-028 | Chart totals use K / M / B with two decimals (labels use zero decimals). Table body integers use French grouping. Totals use two decimals and a space thousands separator. Strings that already contain `%` are left as-is | `src/lib/helpers.js` | **Implemented** |
| BR-029 | Overview chart requests stay on the public URL even when the user is signed in. Table requests gain the `/auth` prefix and the Authorization header | `filterStore.js` | **Implemented**. Whether the public chart payload is the same dataset as the auth tables is **Unknown** |

## Price range

| ID | Rule | Evidence | Label |
|---|---|---|---|
| BR-030 | A signed-in user may show at most five price-range tables | Copy and the length check in `PricesRanges.jsx` | **Implemented** in the UI. The store does not reject a sixth row by itself |
| BR-031 | Price-range years offered from the filter payload are those greater than or equal to 2014 | `setPriceRangeOptions` | **Implemented** |
| BR-032 | Saved tables are stored and loaded by the signed-in email | `saveFilters`, `getPriceRangeFilterHistory` | **Implemented** |
| BR-033 | Until history returns an array, the client holds one seed table: 2023 full year, Chardonnay, UNITED KINGDOM, Rand, monthly (`group_by_year` `"0"`) | `PRFilters` initial state | **Implemented** |
| BR-034 | `group_by_year` `"1"` is labelled Yearly on the Excel download. Any other value is labelled Monthly | `PriceRange.jsx` | **Implemented** |

## Grape production and vineyard status

| ID | Rule | Evidence | Label |
|---|---|---|---|
| BR-035 | Report rows are not requested until a filter list has been stored | `GrapeProduction.js`, `VineyardStatus.js`. Commit `d5b14c8` | **Implemented** |
| BR-036 | If the filter API fails or returns an empty list, the client uses the hard-coded fallback (years 2021–2025, named regions, cultivars, age bands, WHITE/RED) | `filtersFallback` | **Implemented** fallback, not a confirmed live list |
| BR-037 | Initial year, Clear on Year, and Reset set Year to the previous calendar year when building the reset object | `grapeProductionStore.js`, `SidebarFilters.jsx` | **Implemented**. Supersedes the 2024 literal in commit `c7e010c` |
| BR-038 | Single-selects start at the first API value. Other multi-selects start empty | `initializeSelectedFilters` | **Implemented** |
| BR-039 | A multi-select with `max` 0 does not cap the count. A positive `max` stops further selection | `SidebarFilters.jsx` | **Implemented** |
| BR-040 | The first report generates once when its table mounts. Later filter commits regenerate a table that has already generated | `GenericPivotTable.jsx` | **Implemented** |
| BR-041 | When the payload has both WithSultana and WithoutSultana series, the user can toggle sultana. If only WithSultana exists, that series is shown | `processData` | **Implemented** |
| BR-042 | A note ending in parentheses shows the parenthetical text in red | `parseNotes` | **Implemented** |

## Deploy script (operational, not a user rule)

| ID | Rule | Evidence | Label |
|---|---|---|---|
| BR-043 | `master_deploy.php` refuses a request that has no `tkn` query parameter | The file dies with "Nope" | **Implemented** |
| BR-044 | An IP allow-list is defined, and the check that would reject other IPs is commented out | `master_deploy.php` | **Implemented** as disabled. **Requires confirmation** |
| BR-045 | The case labelled BI calls the same BIQA deploy URL as the BIQA case | `master_deploy.php` | **Implemented**. Likely unintended. **Requires confirmation** |

Tokens and the downstream query secret in that file are committed in the source repo. They are intentionally omitted here.

## Conflicts

- `DEVELOPER_DOCUMENTATION.md` describes OpenID Connect and route-level auth. `master` uses email and password against `/loginAuth` and gates features inside the page.
- `NewKnowledgeBase.md` states the 401 rule for the reporting and price-range work. The RSA report and the grape and vineyard pages were not updated to call the same helper (BR-016).
- `bi_tool_frontend_phase6/business-rules.md` describes DAFF, Dev, Super User, and SU Admin. Those roles are on `phase_6`, not on this `master` tip.
