# Business rules — matchcentre.footprintapp.net

Evidence date: 2026-10-09. Source: `master` at `3bd3534` unless a rule says `origin/prod`.

Rules are taken from code. Each rule is **Documented** (written as a requirement somewhere in the repo), **Implemented** (enforced by code), or **Inferred** (a reasonable reading that the code does not state). No rule in this file is **Documented**. The repository has no business-rules document of its own.

If a rule is only an inference, it is marked **Inferred** and must not be treated as an approved policy.

## Tournament visibility and grouping

| ID | Rule | Provenance |
|---|---|---|
| BR-001 | A tournament with `Hide != 1` can be selected for the home list. The home view then shows a row only when `Hide == 0` | **Implemented** (`IndexController::indexAction`). Values of `Hide` other than 0 and 1 are **Unknown** |
| BR-002 | `HightedTourn == 1` places the tournament in Featured and not in the level tabs | **Implemented**. The column name is spelled that way. **Inferred** that it means highlighted |
| BR-003 | `Level` 1 is International, 2 is National, 3 is Provincial. The labels are in the view. The integers are in the controller | **Implemented** mapping in code. **Inferred** that no other level is shown |
| BR-004 | Current means `StartDate <= today` and `EndDate >= today`. Completed means `EndDate < today`. Upcoming means `StartDate > today`. Today is the server date in `Africa/Johannesburg` | **Implemented** |
| BR-005 | Upcoming groups are sorted by `StartDate` ascending | **Implemented** |
| BR-006 | On `master`, the home year is always 2019, even when `year` is passed | **Implemented** |
| BR-007 | On `origin/prod`, the year is the `year` parameter or the calendar year, and a tournament is included when `Hide != 1`, `LastUpdate` is new enough, and `EndDate` falls in that calendar year using the predicate written in `IndexController`. The year bucket also uses `EndDate` | **Implemented on prod/qa, not on master** |
| BR-008 | A fixture with `Hide == 1` is not shown on the match page. The visitor is redirected to `/` | **Implemented** |
| BR-009 | Result queries that build the public list require `closed = 1` (and in `GameResults`, `F.Closed = 1`) and `Hide != 1`. `getFixtureResults` also requires `FixtureDate` at or before the current datetime | **Implemented**. Whether `Closed` and `closed` are the same column is **Unknown** (MySQL column-name case) |
| BR-010 | Home results and `getFixtureResults` return at most 8 fixtures, newest date and start time first. Upcoming returns at most 8, soonest first | **Implemented** |

## Logs and points

| ID | Rule | Provenance |
|---|---|---|
| BR-011 | Log rows are built from closed fixtures only (`closed = 1` on `Fixtures_Log_List`) | **Implemented** (`Model_Logs::getUpdates`) |
| BR-012 | When tournament `PointsSystem == 1`, log order is pool, then `PTS` descending, wins, point difference, tries. Otherwise order is pool, wins, point difference, tries | **Implemented** |
| BR-013 | `PTS` in that query is the sum of `T_Points`, `L_Points`, `W_Points`, `D_Points`, `TD_Points`, and `F_PTS` | **Implemented** as SQL. The meaning of each column in the database view is **Unknown** |
| BR-014 | When tournament `Structure == 2`, the log view groups teams under a pool name taken from JSON column `Pools` | **Implemented** in `TournamentsController::logs`. **Inferred** that structure 2 means a pool competition. Other structure values are **Unknown** |
| BR-015 | Stats-tab points for a player are `(tries * T_Tries) + (conversions * T_Conversion) + (penalties * T_Penalties) + (drop goals * T_Dropgoal) + (22-metre tries * T_22Mtry)` using the match-report point columns | **Implemented** (`TournamentsController::stats`) |
| BR-016 | The tries count on that tab adds `tries` and `22Metertries` together | **Implemented** |
| BR-017 | Commentary event codes map to tournament point columns: 2 try, 3 conversion, 4 penalty, 5 drop goal, 6 penalty try, 53 and 56 both 22-metre try | **Implemented** (`Model_Fixture::getPoints`). The numeric point values are data, not hard-coded |
| BR-018 | Tournament settings also select `PenaltyTry`, `PowerPlayTry`, `Allow22MeterTry`, `WinnersPoints`, `DrawersPoints`, `LostPoints`, `TryPoints`, `ScoringTries`, `BunosPoints`, `ScoringTriesDifference`, `FtimeMin`, and `HtimeMin` | **Implemented** as selected columns. How each one changes a score beyond BR-015 and BR-017 was not fully traced. **Unknown** where not cited |

## Squads and people

| ID | Rule | Provenance |
|---|---|---|
| BR-019 | `SquadType > 12` loads players from `gir_profile`. Any other type loads `profile` | **Implemented**. Why 12 is the cut-off is **Unknown / Requires Confirmation** |
| BR-020 | The squad player list is JSON in `squad.Players`. A player is skipped if the profile lookup returns no `UserID`, or if that `UserID` was already added | **Implemented** |
| BR-021 | Jersey number defaults to 0 when empty. Captain and vice flags default to 0 when empty | **Implemented** |
| BR-022 | Age shown for a player is computed from `DateOfBirth` | **Implemented** (`age()`). The exact birthday-boundary formula is in that function |
| BR-023 | Match officials on the match page are ids in `MatchOfficials` JSON, resolved to `FirstName` + `LastName`, plus `boksmart` | **Implemented** |
| BR-024 | A profile position label falls back to the text `POS: not assigned`. Organisation falls back to `not assigned` | **Implemented** |

## Login and access

| ID | Rule | Provenance |
|---|---|---|
| BR-025 | A visitor who already has a `Zend_Auth` identity and opens `/login` is sent to `/` | **Implemented** |
| BR-026 | Login requires both email and password. The message when either is missing is "Please enter your login details" | **Implemented** |
| BR-027 | Only a `co_credetials` row with `FPLogin = 1` may attempt a password check. Any other email is replaced before authentication so it cannot match | **Implemented** |
| BR-028 | The stored password is compared as an MD5 digest | **Implemented** |
| BR-029 | One fixed submitted password, written as a literal in `LoginController::indexAction`, causes the action to authenticate with the stored hash instead of the MD5 of what was typed | **Implemented**. Whether this is an approved support path is **Unknown / Requires Confirmation**. The literal is not repeated here |
| BR-030 | After a valid login, `Status > 0` redirects to `/`. Any other status redirects to `/first` | **Implemented**. No controller for `/first` was found. Destination behaviour is **Unknown** |
| BR-031 | If authentication fails and the credential password is an empty string, a reset key is stored and a reset email is sent | **Implemented** |
| BR-032 | If no credential row exists but `profile.Email` matches, a credential row is inserted with a reset key and the same email is sent | **Implemented** |
| BR-033 | Otherwise the visitor sees "Email and/or password is invalid!" | **Implemented** |
| BR-034 | The reset message says the password expired because it has not been changed in a while. No age check or expiry column was found in this action | **Implemented** email text. The expiry reason is **not** enforced by a date comparison in the code that was read. **Requires Confirmation** |
| BR-035 | Logout clears the auth identity and destroys the session | **Implemented** |
| BR-036 | Session cookie lifetime is 864000 seconds | **Implemented** in `application.ini` |
| BR-037 | Match, tournament, team, commentary, ajax refresh, and Blue Card actions that were read do not check `Zend_Auth` | **Inferred** public access. A plugin outside those actions could still block them. `LayoutPicker` does not check identity |

## Blue Card

| ID | Rule | Provenance |
|---|---|---|
| BR-038 | Incident cause options are Tackle, Ball Carry, Ruck, Scrum, Line-out, Open play, Collision, Unknown | **Implemented** |
| BR-039 | Applicant roles are Referee, Coach, Team Manager, Physiotherapist, Medical Doctor, Parent, Guardian, Player | **Implemented** |
| BR-040 | If injury date is missing or empty, it is set to today (`d/m/Y`) | **Implemented** |
| BR-041 | Role `Player` uses the player email template. Any other role uses the applicant template | **Implemented** |
| BR-042 | Player gender `Female` uses the pronoun "she". Every other value uses "he" | **Implemented** |
| BR-043 | Union contacts for `UnionName` and any `Contact` emails are copied on the message | **Implemented** |
| BR-044 | The submission is stored on `BlueCard_Consents` after the mail attempt. `Contact` is stored as JSON | **Implemented** |
| BR-045 | The POST check is commented out, so the action runs for a request that is not a form post | **Implemented** |

## Menu

| ID | Rule | Provenance |
|---|---|---|
| BR-046 | The menu home loads venues for id 284 only | **Implemented** literal |
| BR-047 | Nutrition allergens are a JSON list of ids on the nutrition row, expanded through `Model_Allergens::getInRange` | **Implemented** |

## Cache and files

| ID | Rule | Provenance |
|---|---|---|
| BR-048 | Tournament images are base64 strings on the tournament row, decoded to JPEG files named by `TID` | **Implemented** |
| BR-049 | The profile page reads the tournament from `public/archives/tournaments/{id}.json` and does not query `tournament` when that file is missing | **Implemented** |
| BR-050 | `public/archives/` is gitignored, so a new deployment has no cache until a writer action runs | **Implemented** ignore rule. Operational consequence **Inferred** |

## Deployment identity

| ID | Rule | Provenance |
|---|---|---|
| BR-051 | CodeDeploy installs the tree at `/mnt/data/www/html/footprintapp.net/matchcenter.footprintapp.net/` owned by `apache:apache` | **Implemented** in `appspec.yml` |
| BR-052 | Only Bitbucket branches `qa` and `prod` run that deploy. `master` does not | **Implemented** in `bitbucket-pipelines.yml` |

## Rules that look like policy but are not enforced

| Observation | Why it is not a rule yet |
|---|---|
| Reset email claims the password expired from age | No date comparison in `indexAction` |
| `.gitignore` lists `application/configs/application.ini` | The file remains tracked |
| Ini says production hides PHP errors | `public/index.php` turns `display_errors` on before bootstrap. Which setting wins after Zend reads the ini is **Unknown** |
| Commented maintenance page | It does not run |

## Requires confirmation

1. Approved meaning of `Hide`, `Level`, `Structure`, `PointsSystem`, and `SquadType`.
2. Whether the fixed login literal in BR-029 is still accepted.
3. Whether public match pages are intentionally unauthenticated.
4. Whether company, permit, and accreditation behaviour in `companiesold`, `companyonl`, and `CronController` is in scope for this product. Those paths were not turned into approved rules here because several of their model classes are missing and no document says they belong to Match Centre.
