# Business rules — sawis_staging

Evidence date: 2026-10-04.

Source: `git@bitbucket.org:silicon_bit_bucket/sawis_staging.git`, primarily `qa_staging_2020` at `5040b8b`. Production-only rules are marked. 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**.

Passwords are not recorded here.

## Access

### BR-001 — Public entry does not authenticate

**Implemented.** `index.php` does not read credentials. It destroys the session and shows the SAWIS website address.

### BR-002 — SQL console allows two address prefixes

**Implemented.** `query.php` dies with `SECURITY CHECK FAILED` unless `REMOTE_ADDR` contains the substring `192.168.100` or `10.212.134`. The check is `strpos`, so any address that contains those characters is allowed. It is not a parsed CIDR check.

No user identity is required after that check.

### BR-003 — Sync UI and deploy do not check a user

**Implemented.** `sync.ui.php` calls `session_destroy()` and then runs the posted action. `deploy.php` runs git commands for any request. No role check is in either file.

### BR-004 — Message page describes access it does not grant

**Implemented.** `message.php` can say the session expired, the user logged out, access is insufficient, or an internal error occurred. It prints `SECURITYGROUPID` from session settings when that object exists. This repository has no login screen that sets `$_SESSION[USER]`.

### BR-005 — Failed-login lockout does not save

**Implemented.** `CheckSecurityLock` treats five `sysLogin` rows of `Login failed: Incorrect password` with no later `Login successful` in that window as lockout. It assigns `blnLogin = 0` and `strLastUser = Security Lockout`. The following `Save()` is commented out, so this function does not write the user row.

### BR-006 — Password check for a profile change

**Implemented** in `checkChangePassword`: the new password must equal the confirmation, and `password_verify` must succeed against `$strPasswordDB`. The function's comment points at `user.php`, which is not in this repository. Nothing in the sync scripts calls it.

### BR-007 — Login helper stores the password in the session and in cookies

**Implemented** in `Login()`: `$_SESSION['PASSWORD']` and cookie `celpassword` receive `$row->strPassword`. Cookie lifetime is 3600 seconds. No sync script calls `Login()`.

## Sync control

### BR-008 — Sync stops only when the flag is off and no action was posted

**Implemented.** If `sysSettings` value `blnEnableSync` is `0` and `$Process` is null and `$Action` is null, the cron dies with `Sync disabled / blnEnableSync = 0`.

**Inferred.** A named action is a bypass of the disable flag, because that is what the condition requires.

### BR-009 — Enable and disable write the S3 settings row

**Implemented.** Disable sets `blnEnableSync` to `0`. Enable sets it to `1`. `strLastUser` is `CUSTOM SYNC RUNNER`. The update runs on `$DATABASE_SETTINGS_S3`, then the code switches back to `$DATABASE_SETTINGS`.

### BR-010 — Last sync date must be a full timestamp

**Implemented.** `Set Last Sync Date` accepts a string only when `DateTime::createFromFormat("Y-m-d H:i:s", ...)` returns a date whose formatted value equals the input. Otherwise the page message says the value is an invalid date format and the row is not updated.

The SQL concatenates the posted string. The format check is the only validation in that action.

### BR-011 — Reset and "set synced" affect today only

**Implemented.** Both actions update `sysSyncProcess` where `dtDate` is today's `Y-m-d`. Reset stores `blnProcessed` `0`. Set Synced stores `1`. They use the connection already selected. They do not call `switchBD` to S3 first, unlike enable/disable.

### BR-012 — Stage O.1 and stage -1 cannot be posted from the form

**Implemented.** Those checkboxes are HTML `Disabled`. The labels call the email stage "wip".

### BR-013 — Default stages

**Implemented** as the `Checked` attribute: stage 1.1, 2.0, 2.1, 2.2, 2.3, 4.0, and O start checked. Mapper stages 3.0, 3.0.5, 3.1, and 3.3 start unchecked.

## Data movement

### BR-014 — Staging rows drive the table update

**Implemented** in `UpdateSTG`. The function counts `stg<Entity>`. The update and insert block runs only when that count is not zero.

### BR-015 — Environment push logs success and failure counts

**Implemented** in `UpdateENV`. A failed statement inserts `sysSyncLog` level `Error`, action `ENV UPDATE`, with the MySQL error. A finished batch inserts level `Info` and the text `<env> Complete: <n>/<log count>` or `<env> Complete: no LOG data.`

### BR-016 — BlendMaster duplicate keys are warnings, not inserts

**Implemented** for `Entity->Type == "BlendMaster"`. Error text containing `Duplicate entry` or `1062` is logged as `Warning` / `STG Import` and the function returns false. Other entity types do not use this helper.

On `prod_staging_2020`, `doQuery` can return false instead of stopping the process when the third argument is false. On the QA tip the method does not take that argument. See `project-plan.md`.

### BR-017 — Lab analysis is not inserted

**Inferred** from the comment dated 2021-01-19 in `project.functions.inc.php`: lab analysis has a custom update query and must never be an insert.

**Inferred** from the comment dated 2021-07-28 and from branch `fix_sync_hotfix20210630`: after sync, the API lab-analysis row status becomes `ST` so the API side can see that staging has taken the row. The comment spells the status meaning as "sawis stabel".

### BR-018 — Mapper file absence is an info log, not a hard stop

**Implemented.** If `DATA/IN/GRAPE VARIETY TABLE.csv` cannot be opened, `cron.mapper.php` writes `sysSyncLog` level `Info`, action `FILE Import`, notes `N/A: no file.`

### BR-019 — An empty mapper action truncates the sync log

**Implemented.** The first switch in `cron.mapper.php` defaults to `TRUNCATE TABLE sysSyncLog`.

### BR-020 — Cultivar CSV is the only mapper entity

**Implemented.** `$arrSyncEntities` in `cron.mapper.php` contains only the Cultivar object. Its natural key in the update is `CultivarCode`. Insert uses `INSERT IGNORE` and skips rows whose `CultivarCode` already exists.

Cultivar fields written from the file: wine-type code, English name, Afrikaans name, and the flags certifiable, blanc de noir, rootstock, distill wine, wine, and active. The SQL comment says web reference and `strCultivar` are missing from the file and are not updated.

### BR-021 — Sync log email reads one historical day

**Implemented.** `emailSyncLog` filters `dtDate = '2017-01-31'` and `blnEmailed = 0`. Info rows are grouped. Error and other non-info, non-`Dev` rows are listed individually. The body signs off `SAWIS Sync`. The subject is `Sawis Online: Sync Log` plus the profile environment name.

Recipients are not hard-coded. They are `sysSettings` values `SMTP Send To` and `SMTP Send As` on the S3 database. A commented line shows an old recipient `maanie@overdrive.co.za` and an old from-address `SAWIS Online <info@sawis.co.za>`.

### BR-022 — MySQL strict mode is cleared globally by the cron

**Implemented** in `cron.sync.php`: `set global sql_mode=''` after a `SHOW VARIABLES` check. The 2025-03-06 commit subject says this is to stop strict mode on MySQL. The statement is global, so it is not limited to the staging schema.

### BR-023 — Foreign keys can be turned off for a console query

**Implemented.** When the console flag is set, `runQuery` removes any existing foreign-key-check statements and wraps the batch so checks are off, then on again.

`cron.mapper.php` also runs `SET FOREIGN_KEY_CHECKS=0` at the start of a sync.

### BR-024 — Charset on connect is utf8

**Implemented.** Successful `switchBD` / `switchBD2` calls `mysqli_set_charset(..., "utf8")`. Failure of that call is ignored (the error printf is commented out).

## Domain comments that the current SQL was not fully re-read for

These are **Inferred** from comments in `cron.sync.php`. They describe rules the authors said they implemented. Confirming each one means reading the matching `UPDATE` in the entity block.

| Comment date | Rule as written |
|---|---|
| 2017-05-22 | Sawis district and region are swapped when moving mapper data to S3 |
| 2017-11-16 | If `Member.strWOLCode` changes, `BG2Master.wol_code` is updated as well |
| 2017-11-16 | If more than one harvest-item cultivar is set, harvest cultivar becomes `000` |
| 2017-12-20 | A deleted BG2 farm is status `Closed`, not `Inactive` |
| 2018-03-27 / 2018-03-29 | `strLastUser` like `R:%` means process `RESET FILE` and `syncOperationType` `R` |
| 2018-06-11 | BG2 farm sync updates only rows that are already `Active`, because of a conflict with stage 2.2 |
| 2018-07-09 | Address lines 1 and 5 are mandatory |
| 2018-07-18 | BG2 cellar update joins harvest on `WSNUM` only, not on cellar id, so the cellar id can change |
| 2019-06-21 | Block sync keeps rows where `tblBlock.intYear` equals `sysYear.intYear` |
| 2026-08-12 | `strMapperWO` is copied on location data toward the API. This commit is on the QA line and is not an ancestor of the 2026-09-14 production tip |

Member-type codes in a 2018-09-20 comment (`04` learner management, `05` EzyWine, `06` member-chosen type) refer to behaviour that is not implemented in this repository.

## IPW target

### BR-025 — QA IPW profile

**Implemented** on `qa_staging_2020` for host key `10.0.0.62` and for the QA host key: IPW hostname `10.0.0.105`, database `ipw_live`.

### BR-026 — Production IPW profile

**Implemented** on `prod_staging_2020` for host key `10.0.0.62`: hostname `10.0.0.155`, database `ipw_staging_20260823`, username `sawis_readon`. A comment names table `sawis_compliance` and says the previous profile is old.

The username `sawis_readon` implies a read-only account. The repository does not show the MySQL grants. Whether sync can write that database is **Unknown / Requires Confirmation**.

## Query and text helpers

### BR-027 — Proper-case skips strings that contain a digit

**Implemented** in `strtoproper`. If the value matches `/[0-9]/`, it is returned unchanged. Otherwise it is lower-cased and passed through `ucwords`. A trailing apostrophe plus one capital letter is lower-cased, and `'S` is replaced with `'s`.

### BR-028 — Mapper value cleaning only trims

**Implemented** in `cleanMapperData`. It trims. The comment about proper case and replacing double quotes is not done. The function returns the trimmed value.

## Deploy

### BR-029 — Deploy replaces the working tree with one remote branch

**Implemented.** `git reset --hard` of the branch named in that copy of `deploy.php`. There is no confirmation step and no authentication step in the script.

## What is not a rule here

- No document in the repository states who may sync, which vintage is current, or which environment is production.
- `sysSettings` can change limits and addresses without a code release. The rows themselves were not readable from git.
