# Business rules — sawis (SAWIS Online)

Evidence date: 2026-10-04.

Source: `dev_2020` at `069966f`.

Rules are taken from executable checks, SQL, and in-code comments. A comment is not the same as a signed policy. Each rule is marked **Documented** (copy or comment that states the rule), **Implemented** (the check exists), or **Inferred**.

No business-rules document was found in the source tree. `NewKnowledgeBase.md` is a developer note log. Notes from that file are labelled as such.

## Organisation statements

These are shown to a signed-in user on `home.php`. They are **Documented**. The code does not implement them as a single policy:

- Individual information is treated as confidential.
- SAWIS turns industry data into information and applies an origin system.
- The interface promise is real-time access. Whether every screen is real-time rather than batch-synced is **Unknown**. A sync lock (`blnSyncInProgress`) shows that some data arrives from outside this application.

English and Afrikaans copies of the purpose, vision, and promise both exist. **Documented**.

## Who may sign in

| Rule | Status |
|---|---|
| Login name is `sysUser.strEmail` | **Implemented** (`index.php`) |
| The user’s security group must be active (`sysSecurityGroup.blnActive = 1`) | **Implemented** |
| The user row must be active (`sysUser.blnActive = 1`) or the message is “User account is inactive.” | **Implemented** |
| Member status `1.0 - New` cannot enter the home page. The user is offered a registration draft | **Implemented** |
| `refMemberID = 0` takes the same draft path | **Implemented** |
| `refMemberID` null is allowed past that check | **Implemented**. **Inferred** that a user with a null member id is an internal account. **Requires Confirmation** |
| Credentials are accepted only when the SAWIS API `loginUser` returns XML, or when a supplied hash equals the last stored password hash | **Implemented** |
| More than 10 failed logins from one IP within one minute inserts an IP block row noting “Bot activity” | **Implemented** insert. Enforcement on the next request was not re-read |
| Successful login writes a `sysUserCompanyGroup` row if one is missing (`INSERT IGNORE`) | **Implemented** |
| Language is set to `EN` on every successful login | **Implemented**. Conflicts with bilingual screens. **Requires Confirmation** |
| Admin Dev and Sawis Admin store a password-less hash URL for re-entry | **Implemented** |

## Who may see and change what

| Rule | Status |
|---|---|
| Menu level-2 items are limited to rows in `sysSecurity` for the user’s `refSecurityGroupID`, and only where `blnMenuItem = 1` | **Implemented** (`_nemo.menu.cls.php`) |
| `blnView` false renders the item as an inactive link | **Implemented** |
| `blnSave`, `blnDelete`, and `blnSpecial` are selected with the menu. Email comment: only `blnSpecial` may send a new email | **Documented** in `includes/email.cls.php` comment. Call-site enforcement was not re-read |
| Groups 1 and 2 may filter by another cellar or member on activity, duties, bottling, bulk exports, WS, WSB traceability, and the admin report list | **Implemented** |
| Group 1 is Admin Dev | **Documented** in `user.navigation.analytics.report.cls.php` |
| Group 2 is the other half of the admin pair. The name “Sawis Admin” is checked as a string, not as the number 2 | **Inferred** that id 2 is Sawis Admin. **Requires Confirmation** |
| Users outside groups 1 and 2 are sent home from cellar-operation pages unless `tblMember.blnAllowStockMovement = 1` for their member id | **Implemented** (`system.php`). **Inferred** that this flag means “pilot group”, because the variables are named `PILOT` |
| Sawis View Only cannot use update, approve, or new-document buttons on the screens listed in `features.md` | **Implemented** (button visibility). The group’s numeric id is **Unknown** |
| Security group 13 cannot edit `member.profile.php`. Comment: member still unapproved | **Implemented** check. The “unapproved” meaning is **Inferred** from the comment |
| A member managing users may only assign Member Superuser and Member User | **Documented** in a comment on `my.user.php`. The dropdown SQL was not re-read |
| `checkMemberAccess` allows a user record only when `sysUser.refMemberID` equals the acting member. A relationship row also matches `refEntityID` or `syncProductionCellarID` | **Implemented** for those branches. Further entity branches exist and were not all listed |
| Staff switch-member prefixes the username with `pp.` and clears in-progress registration and BG1 sessions | **Implemented** (`home.php`) |
| Referrer column on navigation analytics is visible only to security group 1 | **Documented** and **Implemented** (note 2026-01-05) |

The full permission matrix is the `sysSecurity` table. It is not in git.

## Registration

| Rule | Status |
|---|---|
| Three paths: new member, link to an existing member, change of ownership of an existing cellar | **Documented** labels and **Implemented** radios |
| A membership-type radio is disabled when its code is absent from `tblMemberType` | **Implemented** |
| Type meanings are the English sentences in `member.registration.cls.php` (private cellar, producer cellar, wholesaler cellar, producing wholesaler, distillery, bottler, juice cellar, concentrate manufacturer, exporter types, certification-only entity `CERPVT`, grape grower `FRM000`) | **Documented** |
| `CERPVT` is described in a comment as “Facilitation SAWIS Member” and BG1 disables radio options 1, 3, and 4 for that type | **Documented** in comments (2017-07-03 and 2018-02-08). Confirm the BG1 check still matches the comment before relying on it |
| Draft state is serialized onto `tmpMember.RegistrationArgs` | **Implemented** |
| Pending members, relationships, farms, and single vineyards have separate approval screens | **Implemented**. Approval criteria beyond the button are **Unknown** |
| Change-of-ownership collects an existing member id | **Implemented** |

## Vineyard blocks

From comments in `includes/block.cls.php` (2017). Treat as **Inferred** until the current `Save()` body is re-checked; the comments describe the calculation the authors said they implemented:

| Rule | Status |
|---|---|
| Wide row, narrow row, and vine spacing must not be &lt; 0.5 or &gt; 5. Vine spacing may be 0, but must not be between 0 and 0.5 | **Inferred** from the 2017-06-06 comment |
| If vintage year − planted year &lt; 3, status is New; otherwise Production | **Inferred** from the 2017-07-07 comment |
| Vines closing = vines opening − vines uprooted + vines grafted + vines amended | **Inferred** from the 2017-08-07 comment (“CALC HAS CHANGED”) |
| A 2017-07-18 comment says Francis asked to deactivate an update of sync hectares | **Documented** as a comment. Whether the update is still absent was not re-verified |
| New blocks are not renamed with `(s)` or `(g)` | **Inferred** from 2017-10-17 comments |
| Block lists that offer selection are restricted to active blocks (`blnActive`) | **Inferred** from 2017-09 comments |
| SAWIS 1 PDF includes an amended-vines column | **Inferred** from a 2017-07-25 comment. Output folder `documents/SAWIS_1_Reports/` exists |

## Harvest loads and WS numbers

From `NewKnowledgeBase.md` (2026-01). **Inferred** as current behaviour because the notes describe code on this tip, not a separate specification:

| Rule | Status |
|---|---|
| A single-vineyard WS number does not auto-fill cultivar. Cultivar comes from the chosen block | **Inferred** (note 2026-01-04) |
| A non-single-vineyard WS number auto-selects cultivar only when exactly one cultivar is on that WS number | **Inferred** (notes 2026-01-04 and 2026-01-26) |
| Several cultivars require a manual choice | **Inferred** |
| Farm selection refreshes members, blocks, IPW status, and production area | **Inferred** |
| The string `"null"` is not a single-vineyard value for blend-sandbox warnings | **Inferred** (note 2026-01-30) |
| Blend-sandbox single-vineyard warning applies to every selected tank that has a single-vineyard value, when any selected WSB has one or the selected WS tanks disagree | **Inferred** |

`BG1_Vintage` in `sysSettings` is the default vintage. Reports also offer vintages present on `tblHarvest` from that year down to 50 years earlier. **Implemented** in `report.php` and `bulk.exports.cls.php`.

## Tanks, movement, and blends

| Rule | Status |
|---|---|
| Movable-tank save and delete reject a tank id that is not a positive integer | **Inferred** (note 2025-12-01) |
| Stock-movement certification dropdown order is certification numbers, then “None”, then “Uncertified” | **Inferred** (note 2026-01-08) |
| Crush/press/separator output status cannot be “Finalize”. Choices are Fermenting/Settling, or Settling only when all loads are white. Finalized tanks stay locked | **Inferred** (note 2026-01-08) |
| Input tank-register rows set `blnInitialFinalLitres` when output litres are finalised for Concentrate, Bottling, Distilled, De Alcoholised, and Must. Output rows stay 0 | **Inferred** (note 2026-01-27). An earlier note (2025-01-08) said the flag is set when any output tank is finalised during transfer. The 2026-01-27 note is the later statement |
| If a blend source tank is also an output tank, the movable output tank is not closed | **Inferred** (note 2026-01-21) |
| A special bottling WSB match must be the same tank (`tblBottlingNotices.refTankID`) as well as size, date, and company | **Inferred** (note 2026-01-29) |
| WSB numbers are parsed as 6 digits + 3-character transaction type + 2-digit iteration (example placeholder `123456BLN01`) | **Implemented** in the traceability worker and the search field lengths (min 6, max 11) |
| Traceability reads `tblBlendMaster` and mapper service `WSBNUMQUERY3` | **Implemented** |
| Loss list page size is 100, and the output-difference total uses the full filtered set | **Inferred** (note 2025-12-03) |
| Wine-type lookup must escape backslashes and quotes (`$xdb->qs`) | **Inferred** as an implementation constraint (note 2025-12-03), not a business rule about wine types |
| Tank unlock after 30 minutes is not active | **Implemented** as commented SQL. **Unknown** what clears `blnLockTank` in production |

## Duties and packhouse

| Rule | Status |
|---|---|
| Duties Excel columns Consignor ID, Consignee ID, Price Per Litre, and Actual L are right-aligned and coerced to numbers when the header matches exactly | **Inferred** (note 2026-02-02) |
| Members with an empty `strSARSPackhouseNumber`, registration older than the configured month interval, and `dtFirstEdit` after the configured minimum date, are listed for the packhouse cron | **Implemented** |
| Seed values if settings are missing: active `true`, interval 3 months, minimum date `2016-01-01`, notify member `false`, admin email `sawis3@sawis.co.za` | **Implemented** insert in `_cron.daily.php` |
| Member email is Afrikaans or English from ``strSetting:CorrespondenceLanguage``, default English | **Implemented** |
| Member email is skipped unless `CRON_Packhouse_Task_Notify_Member` is true | **Implemented** |
| Admin summary is sent when the query returns rows, using template `CRON Packhouse Task ADMIN` | **Implemented** |

## Reporting access

| Rule | Status |
|---|---|
| Groups 1 and 2 see the long report catalogue (harvest, SAWIS 5, SAWIS 10, tonnage, block totals, both cellar-stock reports, plus the shared three) | **Implemented** |
| Other groups see Fortification, Tank Walkthrough, and WS/WSB Transactions | **Implemented** |
| Fortification for a non-admin uses the session company id, not a typed cellar id | **Implemented** |
| Tonnage per Hectare, Litres per Tonne, and the member copies of Tonnage per Farm and Block Totals are not registered | **Implemented** as comments |

## Process rules and maintenance

| Rule | Status |
|---|---|
| Process-logic rule status is read-only on the edit form | **Implemented** |
| Approve Rule is offered only when status is `In Review for Implementation` | **Implemented** |
| Comment: status changes if the description changes; delete from the list makes the rule inactive | **Documented** in a comment. Class method not re-read |
| When `SYSTEM_UNDER_CONSTRUCTION` is 1, every session is cleared and the user is sent to the under-construction error page | **Implemented** |
| When a sync is in progress, requests wait up to 60 seconds and then stop with a refresh instruction | **Implemented** |
| Navigation logs ignore requests whose only query parameter is `page` | **Inferred** (note 2026-01-05) |
| Detail URLs are grouped by script name for analytics; the raw URL stays on a tooltip | **Inferred** (note 2026-01-05) |

## Inspector audit

| Rule | Status |
|---|---|
| Audit document upload context is `wine` or `tank` | **Inferred** (note 2025-11-27) |
| Files are stored under `documents/Inspector_Audit_Documents/` | **Inferred** from that note. The folder was not listed in the top-level `documents/` directory listing taken during this pass (subfolders seen: `BlendSandboxReports`, `FAQ`, `SAWIS_1_Reports`, `SAWIS_6_Documents`, `Signed_SAWIS_6_Documents`, `TankCalibrationCert`, `thumbnail`). The audit folder may be created on first upload. **Requires Confirmation** |
| Tank audit uses the latest `TankAuditNoteID` | **Inferred** |
| Users can be marked inspector (`blnInspector`, `InspectorCode`) | **Implemented** fields on `user.php` |

## Rules that must not be invented from this file

- Statutory wine-certification law, levy rates, and SAWIS 5 / SAWIS 10 form definitions are not written in the repository. The reports exist. Their legal meaning is **Unknown / Requires Confirmation**.
- Prices, duty rates, and cultivar master data live in the database, not in git.
- Which security group id maps to which job title, except Admin Dev = 1, is **Requires Confirmation**.
