# Business rules — ipw-php

Evidence date: 2026-10-11. Source: `main` at `054883d9`.

Rules are included only where code, a matching knowledge-base page, or the README states them. Each rule is marked **Documented**, **Implemented**, or **Inferred**.

`knowledgeBase/BusinessRules.md` and `knowledgeBase/Certification.md` contain additional statements (category weights must sum to 100%, every file is virus-scanned, files are encrypted at rest, certificate validity of one or two years by biodiversity type). Those statements were not found in the CI4 scoring and certificate code reviewed for this document. They are not repeated here as rules. See the conflict list at the end.

## Evaluation identity

| ID | Rule | Evidence | Label |
|---|---|---|---|
| BR-001 | A submission’s application type is 0 Self-assessment, 1 IPW Audit, or 2 SEV Audit. Any other integer is displayed as Unknown | `MemberEvaluations::getApplicationTypeText` | Implemented |
| BR-002 | Farm forms are form type 1. Cellar forms are form type 2 | `ScoringRules::derivedPassingScore` | Implemented |
| BR-003 | Cellar type is meaningful on cellar forms only. A farm row’s default `cellar_type` 0 must not be shown as cellar type A | Comment on `getCellarTypeText` | Implemented |
| BR-004 | Stored form titles may begin with “IPW EVALUATION FORM:”. The screen strips that prefix and shows FARM or CELLAR. The stored `form_desc` is not rewritten | README, commit `1dc0392` | Documented and implemented |
| BR-005 | Forms 1 through 38 use the legacy scoring scheme unless `scoring_scheme` is explicitly `legacy` or `modern`. New forms default to modern | `ScoringRules::schemeFromForm` | Implemented |

## Scores

| ID | Rule | Evidence | Label |
|---|---|---|---|
| BR-010 | Legacy main-item scores are only 5, 3, 2, or 0. Modern main-item scores are 0, 1, 2, 3, 4, or 5 | `ScoringRules::allowedMainScores` | Implemented |
| BR-011 | Members are subject to `disable_options`. Staff scoring an IPW audit (application type 1, user class not 2) ignore `disable_options` and may use 0–5 even on legacy forms | `isStaffIpwAuditScoringContext` | Implemented |
| BR-012 | On a modern form, score 0 is never disabled by the bit mask | `isMainScoreDisabled` | Implemented |
| BR-013 | A legacy item with `disable_options` `1111` is fully locked for members and is excluded from self-assessment required-question checks | `isMainItemFullyLockedForMembers` | Implemented |
| BR-014 | Appendix answers are Yes = 1, No = 2, N/A = 3 | `validateAppendixChoice` | Implemented |
| BR-015 | A non-empty note scores 2 points. An empty note scores 0 | `ScoringRules::notePoints` | Implemented |
| BR-016 | The submission score is stored as a half-up integer. A parent item total is stored to one decimal, half-up | `persistIntegerScore`, `persistParentTotal` | Implemented |
| BR-017 | Farm passing score is 70% of the applicable total when the total is positive. The result is the half-up integer | `FARM_PASS_PERCENTAGE` | Implemented |
| BR-018 | Cellar types A, B, and C use 65% of the applicable total. Types D and E do not. They keep 96 pass out of 160 total | `CELLAR_PASS_PERCENTAGE`, `CELLAR_DE_PASS`, `CELLAR_DE_TOTAL` | Implemented |
| BR-019 | Optional farm Table B raises the maximum from the base total (example 285, pass example 200 stored or 199 displayed) to the full total (example 319, pass example 223) only when Table B contributes a positive score. A zero-point Table B remains saved and the base total stays in force | README; `SubmissionScoreRollup` (“Only a positive child total marks Table B as included”) | Documented and implemented |
| BR-020 | `285 * 0.70` is 199.5, and half-up rounding stores 200. A commit and the README also describe a compact 199/285 display | `ScoringRules`; commit `071ad4c`; README | Implemented formula. Display figure **Requires Confirmation** |
| BR-021 | Editable scoring statuses are New (0) and In Progress (10) only | `ScoringRules::isEditableStatus` | Implemented |
| BR-022 | A Lead Auditor may submit IPW audit type 1 with unanswered required questions. Those items score 0. Audit Pass and Audit Fail rules still apply. Members cannot use this path. Other application types cannot use this path | `staff_incomplete_ipw_audit_submit_allowed` | Implemented |
| BR-023 | Appendix parent overrides on the main audit grid are limited to Lead Auditors, not ordinary auditors, and not members | `staff_appendix_parent_override_allowed` | Implemented |
| BR-024 | SEV application type 2 is excluded when pass compliance rows are written | `PassComplianceWriter` | Implemented |
| BR-025 | Score backfill commands reconcile scores and write a revert snapshot. They do not change submission status | `BackfillTableBRollup` command description | Implemented |

Cellar type question greying (from `CellarTypes` defaults):

| Type | Value | Who | Questions greyed in the default map |
|---|---|---|---|
| A | 0 | Wine and bottle on site | none |
| B | 1 | Wine only | 6, 13, 14 |
| C | 2 | Wine and bottle off site | 3, 6, 13 |
| D | 3 | Bottlers plus own wine | 14 |
| E | 4 | Bottlers, services only | 3 |

Types D and E use pass 96 out of total 160. Default totals for A (165 / 107), B (145 / 94), and C (145 / 94) are template defaults in `CellarTypes`. Per-form totals in `eval_form` can differ. Label: **Implemented** defaults, not a guarantee that every year uses those totals.

## Submission status

Codes returned by `getStatusDescription`:

| Code | Label |
|---|---|
| 0 | New |
| 10 | In Progress |
| 20 | Submitted |
| 30 | Audit Busy |
| 40 | SEV |
| 50 | Passed |
| 55 | Audit Pass |
| 60 | Failed |
| 61 | Probation |
| 65 | Audit Fail |
| 90 | Deleted |

`getStatusText` (HTML badges) has no case for 30 or 40, so those codes render as Unknown on that helper. `printStatusLabel` omits 30, 40, 61, and 90. Label: **Implemented** inconsistency. Do not treat the three helpers as one catalogue without checking which method the screen calls.

The legacy overview lists an extra status Pass Probation (51) and a transition diagram. Status 51 was not in the three helpers above. Whether 51 still exists in data is **Unknown / Requires Confirmation**.

`knowledgeBase/BusinessRules.md` requires a reviewer assignment, a review timeframe, and confirmed notification delivery for every status change. Those controls were not verified. They are not rules of the current code until a controller shows them.

## Probation

Source of truth: `knowledgeBase/ProbationManagement.md` together with `App\Services\ProbationLifecycle`. Label: **Documented** and **Implemented**, except production scheduling.

| ID | Rule |
|---|---|
| BR-030 | Probation is created only for the first audit fail, status 65. Self-assessment fail 60 does not create it |
| BR-031 | One `group_probation` row per evaluation (`group_eval_form_id` unique). A later fail reuses the row |
| BR-032 | A new row is active for six calendar months. `probation_status` 0 is active. 1 is history |
| BR-033 | If the existing row is already expired, it is moved to history and the form stays 65 |
| BR-034 | Pass 50 or Audit Pass 55 closes the active row, except SEV application type 2. An IPW audit (type 1) closes via the sibling form: same group and eval form, status not 90, latest `application_date` |
| BR-035 | Staff may change the active end date and may move the row to history. Staff cannot create a row, hard-delete it, or attach it to a different evaluation |
| BR-036 | The end date itself is still active. Expiry is after that date |
| BR-037 | Daily process: if an attachment exists after expiry, mark history and `auto_processed`. If no attachment, delete `sawis_compliance`, set the form to 60, mark history, and set `auto_processed` |
| BR-038 | Monthly reminders go only to Member Administrator role id 14. Reminders do not set `auto_processed` |
| BR-039 | Mail uses `content_email` ids 9 (admin list change) and 11 (member), with a fallback if the imported template is missing |
| BR-040 | Each create, date or history change, pass removal, and automatic expiry writes an `auditlog` row in a transaction |
| BR-041 | Built-in probation grants are System Developer and IPW Administrator (`view/probation_view`, `edit/probation_view`). Data Capturer, Auditor, Lead Auditor, and member roles must not regain probation through grant synchronisation |

**Inferred** from the knowledge-base warning, not from a server inventory: production should run `php spark probation:process` only after this code is deployed, and a dry run should come first. Whether that job is scheduled is **Unknown / Requires Confirmation**.

A later commit subject, `1d6eba9` “loosen probation rules” (2026-09-15), is after the knowledge-base page’s described design. The service comments still match the table above. If “loosen” changed a threshold, re-read `ProbationLifecycle` before coding against an older copy of this file. The service on `054883d9` is the implementation.

## SAWIS, audits, and certificates

| ID | Rule | Evidence | Label |
|---|---|---|---|
| BR-050 | BIS is a read source for member profile, farm master, and SO2. The legacy integration readme says the application never inserts, updates, or deletes on BIS | `BIS_INTEGRATION_README.md` | Documented for the legacy client. The CI4 service was not line-audited for every SQL string. Treat writes to BIS as not allowed unless a later review finds one |
| BR-051 | SAWIS number `99999` is a demo that forces the farm flag and a “DEMO FARM” option in the legacy member form flow | `BIS_INTEGRATION_README.md` sections 2.1 and 2.2 | Documented for legacy pages. Not re-checked in `BisService` |
| BR-052 | Compliance badge Yes means `sawis_compliance.swsa_outcome = 1` for the group and year. `group_audit` types 50 and 51 apply only when no compliance rows exist | README | Documented |
| BR-053 | `group_audit` types 60 and 61 are suspend and un-suspend. They are not farm assessment audits | `GroupAuditModel::EXCLUDED_AUDIT_TYPE_IDS` | Implemented |
| BR-054 | IPW audit list pass/fail on the reports dashboard uses audit status id 1 passed and 2 failed. Completed audit status id 3 is a different constant | `GroupAuditModel` | Implemented |
| BR-055 | Members see and download only their selected group’s certificates, and only with `view/member_eval_form` or `view/member_view_status_only`. No selected group yields no certificates | `Certificates::resolveCertificateScope` | Implemented |
| BR-056 | Certificate PDFs follow the legacy bilingual layout and resolve farm number and farm name from SAWIS `FARM_MASTER` | README | Documented |
| BR-057 | Number transfer can complete or roll back. Rollback is refused unless the history row is in the completed state | `SawisNumberTransferService` status constants | Implemented |

## Documents

| ID | Rule | Evidence | Label |
|---|---|---|---|
| BR-060 | Download is by UUID at `document_uploads/download/{uuid}` | README | Documented |
| BR-061 | Files are stored at `public/assets/content/audit/<year>/<month>/` and `file_path` stores that relative path | README | Documented |
| BR-062 | PDFs open inline. The download response must not pass the original filename as CI4 `download()` contents | README | Documented |
| BR-063 | Member document lists are scoped like other member data. A missing scope must not list every group’s files | Pattern used in `Certificates` and `MemberEvaluations`; document views return 403. Exact document query not fully extracted | Inferred for documents. Confirm in `DocumentManagement` before relying on it |

The 10MB-per-file rule appears as an operator note in the README (`upload_max_filesize` at least 10M) and as a stated maximum in `knowledgeBase/BusinessRules.md`. The PHP `upload_max_filesize` value on the server is environment configuration, not a rule encoded in the repository. Label: **Documented** operational expectation.

## Access

| ID | Rule | Evidence | Label |
|---|---|---|---|
| BR-070 | Anonymous users may open only `PublicRoutes` patterns. `certified` and `certified-farms` require a logged-in non-member staff user | `PublicRoutes.php` | Implemented |
| BR-071 | Member class id is 2. Everyone else on an IPW audit is treated as staff for the score-grid exception | `ScoringRules::MEMBER_USER_CLASS_ID` | Implemented |
| BR-072 | Denial beats a missing permission. Route policy and menu grants both run | `Filters.php` | Implemented |
| BR-073 | Developer break-glass is the fine role System Developer with action `all_actions` and object `all_objects` | `RoutePolicyGrants::developerWildcard` | Implemented |
| BR-074 | Coarse menu grants for legacy role ids 1 and 2 cover users, member admin, live users, SAWIS compliance, certificates, probation, documents, reports, evaluation setup, and member evaluations | `RoutePolicyGrants::coarseMenuMatrix` | Implemented |
| BR-075 | One member role per group is the documented assignment UX (radio buttons). Admins can hold multiple roles and may use `user_group_id` 0 | `knowledgeBase/RBAC_Implementation.md` | Documented. The radio-button UI was not re-opened in this review |
| BR-076 | Equal-importance roles: the legacy overview says denials take precedence over allowances | `knowledgeBase/Overview.md` citing `system/class.rbac_bv.php` | Documented for the legacy RBAC class. The CI4 `RbacService` was not compared line by line. **Requires Confirmation** before relying on the precedence sentence |

## Mail and reminders

| ID | Rule | Evidence | Label |
|---|---|---|---|
| BR-080 | Outbound mail is HTML wrapped by `sendMail`. An empty type greets the recipient using the capitalised mailbox name. Type non-empty greets “EDI Team.” | `mail_helper.php` | Implemented |
| BR-081 | Probation mail template ids are 9 and 11 as in BR-039 | Probation knowledge base | Documented |

WhatsApp is not a business rule. Commit subjects mention “email & whatsapp feedback” as client feedback. No WhatsApp integration was found under `app/`.

## Conflicts that are not rules

| Statement in older docs | What the CI4 tree shows |
|---|---|
| Category weights must sum to 100% | Pass marks are 70%, 65%, or the fixed 96/160 pair |
| Files are virus-scanned and encrypted at rest | Not found in the upload notes that were read |
| Certificate tables `certificate`, `certificate_type`, `certificate_history` with 1-year and 2-year biodiversity types | PDF generation from passed evaluation forms |
| Status flow New → Busy → Submitted → Audit Busy → Pass / Fail / Probation as a strict state machine | Codes exist. A single validator that forbids every other transition was not identified |
| SEV is only status 40 | Status 40 remains labelled SEV, and application type 2 is also SEV Audit |
| `overview.md` product name Transactflow | IPW evaluation platform |

Human confirmation is required before anyone “corrects” the code to match the older knowledge-base pages, or deletes those pages.
