# PRD — sarugby.online

Evidence date: 2026-09-28. Source: `git@bitbucket.org:overdrive/sarugby.online.git`, `master` at `8a0437d` (2020-01-07).

There is no prior PRD in the repository or in Silicon Ark. Every requirement below was recovered from the code. Provenance is marked on each item.

The default branch is the only branch. Whether it is what visitors receive at `www.sarugby.online` is **Unknown / Requires Confirmation**.

## Product purpose

The site presents SA Rugby tournaments to the public and collects operational data around events: company accreditation, vehicle permits, and Blue Card concussion consents. A food-menu module shows tournament catering sessions. A Footprint-branded login exists beside the accreditation login.

Provenance: **Inferred** from module responsibilities and from titles and email copy. No vision statement is in the repository.

The layout title is "SA Rugby - Official Home of the Springboks" (`application/layouts/default.phtml`). Accreditation email signs off "SA Rugby Event Administration" and gives the contact centre `+27219287000` and `accreditation@sarugby.co.za` (`companiesold` `RegisterController`). The Blue Card thank-you copy signs off "The BokSmart Team".

## Users and roles

| Role | What they do | Evidence | Provenance |
|---|---|---|---|
| Public visitor | Opens the home page, a tournament, a team, fixtures, results, news, a food menu, or the Blue Card form. No login is required on those controllers. | `default` `IndexController`, `TournamentsController`, `NewsController`; `menu` `SessionController`; `bluecard` `IndexController` | Implemented |
| Footprint user | Signs in on `/login` against table `co_credetials` where `FPLogin = 1`. Password stored as MD5. | `default` `LoginController` | Implemented |
| Company applicant | Submits a company and an administrator person. Must confirm email before `AllowLogin` is set. | `companiesold` `RegisterController` | Implemented |
| Company user | Signs in against table `CompaniesLogin` (email + MD5 password). Roles offered when adding a person: Administrator, Director, Management, Operations, Driver, Technician, General Staff. | `companiesold` `LoginController`, `UsersController` | Implemented |
| Company administrator | The registering person is stored with `Rol = "Administrator"`. | `RegisterController::newcompanyAction` | Implemented |
| Accreditation reviewer | Named only in email copy: the account "will be reviewed by an administrator" and activation "can take up to 48 hours". No review screen is in this repository. | Registration email HTML | Documented in the email. The review UI is **Unknown** |
| Blue Card applicant | Submits the concussion form as Referee, Coach, Team Manager, Physiotherapist, Medical Doctor, Parent, Guardian, or Player. | `bluecard` `IndexController::indexAction` | Implemented |
| Cron caller | Requests `/cron/queing`, `/cron/matches`, `/cron/getaccre`, `/ajax/getdata` with no authentication check in those actions. | `CronController`, `AjaxController` | Implemented. Who is allowed to call them is **Unknown** |

There is no role table in the code that was read. Company roles are a hard-coded list passed to the view.

## Main workflows

### W1 — Browse tournaments for a year

Trigger: visitor opens `/` (`default` `IndexController::indexAction`).

Inputs: optional query `year`. Empty becomes the current calendar year. Timezone in this controller's front controller is `Africa/Johannesburg`.

Behaviour:

1. Read `{year}.json` from the current working directory (not from `UPDATE_PATH`). The full path variable `$fullPath` is built but the read uses only the file name.
2. Load tournaments with `Hide != 1`, `LastUpdate` at or after the JSON timestamp, and `StartDate` on or after `{year}-01-01`.
3. Write JPEG files from base64 `SmallImage` and `TournImage` under `UPDATE_PATH/images/tournaments/`, write one JSON file per tournament, and write the year file under `UPDATE_PATH/tournaments/years/`.
4. Split visible rows (`Hide == 0`) into Featured (`HightedTourn == 1`) and, by `Level`, international (`1`), national (`2`), and provincial (`3`). Each level is split into Current, Completed, and Upcoming by comparing `StartDate` and `EndDate` to today.
5. Upcoming lists are sorted by `StartDate` ascending.
6. The action then calls `lasttestresults()` and `upcomingfixtures()`.

Provenance: **Implemented**.

Empty or unreadable JSON is not given a dedicated message in the portion read. A missing file would surface as a PHP warning because `display_errors` is enabled in `public/index.php`.

### W2 — Open a tournament

Trigger: `/tournaments/profile` with parameter `tour` (the tournament id).

Behaviour (`TournamentsController::profileAction`):

1. Read `UPDATE_PATH/tournaments/{id}.json`.
2. Refresh logs via `logsUpdate`, then assign logs. If `Structure == 2`, logs are grouped into pools (`logs()`).
3. Load matches and stats for the id.
4. Load completed results and upcoming fixtures from `Model_Fixture`.

Other tournament actions in the same controller: `fixtures`, `result`, `match`, `team`, `commentary`, `getmatchreport`, plus `*test` variants and `resultold`. `getmatchreportAction` fetches HTML from `https://footprintapp.net/pdf/matchreport/id/{id}` and streams an A4 PDF named `{SquadName1} vs {SquadName2} Report.pdf`.

`teamAction` loads the squad, its members, results, and upcoming fixtures. Member rows come from `profile` unless `SquadType > 12`, in which case they come from `gir_profile`. Sort field depends on `SquadType`: jersey number when `< 2`, rank number when `> 4` and `< 12` or when `== 12`, otherwise last name. The meaning of each `SquadType` number is **Unknown / Requires Confirmation**. The expansion of "GIR" is **Unknown / Requires Confirmation**. `gir_profile` joins `co_credetials` and `ef_unions` (`Model_GirProfile`).

Provenance: **Implemented**.

### W3 — Read news

Trigger: `/news` (`NewsController::indexAction`).

The action loads tournaments with `Hide != 1`, reads `Results_all.json` and `Fixtures_all.json` from `UPDATES_PATH` (that constant is not defined in `public/index.php`), and loads every row from `News_Articles_SA_Rugby`.

Provenance: **Implemented**. The undefined constant is an observed defect, not a stated requirement.

### W4 — Footprint login and password reset

Trigger: POST to `/login` with `data[Email]` and `data[Password]`.

Behaviour (`default` `LoginController`):

1. If Zend_Auth already has an identity, redirect to `/`.
2. Look up `co_credetials` where `Username` equals the email and `FPLogin = 1`. If that row is missing, replace the email with `uniqid()` so the following authentication fails.
3. Credential sent to the adapter is `md5` of the password. A fixed password string, present in the controller, skips the hash and uses the stored password instead. This is recorded as implemented behaviour and needs human review. The string is not repeated here.
4. On success, replace the auth identity with the `profile` row for that `UserID`, stamp `LastLogin`, and redirect to `/` when `profile.Status > 0`, otherwise to `/first`. No `first` controller exists in `application/modules`.
5. On failure, if the credential row exists and `Password` is empty, write a `ResetKey` and email a link `http://{host}/login/reset/user/{key}`. The mail says the Footprint password expired. From address `support@footprint.net`, display name "SARU E-Filing", subject "Footprint Credentails".
6. If no credential row exists but a `profile` row has that email, insert a credential row with the reset key and send the same mail.
7. Otherwise show "Email and/or password is invalid!".
8. Logout clears Zend_Auth and the session and redirects to `/`.

`resetpasswordAction` emails a reset link when posted an email that matches `co_credetials.Username`. `resetAction` (further in the same file) consumes `ResetKey`. Password-length rules were not found in the portion read.

`forgotAction` and `registerAction` exist. `forgotAction` is empty. `registerAction` only loads countries from `Model_DbTable_Countries`. That class was not in the model file list. Provenance: **Implemented** as incomplete.

The login form is not protected by a captcha in this controller.

### W5 — Register a company (companiesold)

Trigger: POST `/companies/register/newcompany` as implemented in `Companies_RegisterController`. The URL prefix `/companies/` does not match the module directory `companiesold`. See the routing discrepancy in `technical-documentation.md`.

Inputs: `P_data` (person) and `data` (company), including `RegistrationNumber`, `IDNumber`, `Email`, `password`, `Country`, `TradingName`, `FirstName`, `Surname`.

Behaviour:

1. Reject when `Companies_Profile.RegistrationNumber` already exists. JSON `OK = 2`, message that the account already exists and the user should contact SARU.
2. Reject when `Company_Members` already has that `IDNumber` or that `Email`. Same JSON message.
3. Otherwise store the person with MD5 password, `Rol = Administrator`, `DateCreated` today, `IDType = 1` when `Country` is the string `SOUTH AFRICA`, else `IDType = 2`, and a random `Ref`.
4. Insert the company with random `RefNumber`, `CreatedBy` and `AdminID` set to the new member id, `CreatedDate` today.
5. Store `Validate_Key` on the member and email `http://www.sarugby.online/companies/register/validateemail/id/{key}`.
6. Email text says the profile will be reviewed, activation can take up to 48 hours, and the company cannot sign up to an event until extra documents are added. Those checks are not enforced in this action. Provenance of the 48-hour sentence: **Documented** in the email. Enforcement: **Unknown**.
7. Success JSON `OK = 1`. Exceptions return `OK = 0`.

`validateemailAction` clears `Validate_Key`, sets `Email_Confirmed = 1`, `Email_Confirmed_Date` to today, and `AllowLogin = 1`. A missing key tells the visitor the account may already be active. No key redirects to `/companies/login/`.

The registration page (`indexAction`) loads B-BBEE levels (`BBEE_Level`), company types (`Accre_Company_Types`), and countries.

Mail is sent through SMTP `mail.sarugby.co.za` port 25 as `accreditation@sarugby.co.za`, subject "SARugby Notification". The mailbox password is in the controller. It is not copied into this PRD.

### W6 — Register a company (companyonl)

`Company_IndexController::newcompanyAction` is a second registration path.

It rejects an existing `IDNumber` or `Email` the same way. It does not reject an existing company before insert. It sets the new member's `CID` to the id returned by `check_Company`, which is `0` when the registration number is new. It does not call `Create_Company` on this path. The validation link is `http://www.sarugby.online/company/index/validateemail/id/{Member_id}` (the member id, not a `Validate_Key`).

Provenance: **Implemented**. Which of W5 and W6 is the live registration form is **Unknown / Requires Confirmation**.

### W7 — Company sign-in

Trigger: POST email and password to the companies login action.

Behaviour (`Companies_LoginController`):

1. If already authenticated, redirect to `/companies/`.
2. A fixed password string in the controller loads the stored password via `Model_CompanyProfile::adminLogin` and uses that value. Any other password is MD5-hashed first. The string is not repeated here. Human review is required.
3. Authenticate against `CompaniesLogin` columns `Email` and `password`.
4. On success, store the identity row, stamp `LastLogin`, redirect to `/companies/`.
5. On failure, show "Bad entered credentials".
6. Logout redirects to `/companies/login`.

`PermitsController`, `UsersController`, `EventsController`, `IndexController`, and `CompanyController` in this module redirect to `/companies/login/` when Zend_Auth has no identity. `RegisterController` and `LoginController` do not require an identity.

Forgot-password writes `ResetKey` on `Company_Members` and emails a link. The visible link text uses `/login/reset/user/` while the `href` uses `/companies/login/reset/user/`. Reset stores MD5 of the new password, sets `AllowLogin = 1` and `Email_Confirmed = 1`, and emails the new password in clear text.

Provenance: **Implemented**.

### W8 — Company home, people, and permits

Company home (`Companies_IndexController::indexAction`) counts members and permits for `$this->identity->CID`. The same `init` also sets `$this->_CID = 35`. `getpermitpdfAction` pulls HTML from `https://www.footprintapp.net/pdf/getpermit/id/{id}`, renders A4 landscape PDF with dompdf, and writes it under `AC_PATH/loadin/pdf/`. `AC_PATH` is not defined in `public/index.php`.

Add person (`UsersController::addAction`): if `AllowLogin` is set, generate a short random password and store its MD5. Always set `CID` from the identity and `DateCreated` to today. Optional base64 image is written to `UPDATE_PATH/accreditation/{MID}_image.jpg`. The JSON result uses `OK = 0` for success and `OK = 1` for failure.

Create permit (`PermitsController::createAction` and `addpermitAction`):

- Event id is hard-coded to `27`.
- Load-in and load-out dates come from JSON columns `LoadinActiveDate` and `LoadOutActiveDate` on the accreditation list row (`d/m/Y` strings).
- Occupants with `Type == 1` are matched to `Company_Members` by `IDNumber` and their name fields are overwritten from that row.
- A driver with a name is appended to the occupant list with `Driver = 1`.
- New permit: `Status = 2`, `PermitNr` later rewritten to `PE` plus zero-padding plus the insert id, `PermitRefSerial` a random 13-digit string, `EID = 27`, `MID` and `CID` from the identity.
- Email states: an administrator will review the request; cancellations or changes may be submitted until 1pm daily and will not be accepted for the next day after that time; load-out is first come, first served; administrators may change the requested date and time; work permits should travel with a listed health-and-safety file. Those deadline and document rules are **Documented** in the email. They are not checked in `addpermitAction`.
- The load-in versus load-out label in that email is chosen by comparing `$permit["Surname"]` to `2`. That comparison looks like a defect. Provenance: **Implemented** as written.

`savepermitAction` updates an existing permit and stamps `ApplicantModifiedDate` and `ApplicantModifiedBy`. It does not re-send the email.

`EventsController::indexAction` loads `Model_AccreditationList::getEvents(2)` and `print_r`s the result. It does not render a view.

### W9 — Submit a Blue Card

Trigger: `/bluecard` shows the form. `/bluecard/index/submit` reads `data` and does not check `isPost` (the check is commented out).

Form lists (`indexAction`):

- Incident cause: Tackle, Ball Carry, Ruck, Scrum, Line-out, Open play, Collision, Unknown.
- Applicant role: Referee, Coach, Team Manager, Physiotherapist, Medical Doctor, Parent, Guardian, Player.
- Playing positions 1–15 with the names in the controller (loose-head prop through full back).
- Unions from `ef_unions` via `Model_Unions::getUnions`.
- Ethnicity from table `Ethnicity`.
- A random reference of 4 bytes, hex-encoded, upper case.

Submit behaviour:

1. If `ApplicantRoll` is `Player`, the mail body is `player_contant` and the player name is the applicant. Otherwise the mail body is `contant`, the player name is `PlayerName` + `PlayerSurname`, and the pronoun is "she" when `PlayerGender` is `Female`, else "he".
2. Contact emails in `data[Contact]` are added as CC. Mail is sent to `ApplicantEmail` with those CCs, or to each CC if the applicant email is empty.
3. Attachments are PDFs and one spreadsheet read from `UPDATE_PATH/bluecard/` (research consent, infographic, 2018 advice sheet, concussion register template, World Rugby return-to-play sheet dated in the filename 22 05 2017, management infographic, prevention PDF).
4. `data[Contact]` is JSON-encoded and the row is inserted into `BlueCard_Consents`.
5. The thank-you HTML includes the reference, Sports Concussion SA phone numbers `011-3047724` and `0825746918`, `sportsconcussion@mweb.co.za`, `www.sportsconcussion.co.za`, and `www.BokSmart.com/Concussion`, plus download links under `https://sarugby.online/public/archives/bluecarddownload/`.

Provenance: **Implemented** and **Documented** in the thank-you HTML. `testAction` sends the pack to a hard-coded address and should be treated as a leftover, not a product requirement.

### W10 — Food session menu

Trigger: `/menu/session/dates?id={venue}`, `/menu/session/menus?id={session}`, `/menu/session/menu?id={session}`, `/menu/session/nutrition?id={nutrition}`.

Behaviour: dates come from `Tournament_Foodsessions` for a venue. Menus for a session load sibling sessions for the same venue and date. A menu loads `SessionFood_Items` for `SID`. Nutrition loads `Nutritions` and resolves `Allergens` JSON through `Model_Allergens`. No login check.

Provenance: **Implemented**. Who edits menus is not in this module. **Unknown / Requires Confirmation**.

### W11 — Export and scheduled jobs

`/ajax/getdata` (`AjaxController::getdataAction`) reads `UPDATE_PATH/lastupdate.txt`, exports tournaments, logs, and squads changed after that timestamp into JSON and JPEG files, then writes a new timestamp. No authentication.

`/cron/queing` loads an accreditation queue (`Model_AccreditationsApplications::getCue`). For each row it builds an A6 PDF via dompdf from `{scheme}://{host}/pdf/a6/id/{AAID}`. When `AC == 14` it also POSTs to AppWizard. See integrations.

`/cron/matches` loads open tournaments and calls `setfixturesByWinAnLose`. When `Structure == 2` it also calls pool score methods on `Model_Fixture`.

`/cron/getaccre` reads `Model_AccreditationQueing` and calls `ApplyAccreditationNow`, which is not defined in this controller, then deletes the queue row.

Provenance: **Implemented**. Schedulers (cron entries, Windows tasks) are not in the repository. **Unknown / Requires Confirmation**.

## Integrations

| System | Direction | Trigger | Data | Failure |
|---|---|---|---|---|
| MySQL `teamwizz_SARU` on `qadb.footprintapp.net` | App reads and writes | Every request that uses a model | Tournaments, people, companies, permits, blue cards, menus | Not handled beyond empty `catch` blocks and `print_r` in several actions. Production host is **Unknown** |
| `mail.sarugby.co.za` | Outbound SMTP | Company registration, permit request, company password reset | HTML body, from `accreditation@sarugby.co.za` | Exceptions swallowed |
| `Zend_Mail` default transport | Outbound | Footprint login reset | From `support@footprint.net` | `send()` return value gates the "reset" view flag |
| `https://fsrugby.appwizard.co.za/scripts/AppWiz.dll` | Outbound POST | `CronController::senddata_to_Api` when `AC == 14` | Multipart fields `G=Guest`, `A=acrSARugby`, `Barcode`, `Name`, `Surname`, `IDNo`, `Category`, `Zone` (zone numbers concatenated), `TheFile` (photo). Timeout set to 86400 seconds | Response is stored in a local variable and not checked |
| `footprintapp.net` PDF URLs | Inbound HTML fetched by this app | Match report and permit PDF actions | HTML converted to PDF | Empty HTML is echoed in the accreditation zip path; other callers do not show a dedicated error |
| Sports Concussion SA and BokSmart | Links and phone numbers in the thank-you page | After Blue Card copy is rendered | None from this app | Not applicable |
| Themeum Sportsline demo | Stylesheet and script URLs in the default layout | Every default-layout page | Static assets, some still pointing at `demo.themeum.com` | Browser failure only |

A commented curl URL `https://footprintapp.net/test/recievedata` is not active.

## Important constraints

- Passwords that this code writes are MD5, not a slow hash. Provenance: **Implemented**.
- Company registration treats the country string `SOUTH AFRICA` as South African ID type `1`. Any other country string, including different capitalisation, becomes type `2`. Provenance: **Implemented**.
- Duplicate company registration number, or duplicate person ID or email, blocks `companiesold` registration. Provenance: **Implemented**.
- Tournament `Hide == 1` is excluded from the public year list. Provenance: **Implemented**.
- Session cookie lifetime in `application.ini` is `864000` seconds (10 days). Provenance: **Implemented** as configuration.
- Default locale in the ini file is `af_ZA`. The default layout declares `lang="en-US"`. Which locale the UI actually uses is **Unknown / Requires Confirmation**.
- `public/archives` is gitignored. The public site depends on files there (JSON, images, Blue Card PDFs). A fresh clone does not contain them.

## Requirements discovered from the implementation

| ID | Requirement | Provenance | Confidence |
|---|---|---|---|
| FR-001 | The home page groups the current year's visible tournaments into featured, international, national, and provincial, and into current, completed, and upcoming. | Implemented, `IndexController::indexAction` | High |
| FR-002 | A tournament page shows the JSON profile, logs, matches, stats, results, and upcoming fixtures. | Implemented, `TournamentsController::profileAction` | High |
| FR-003 | Squads with `SquadType > 12` load players from `gir_profile`; other squads load `profile`. | Implemented | High for the branch; low for the meaning of the type codes |
| FR-004 | A visitor can submit a Blue Card consent. The system emails concussion documents and stores `BlueCard_Consents`. | Implemented | High |
| FR-005 | A company can self-register, confirm email, and is told an administrator reviews the account within 48 hours. | Email is documented; review workflow is not in this repo | Medium |
| FR-006 | A signed-in company user can add people and request a load-in or load-out permit for event `27`. | Implemented, event id hard-coded | High for the code path; low that `27` is still the intended event |
| FR-007 | Permit email states a 13:00 cutoff for next-day changes. | Documented in the email; not enforced in the action | Medium |
| FR-008 | Food sessions can be listed by venue and date, with nutrition and allergens. | Implemented | High |
| FR-009 | Changed tournaments, logs, and squads can be exported to JSON files by calling `/ajax/getdata`. | Implemented | High |
| FR-010 | Accreditation applications with `AC == 14` are POSTed to AppWizard after a PDF is built. | Implemented | High for the code; unknown whether the endpoint is still in use |

## Assumptions not promoted to requirements

- That `www.sarugby.online` still runs this code.
- That `qadb.footprintapp.net` is the production database.
- That the 48-hour review and the 13:00 permit cutoff are operational policy rather than email text only.
- That module directory names were meant to be `companies` and `company`.
- That `website/` is an older or alternate deployable copy.
