# Business rules — sawis_api_old

Evidence date: 2026-10-04.

Source: `git@bitbucket.org:overdrive/sawis_api_old.git`, `master` at `1124919`. There is no business-rules document in the source repository.

Rules below are what the code enforces or what a comment says was a spec. A comment is not treated as a live rule unless the current SQL or condition still implements it. Those cases are marked **Inferred** when the code only implies the rule, or left as comment text when the SQL is the authority.

Passwords, the `api.user.php` token, and connection secrets are not recorded here.

## Access

### BR-001 — An inactive or unknown vendor key is rejected

**Implemented.** `checkVendor` loads `apiVendor` where `VendorKey` equals the request value and `blnActive = 1`. Any other result returns the text `INVALID VENDOR KEY` and HTTP 202, and writes `apiLog`.

### BR-002 — Authenticate compares the request password to the vendor row

**Implemented.** For `call=authenticate`, `$_REQUEST["Password"]` must equal `apiVendor.strPassword`. Success returns a new UUID stored on `apiToken`. Failure returns the text `AUTHENTICATION FAILED` with HTTP 200 (no status argument is passed to `response`).

### BR-003 — Later calls need a token no older than three days

**Implemented.** `clearTokenCache` deletes `apiToken` rows with `dtToken` older than `3 DAY`. `checkToken` requires a row for that token and vendor, then sets `dtToken` to now. The property comment says `CHANGE BACK AFTER TESTING` and shows a previous value `30 MINUTE` in a comment. The value that runs is `3 DAY`.

A missing token returns `INVALID TOKEN` and HTTP 202.

### BR-004 — A query parameter equal to the vendor password skips the token

**Implemented.** `processApi` treats `IGNORE_TOKENZ` as a skip when its value equals the vendor password from `apiVendor`. The call still passes `checkVendor`.

### BR-005 — Login to a named site uses a column on the user

**Implemented.** `checkAccess` reads `sysUser` property `blnSite_` plus `apiVendor.strSite`. The value must be `1`. Used by `loginUser` and `loginToken`. The comment says this could have been done inside `checkVendor`. It is not.

`loginUser` also requires `sysUser.blnActive = 1` and a matching password hash.

### BR-006 — New users receive a group that depends on the vendor

**Implemented.** Vendor key `SAWIS3`: if `refSecurityGroupID` is 0, set it to `sysSecurityGroup.strSecurityGroup = 'Member Superuser [Unapproved]'`. Every other vendor key: set the group to `External Site User`.

### BR-007 — Read calls do not check a user role

**Implemented.** After the vendor key and token, a read method runs its SQL. Column visibility is BR-010. There is no security-group check on `getMemberList` or the other list calls.

## Member visibility

### BR-008 — Member id zero is excluded

**Implemented.** `getMemberList`, `getMemberAddressList`, `getMemberContactList`, and `getMemberProfile` add `tblMember.MemberID > 0`. `getMemberTypeList` adds `MemberTypeID > 0`.

### BR-009 — Four vendor keys add further member filters

**Implemented** in `setVendor_CallWHERE_Additional`. The SQL is the rule. Comments above VINPRO and WOLINE describe a spec.

| Key | Rule the SQL applies |
|---|---|
| `VINPRO` | (`strStatus` like `Inactive` and `blnVinPro = 1`) or (`strStatus` like `Active`). `refVinPro <> 9`. Applied to member list, address, contact, and profile |
| `WOLINE` | `strStatus` like `Active` and `refWineOnline <> 9` on those four calls. The comment does not mention the Active requirement. The SQL does |
| `WIETA1` | `refWieta <> 9` on those four calls and on `getMemberTypeList`. The comment says the WIETA type field must not be 9 |
| `EZW1NE` | `AND 1=0` on those four calls, so the queries return no rows |
| `LMS001` | The `case` is empty |

`like` without wildcards is what the source writes.

### BR-010 — A vendor sees only included fields

**Implemented.** `cleanVendorRST` keeps a column when `apiVendorField` matches the vendor or `*`, the call, the field or `*`, and `strLastUser = 'INCLUDE'`. An earlier version removed listed fields. That function is commented out. **Inferred:** a vendor with no INCLUDE rows receives rows with no columns. The code does not document that outcome in a comment.

### BR-011 — LMS member profile uses status and refLMS

**Implemented.** A member is included when (`strStatus` is Inactive or Active and `tblMemberType.refLMS <> 9`) or (`strStatus` is Active and `refLMS` is null), and `MemberID > 0`.

### BR-012 — LMS member-id list is three farm relationship codes

**Implemented.** `strMemberTypeCode` must be one of `FRMOWN`, `FRMTNT`, `FRMSCP`.

### BR-013 — Farm list members are three farm type codes

**Implemented.** The derived join keeps `tblMemberRelationship.strType = 'Farm to Member'` and `strMemberTypeCode` in `FRM000`, `FRMEST`, `FRMOWN`, then takes `Max(refMemberTypeID)` per farm and member.

### BR-014 — Title deeds omit temporary entity types

**Implemented.** `EntityType NOT LIKE '%TMP%'`. Comment date 2017-09-20.

### BR-015 — Harvest lines marked deleted are omitted from getWS

**Implemented.** `tblHarvestItem.blnDeleted = 0`. The same predicate on `getWSB` is inside a comment (`n/a`) and does not run. The live `getWSB` FROM clause does not join `tblHarvestItem`.

### BR-016 — WineMS reads keep active WineMS rows

**Implemented.**

- `getWineMSFarmList`: `FarmStatus` like `Active`.
- `getWineMSFarm` and `getWineMSMember`: `blnWineMS = 1`, `refWineMS` in (`1`, `3`), `MRStatus` like `Active`, `FarmStatus` like `Active`.
- `getWineMSSawis1`: `blnWineMS = 1`.

### BR-017 — CIS block list is one farm and one year setting

**Implemented.** `getCGABlockList` without `FarmID` returns the text `Missing Parameters [FarmID]`. With `FarmID`, `intYear` must equal `sysSettings` key `CURENT_BLOCK_YEAR` (the key is spelled that way in the source).

## Writes

### BR-018 — User create requires email and password in XML

**Implemented.** The body must be XML with `rows/row`. `strEmail` and `strPassword` must be non-empty. The HTTP body on failure is `Please make sure you have filled in all required fields` with code 202. The log line includes `INVALID DATA STRUCTURE` and the field name.

A database message containing `Unique key Violation` is returned as `Cannot add User, Email already in use`.

### BR-019 — Password strength for setPassword

**Implemented.** Measured length of the submitted password is 8 to 14 inclusive. Spaces and `&` are stripped before the pattern check. The remainder must be only letters, digits, and `=! - @ # % . _ *` (pattern `^[A-Za-z0-9\d=!\-@#%._*]*$`), and must contain one capital letter and one digit.

The failure sentence also lists `^`, `&`, `*`, `(`, `)`, `_`, `+`, `=`. After the strip, `&` cannot remain. `^`, `(`, `)`, and `+` are not in the pattern. The sentence is not the rule. The pattern is the rule.

`NemoPassword` can insert settings `PASSWORD_Strength_Check_MinLength` = 8, `PASSWORD_MaxHistory` = 8, `PASSWORD_Restricted_Words` = `password|123`, `PASSWORD_Interval` = 12, `PASSWORD_Reset_Interval` = `60 MINUTE`, if those rows are missing. `setPassword` does not call the restricted-word loop. That loop is commented out.

### BR-020 — Password reset stores an aged password and emails a link

**Implemented.** The new password's saved date is the literal `2010-01-01`. The comment calls that a default aged date. The mail template name is `Reset Password`. The link host is `http://10.0.0.149/html` for vendor `SAW3QA`, `https://www.sawisonline.co.za` for vendor `S3PROD`, and `{SERVER_NAME}/sawis` otherwise.

### BR-021 — Certification write requires six fields plus a transaction id

**Implemented.** Key element `TransactionID` must exist on the row. Required values: `ApplicationID`, `MemberID`, `intVintage`, `strOrigin`, `strCultivarCode`, `strCertificationStatusCode`. Existing `tblCertification.TransactionID` is updated. Otherwise the row is inserted. The code reads the previous `strCertificationStatusCode` into a variable when updating. The comment says the rule is "If CERT_STATUS changes". What happens with that variable further down the method is in the same function. The required-field list above is the gate.

### BR-022 — Lab result write requires transaction, lab, and application ids

**Implemented.** `setCertificationResultsRecord` required elements: `TransactionID`, `LabID`, `ApplicationID`. Table `tblLabAnalysis`.

### BR-023 — Export and WIETA writes check a key and not a required-field list

**Implemented.** `setWineExportRecord` requires the element `ExportCertificateID` to exist as a key. `setWietaMembership` requires the element `MemberID`. Both functions contain the comment `TODO` where a required-field list would be. A 2018-03-27 comment on WIETA says certification date defaults to today and expiry to NULL. That comment is the spec note. Confirming every assignment needs the rest of `setWietaMembership`, which does perform the save. The TODO means the extra required-field loop was not added.

### BR-024 — VinPro flag import replaces tmpVinPro and copies the flag

**Implemented.** The first XML row must contain `MemberID` and `blnVinPro`. The script prints the XML, then executes `TRUNCATE TABEL tmpVinPro`. The word `TABEL` is in the source. Rows are inserted into `tmpVinPro`. `tblMember.blnVinPro` is set from the temp table on matching `MemberID`.

### BR-025 — WineMS cellar replace is per cellar id

**Implemented.** `setWineMSCellar` returns the text `Missing refCellarID` when that query argument is absent. Otherwise it deletes `stg_wmsBlock` for that `refCellarID` and inserts the XML rows. Each row must include the required names listed in the PRD (GUID, cellar, member, farm, block, cultivar, spacing, vines, irrigation, trellis).

### BR-026 — Login token expiry comparison treats a past timestamp as success

**Implemented.** `loginToken` takes the success path when `dtTokenExpiration` is less than the current timestamp, and returns `Login Token has expired` otherwise. **Unknown / Requires Confirmation:** whether the comparison was meant to be the other way.

## Operations and retention

### BR-027 — API logs are kept for three months

**Implemented.** `record` deletes `apiLog` where `dtLog` is older than `Now() - INTERVAL 3 MONTH` on every logged call.

### BR-028 — File-integrity default rewrites today's audit and file modes

**Implemented.** When `Action` is not one of the named FIM actions, `cron.fim.php` deletes `sysAudit_{environment}` rows for today's date, records the tree, compares, and sets directories to `0755` and files to `0644`. Names matching `sysSettings` `FIM_EXCLUDE_PATH_LIKE` are skipped. `FIM: PULL MASTER DATA` does nothing.

### BR-029 — Email open marks the message read

**Implemented.** `email.read.receipt.php` and `email.read.online.php` set `tblEmail.strStatus` to `Read` and `strLastUser` to `Email Read Receipt` when `UID` is present and the status is not `Error`.

### BR-030 — CORS credentials are allowed only for five origins

**Implemented.** The origins are listed in `api.php`. A comment dated 2019-07-17 says the change was for GIS. Other origins are not reflected.

## Calculations present but not called by the API

### BR-031 — Hectare from row spacing, vine spacing, and vine count

**Inferred** as a leftover rule, **Implemented** only as a function. `calculateHectare` returns `round(((narrow + wide) / 2) * vineSpacing * vines / 10000, 4)`. `NemoAPI` does not call it. `getBlockDensity` returns `round(vines / hectare, 4)`. `setBlockDensity` passes `$intVine`, which is not the parameter `$intVines`.

### BR-032 — New farm id is derived from the current maximum

**Inferred** the same way. `GetNewFarmID` with a null id takes `MAX(FarmID)`, adds 1000, drops the last three digits, and appends `000`. With an id, it takes the prefix and adds 1 to the latest `FarmID` with that prefix. Tables are `tblFarm` or, when the temp flag is set, `tmpFarm`.

### BR-033 — Smallest common wine-of-origin code

**Inferred** the same way. `bg1GetSmallestCommonLocation` walks harvest farms and returns the ward when all wards match, otherwise the district, otherwise the region, otherwise the geo code. WO code lengths used in the joins are 2 (geo), 5 (region), and 8 (district). Full code matches the ward. Language defaults to `EN` when the session language is empty.

## Rules that are comments only

- VINPRO comment: closed status, and inactive with VinPro = N, are excluded. The SQL expresses the allowed set (active, or inactive with the flag). It does not name status `Closed`. **Implemented** as the allowed-set SQL.
- WOLINE comment: the unique key of a member profile is SAWIS member number, WOL code descending, SAWIS farm number, member type, status, and member prod cellar. No `ORDER BY` or unique-key check for that list was found in `setVendor_CallWHERE_Additional`. **Unknown / Requires Confirmation** whether another method sorts that way.
- `SessionExpire` comment asking to change 3 days back after testing. The code was not changed. The rule that runs is three days.
