# Business rules — sarugby (SA Rugby)

Evidence date: 2026-10-04.

Source: `git@bitbucket.org:overdrive/sarugby.git`, `master` at `6048eba` (2019-04-28).

No business-rules document exists in the source repository. Rules below are taken from PHP, SQL fragments, and email HTML. Anything marked **Inferred** is a conclusion from that evidence, not a written policy. Secret values are not reproduced.

## Public tournaments

### BR-001 — A hidden tournament is excluded from public lists

**Implemented.**

Home, `Model_Tournament::ListTourn`, and latest-results queries use `Hide != 1`. Featured placement also requires `Hide == 0` in the home loop.

Evidence: `IndexController::indexAction`, `Model_Tournament::ListTourn`, `Model_Fixture::fixturesResultsTest2`.

The ajax export `listTournamants` filters on `Updated` only. It does not repeat `Hide != 1`. A hidden tournament that was updated can still be written into the JSON cache.

### BR-002 — Tournament level selects the public group

**Implemented** grouping. The names are **Inferred** as product language because they are PHP keys, not a lookup table in this repo.

| `Level` | Group key |
|---|---|
| 1 | `international` |
| 2 | `national` |
| 3 | `provincial` |
| other, when not featured | Not placed in those three groups |

`HightedTourn == 1` (column spelling in code) places the row in `Featured` instead of the level groups.

Current means `StartDate <= today` and `EndDate >= today`. Completed means `EndDate < today`. Upcoming means `StartDate > today`. Comparison uses `date("Y-m-d")` after PHP timezone `Africa/Johannesburg` is set in `public/index.php`.

Evidence: `IndexController::indexAction`.

### BR-003 — Home year defaults to this year

**Implemented.**

Query `year` empty becomes `date("Y")`. Tournaments must have `StartDate >= {year}-01-01`. Rows are then kept only when the year of `StartDate` equals that year.

Evidence: `IndexController::indexAction`.

### BR-004 — Latest results show eight non-hidden games

**Implemented.**

When the cache is stale, results come from `GameResults_view`, `Hide != 1`, order `FixtureDate DESC`, `StartTime DESC`, limit 8.

Evidence: `Model_Fixture::fixturesResultsTest2`.

### BR-005 — Points from a score line

**Implemented.**

```text
points = tries * T_Tries
       + Conversions * T_Conversion
       + Penalties * T_Penalties
       + DropGoals * T_Dropgoal
       + 22Metertries * T_22Mtry
```

`T_*` values are columns on the row from `Fixture::getTournReport`. The running try total adds `22Metertries` into `tries` as well as scoring them on their own weight.

`Model_Tournament::getTournySettings` also selects `PenaltyTry`. That column is not used in `stats`.

Evidence: `TournamentsController::stats` around the points assignment. `Model_Tournament::getTournySettings`.

**Unknown / Requires Confirmation:** the numeric weights. They live in the database, which was not read.

### BR-006 — Pool logs only when structure is 2

**Implemented.** **Inferred** that structure 2 means a pool tournament, because the code JSON-decodes `Pools` only in that branch.

Evidence: `TournamentsController::logs`.

## Company registration and access

### BR-007 — One company per registration number

**Implemented.**

`Companies_Profile.RegistrationNumber` match returns the existing `CID` and the registration is rejected.

Evidence: `Companies_RegisterController::check_Company`, `newcompanyAction`.

### BR-008 — One member per ID number or email

**Implemented.**

`Company_Members` is searched with `IDNumber = ? OR Email = ?`. A hit rejects registration with the same message as a duplicate company ("Your account already exist").

Evidence: `check_IDNumber`, `newcompanyAction`.

The company module's `newcompanyAction` checks the person and does not use the same company-number check at the start of the action. It is a second path.

### BR-009 — The first user is an administrator

**Implemented.**

`Rol` is set to the string `Administrator` before insert.

Evidence: `Companies_RegisterController::newcompanyAction`.

### BR-010 — Identity document type follows the country string

**Implemented.** **Inferred** that 1 means a South African ID and 2 means another document. The code only sets the integers.

`IDType` is 1 when `Country` equals `SOUTH AFRICA`. Any other value sets `IDType` 2.

Evidence: `newcompanyAction`.

### BR-011 — Registration email says an administrator must activate the company

**Implemented** as the email body.

The message says the account will be reviewed before the company profile can be activated, that this can take up to 48 hours, and that extra documents are required before the company can connect to an event. Contact is `accreditation@sarugby.co.za` and phone `+27219287000`.

Evidence: HTML in `newcompanyAction`.

### BR-012 — Confirming the email allows login immediately

**Implemented.**

A matching `Validate_Key` sets `Validate_Key` empty, `Email_Confirmed` 1, `Email_Confirmed_Date` today, `AllowLogin` 1. The page says the account is now active.

Evidence: `validateemailAction`.

**Discrepancy with BR-011.** The email describes a human review of up to 48 hours. This action does not wait for a reviewer. Which rule is intended is **Unknown / Requires Confirmation**.

### BR-013 — Companies area pages require a Zend auth identity

**Implemented.**

Missing identity redirects to `/companies/login/`.

Evidence: `init` in `Companies_IndexController`, `UsersController`, `PermitsController`, `EventsController`.

`Companies_LoginController` itself redirects an existing identity to `/companies/`.

The login adapter does not add `AllowLogin = 1` in PHP. If that flag is enforced, it is inside the `CompaniesLogin` table or view, which is not defined in this repository. **Unknown / Requires Confirmation.**

### BR-014 — A fixed password skips the company password hash

**Implemented.** Not marked as an approved rule.

`Companies_LoginController::indexAction` compares the submitted password to a literal. On a match it replaces the password with `CompanyProfile::adminLogin` and does not `md5` the submitted value. The literal is not repeated here.

The same pattern exists in `default/controllers/LoginController.php` with a different literal, against `co_credetials`.

**Unknown / Requires Confirmation:** whether either bypass is still accepted. It should be treated as a defect until a person says otherwise.

### BR-015 — Company passwords are MD5

**Implemented.**

Registration, member create (when login is allowed), and password reset store `md5` of the password. Reset also emails the new password in clear text.

Evidence: `RegisterController::newcompanyAction`, `UsersController::addAction`, `LoginController::resetAction`.

### BR-016 — Forgotten-password link and the address in the sentence differ

**Implemented.**

The href is `http://{host}/companies/login/reset/user/{key}`. The text inside the anchor is `http://{host}/login/reset/user/{key}`.

Evidence: `Companies_LoginController::forgotAction`.

The greeting uses `FirstName` for both name parts.

### BR-017 — Member types a company can assign

**Implemented** list.

Administrator, Director, Management, Operations, Driver, Technician, General Staff.

Evidence: `UsersController::init`.

No controller in this module was found that limits actions by that string, other than storing it.

### BR-018 — New member login is off unless requested

**Implemented.**

On add, if `AllowLogin` is not posted, it is stored as 0. If it is posted, a random password is generated and hashed. This action does not send that password.

Evidence: `UsersController::addAction`.

### BR-019 — Session lifetime

**Implemented** configuration.

`resources.session.cookie_lifetime = 864000` (10 days). **Inferred** unit conversion only.

Evidence: `application/configs/application.ini`.

## Permits

### BR-020 — Permits in this controller are for event 27

**Implemented** hardcoding.

`createAction` calls `getEvent(27)`. `addpermitAction` sets `$AC = 27`, `EID` 27, and `getRef(27)`.

Evidence: `PermitsController`.

**Unknown / Requires Confirmation:** what event 27 is. The database was not read.

### BR-021 — A new permit starts at status 2

**Implemented.**

`Status` is set to 2 on insert. The PHP does not define the name of status 2. The email says an administrator will review the request.

**Inferred** that 2 means submitted and waiting. **Unknown / Requires Confirmation.**

Evidence: `addpermitAction`.

### BR-022 — Permit number format

**Implemented.**

After insert, `PermitNr` becomes `PE` plus the insert id padded on the left with zeros to 6 characters. The email shows `{PermitNr}/{year of RequestDate}`.

`PermitRefSerial` is a random 13-digit zero-padded number.

Evidence: `addpermitAction`.

### BR-023 — Known members replace typed occupant details

**Implemented.**

When occupant `Type` is 1, or when a driver name is posted, `GetMemberDetails` may overwrite name, surname, and ID from `Company_Members` and set `MID`.

Evidence: `addpermitAction`.

### BR-024 — Cancellation cutoff and load-out priority are email text

**Implemented** as sentences in the permit email. Not found as a clock check in `addpermitAction` or `savepermitAction`.

- Changes may be submitted until 1pm.
- Changes for the next day after that time will not be accepted.
- Load-out time is limited and approved first come, first served. The email spells one instance "Loud-out".
- Administrators may change the requested date and time.
- Work permits should have a health and safety file with 14 named items (public liability insurance, letter of good standing, health and safety plan, legal appointments, tools and equipment lists, PPE issue lists, risk assessment, tool box talks, MSDS copies, medical records, training records, professional registration, safe work procedures, emergency procedures).

Evidence: email HTML in `addpermitAction`.

**Unknown / Requires Confirmation:** whether an administrator tool outside this repository enforces the 1pm rule.

### BR-025 — Permit email is sent even when the surrounding success block is skipped

**Implemented** control flow.

`$this->sendMail(...)` sits after the `if ($insert)` block and uses `$permit`. If the insert did not load `$permit`, that call is still in the POST path.

Evidence: `PermitsController::addpermitAction`.

## Menu and Blue Card

### BR-026 — Allergens on a nutrition row are a JSON list of ids

**Implemented.**

`nutritionAction` `json_decode`s `Nutritions.Allergens` and loads `Allergens` with `getInRange`.

Evidence: `Menu_SessionController::nutritionAction`.

### BR-027 — Menu QR target is the public menu URL

**Implemented** in the view.

`https://www.sarugby.online/menu/session/menu/id/{VID}` encoded into a Google Chart QR image.

Evidence: `menu/views/scripts/session/menu.phtml`.

### BR-028 — Blue Card lists are fixed in the controller

**Implemented.**

Incident cause: Tackle, Ball Carry, Ruck, Scrum, Line-out, Open play, Collision, Unknown.

Role: Referee, Coach, Team Manager, Physiotherapist, Parent, Guardian, Player.

Positions: 15 labels from loose-head prop through full back, numbered 1–15. "6 - Open side flank" and "7 - Blind side flank" are the strings in code. **Unknown / Requires Confirmation** whether those two labels are swapped relative to current BokSmart naming. The code is the evidence for what the form shows.

Unions and ethnicity come from `ef_unions` and `Ethnicity`.

Evidence: `Bluecard_IndexController::indexAction`.

### BR-029 — The Blue Card reference is not a stored record

**Implemented.**

`random_id(4)` runs while the thank-you HTML is assigned. There is no insert. The thank-you text tells the person to check email for documentation. This action does not send that email.

Contact text on the page: Sports Concussion SA `011-3047724` (office), `0825746918` (after hours), `www.sportsconcussion.co.za`, `sportsconcussion@mweb.co.za`, and `www.BokSmart.com/Concussion`. Signed "The BokSmart Team".

Evidence: `Bluecard_IndexController::indexAction`.

## Cron and external accreditation

### BR-030 — Only accreditation code 14 is posted to AppWiz

**Implemented.**

`queingAction` calls `senddata_to_Api` only when `$data["AC"] == 14`. Other queued rows still go through `addtozip`.

The POST body uses `G` = `Guest`, `A` = `acrSARugby`, plus barcode, name, surname, ID number, category, zone numbers concatenated, and `TheFile` from `User_pic`.

Evidence: `CronController`. The application model class is **not in git**, so this path does not run from this tree alone.

### BR-031 — Default login is limited to FPLogin accounts

**Implemented** in the default module, which is a Footprint-worded flow.

If no `co_credetials` row matches the username with `FPLogin = 1`, the typed email is replaced with `uniqid()` before authenticate, so the password check cannot succeed for that request.

Empty stored password, or a profile email with no credential row, starts a reset mail from `support@footprint.net`.

`Status > 0` goes to `/`. Otherwise the code redirects to `/first`.

Evidence: `default/controllers/LoginController.php`. `Model_Credentials` is missing, so the action cannot complete as written.

## Data rules that are only column names

These columns are selected or written. Their allowed values are not defined in PHP beyond the uses above.

| Item | Where |
|---|---|
| Tournament scoring settings `Tries`, `Conversion`, `Penalties`, `Dropgoal`, `PenaltyTry`, `22Mtry` | `Model_Tournament::getTournySettings` |
| Notification hour flags on `TournamentDetails_view` (`FTANotification_*`, `STANotification_*`, and others) | `Model_Tournament::Tourment_min_Details` |
| `Companies_Profile`, `Company_Members`, `Company_Events` | Company models |
| `Accreditation_Permits`, `Accreditation_List`, `EventGates`, loading zones and services, `vehicletypes` | Accreditation models |

No row-level security, rate limit, or captcha was found on these actions. Queries concatenate request values into SQL fragments (`where` strings and `fetchRow` conditions). That is **Implemented** behaviour and a defect risk, not a business rule.
