# Business rules — bitool_dotnet

Evidence date: 2026-10-19. Source: `develop` at `2223f18b` unless a rule says `master`.

Rules are **Inferred** from implementation. The repository has no business-rules document. Do not treat a comment as a signed rule when the code disagrees with the comment.

## BR-001 — Who may read the report

**Inferred.** Any caller who can reach the host may read filters, charts, and tables. The API does not check a user, a role, or an API key.

Evidence: `Program.cs` on `develop` has CORS `AllowAnyOrigin`, `UseAuthorization`, and no authentication scheme. Controllers have no `[Authorize]`.

`master` also has no authentication. It has no data actions.

## BR-002 — Active fact table

**Inferred.** Services read `{database_table}`, which is the literal `export_data_qa`.

A commented assignment `export_data` is labelled LIVE in `ChartDataService`, `FilterService`, and `TableDataService`. The running assignment is the QA name.

Which name a deployed host uses is **Unknown / Requires Confirmation**. Changing it requires a code edit. It is not a setting in Git.

## BR-003 — Reporting window

**Inferred.**

1. Read the latest row as `ORDER BY period DESC LIMIT 1`. Take its `year` and `month`.
2. If the requested year equals that year and the requested month is greater than that month, the end month is the latest month. Otherwise the end month is the requested month.
3. The window length is `Period` months, ending at that end month. `Period` 1 is that month only.
4. Bounds passed to SQL are `yyyyMM`.

Evidence: the date block in `ChartDataService` and `TableDataService`.

There is no check that `Period` is between 1 and 12 on the server. The filter API returns 1 through 12, and the slider sends that range. A caller can send another integer.

## BR-004 — Empty filter means all values

**Inferred.** An empty product, country, package, or cultivar list adds no `IN` clause.

The client sends an empty list both when the visitor selects nothing and when the visitor selects every option (`filterStore.js`). Those two gestures are the same request.

## BR-005 — Package type vocabulary

**Inferred.**

| Shown to the visitor | Stored comparison |
|---|---|
| `PACKAGED` | `bottels_bulk = 'BOTTELS'` when the filter is applied as a column predicate |
| `BULK` | `bottels_bulk = 'BULK'` |
| Both, or neither | Several charts use flag `BB = 'X'` instead of `bottels_bulk` |
| Packaged only, on those charts | flag `BO = 'X'` |
| Bulk only, on those charts | flag `BU = 'X'` |

The filter list itself is not read from the database. `GetPackageTypeFilter` returns the two strings `BULK` and `PACKAGED`.

`GetExports` always queries bulk and packaged as separate series. It still appends `ApplyFilters`, so a single selected package type also adds `bottels_bulk IN (...)` on both series.

The spelling `BOTTELS` is the value in SQL. It is not renamed except in `GetTotalLitresHelper`, which maps `bottels_bulk = 'BOTTELS'` to the label `PACKAGED` in the result.

## BR-006 — Product types and cultivars in the filter

**Inferred.** Both lists come from `product_type_lookup` where `prd_kde` is one of:

`1`, `2`, `3A`, `3B`, `3C`, `3D`, `4`, `5`, `6`, `7`, `8`, `9A`, `9B`, `9C`, `9D`, `10`, `11A`, `11B`, `36`, `90`.

Product type uses `white_or_red`. Cultivar uses column `` `DESCRIPTION FOR REPORTS` ``. Chart and table queries filter `description_for_reports` (different spelling and no space).

Whether those two column names hold the same text is **Unknown / Requires Confirmation**. The code treats the lookup text as the fact-table value.

## BR-007 — Countries

**Inferred.** The country dropdown is every `engelse_land_beskrywing` in `country_group_lookup`, ordered by that column.

An older predicate `WHERE bb = 'X'` is commented out. A commented line that appended `OTHER COUNTRIES` to the dropdown is not active.

The top-countries query still adds a synthetic `OTHER COUNTRIES` row for rows whose package flag is not `X` or is null. That label is not a country from the lookup.

The chart and table methods are named TOP 25. The SQL does not limit the result to 25 rows.

Country matching uses `sawis_landnaam IN (...)` with the English description the client sent. Whether `sawis_landnaam` equals `engelse_land_beskrywing` is **Unknown / Requires Confirmation**.

## BR-008 — Cultivar chart ignores two filters

**Inferred.** `GetCultivarChartData` calls `ApplyFilters` with method name `GetCultivarChartData`. That call does not add `white_or_red IN` or `description_for_reports IN`.

Country and package type are still applied. The UI still shows those two filters as if they apply to every widget. For this chart they do not restrict product type or cultivar.

Other chart and table calls pass `""`, so they do apply those lists.

## BR-009 — Litre figures

**Inferred.**

- Sums use `ROUND(SUM(totale_liters), 0)`.
- Null sums become 0 in the C# mapping.
- Year-on-year percent is `(current - previous) / previous * 100`, then rounded to 2 decimal places.
- If the previous value is 0, the percent is 0. Division by zero is avoided.
- The previous window is the selected window moved back one year. It is not "the previous calendar year" independent of month.

## BR-010 — Exports history length

**Inferred.** `GetExports` loops `i` from 0 through 9. That is the selected window and nine earlier years, for bulk and for packaged. Ten years is fixed. It does not depend on how many years the table contains. Years with no rows still return a summed value (null becomes 0).

## BR-011 — Latest month clamp

**Inferred.** The clamp in BR-003 runs only when the selected year is the latest year. A future month in an earlier year is not clamped. A month past the latest month in the latest year is clamped to the latest month before the window is measured.

## BR-012 — Defaults the visitor sees

**Inferred** from the client, not from the API.

| Control | Initial value |
|---|---|
| Year | `LatestYear` from the API |
| Ending month | Month label at position `LatestMonth - 1` after the client sorts labels ascending |
| Reporting period | 12 |
| Product type, country, package type, cultivar | empty, which means no `IN` clause |

`Sidebar.js` initial React state uses year `2020`, month `"01 - January"`, and period `3` before the filter response arrives. The effect replaces those values from the store. A submit before the response returns would send the local state. **Inferred** race. Not executed.

`GetFilters` copies `YearName` and `MonthName` and does not copy the integer `Year` or `Month` onto the filter rows. The client parses the name strings. The integer properties on those rows stay at 0. The selected year sent back is parsed from `YearName` / `LatestYear`, not from the zero integer.

## BR-013 — Errors

**Inferred.** Any exception in a controller action is HTTP 400 and the response body is `ex.Message`. There is no distinction between a bad filter and a database failure. The message can include driver text.

The client does not show that message in the page. It writes to the console.

## BR-014 — Analytics and contact

**Inferred.** The page loads Google Analytics measurement id `G-1RFV9ZJG9K` three seconds after start. The only contact path in the UI is `mailto:bi@sawis.co.za`.

## BR-015 — No write-side rules

**Inferred.** This application does not insert, update, or delete domain rows. Every service method that was read is a SELECT. Publishing build output into the Git tree is a repository fact, not a business transaction.

## Conflicts inside the implementation

| Topic | Code says | Also in the code |
|---|---|---|
| Live versus QA data | Comments say `export_data` is LIVE | The assigned table is `export_data_qa` |
| Top 25 | Method and comment names | No 25-row limit |
| Packaged spelling | UI says `PACKAGED` | SQL value is `BOTTELS` |
| Cultivar filter on the cultivar chart | The sidebar sends `cultivarFilterList` | `GetCultivarChartData` drops that clause |
| ALL total versus package filter | `GetTotalLitresHelper` is named as if package filtering might be skipped (`method != "GetTotalLitresHelper"`) | The call passes `""`, so the skip does not run |
| Default branch versus product | `master` is `origin/HEAD` | The rules above exist only on `develop` |

These need a human decision only if a deployed system is supposed to follow the comment or the `master` tree rather than the `develop` statements.
