# PRD — matchcentre.footprintapp.net

Evidence date: 2026-10-09. Source: `git@bitbucket.org:overdrive/matchcentre.footprintapp.net.git`, `master` at `3bd3534` unless a line says `origin/prod`.

Provenance labels: **Documented**, **Implemented**, **Inferred**, **Planned**, **Unknown**.

No formal PRD existed in the repository or in `silicon_ark/matchcentre.footprintapp.net/` before this file. Requirements below are behaviour found in code. They are not a new product decision.

`origin/prod` (`160e70b`, 2023-02-08) is ahead of `master`. Differences that change behaviour are marked.

## Product purpose

**Inferred.** The default layout presents "SA Rugby - Official Home of the Springboks" and a Match Centre banner (`application/layouts/default.phtml`). The `default` module lists rugby tournaments and opens logs, matches, teams, stats, line-ups, commentary, and match-report PDFs.

**Documented** purpose statement: none.

The same repository also contains a Blue Card concussion consent form (`bluecard`), a food-menu browser (`menu`), and company/permit modules (`companiesold`, `companyonl`). Whether those are part of the Match Centre product or copied from another Footprint application is **Unknown / Requires Confirmation**.

## Users and roles

No role or permission matrix is documented. Pages in the match centre do not call `Zend_Auth` in the actions that were read.

| Role | What the code shows | Provenance |
|---|---|---|
| Public visitor | Home, tournament profile, match, team, commentary, and match-report download do not check a logged-in identity | **Inferred** from the absence of an auth check in those actions |
| Footprint account | `default` `LoginController` authenticates against table `co_credetials` where `FPLogin = 1`, then loads `profile` | **Implemented**. Whether match pages require this login is **Unknown**; the actions read do not |
| Blue Card applicant | Submits the blue-card form. Roles offered: Referee, Coach, Team Manager, Physiotherapist, Medical Doctor, Parent, Guardian, Player | **Implemented** in `Bluecard_IndexController::indexAction` |
| Union contact | Blue Card submit copies union emails from `Model_Bluecard::getUnionsEmails` onto the message | **Implemented** |
| Company user | `companiesold` login, registration, permits, and users | **Implemented** as code. Whether it is a supported Match Centre role is **Unknown** |
| Operator / cron caller | `CronController` and `AjaxController::getdataAction` have no auth check in the methods read | **Implemented**. Who is allowed to call them is **Unknown** |

Repository history names Heinrich du Plessis and `heinrichoverdrive` as commit authors. That is git metadata, not an application role.

## Major workflows

### WF-1 — Browse tournaments

Trigger: `GET /` (default module index).

Inputs: optional query `year`. On `master`, the action then overwrites that value with `date("2019")`, which evaluates to the literal year 2019, and prints `yearpost: 2019` into the response (`IndexController::indexAction`).

Behaviour (**Implemented** on `master`):

1. Read `{year}.json` from the PHP working directory (not from `UPDATE_PATH`).
2. Load tournaments with `Hide != 1`, `LastUpdate` at or after the JSON timestamp (default `1977-01-01 00:00:00` when the file is missing or empty), and `StartDate` on or after `{year}-01-01`.
3. Write JPEG thumbnails and full images decoded from base64 into `public/archives/images/tournaments/`, and write one JSON file per tournament under `public/archives/tournaments/`.
4. Write a year file under `public/archives/tournaments/years/`.
5. Keep rows with `Hide == 0`. `HightedTourn == 1` goes to Featured. Otherwise `Level` 1, 2, and 3 go to international, national, and provincial. Each of those is split into Current (start ≤ today ≤ end), Completed (end < today), and Upcoming (start > today). Upcoming lists are sorted by `StartDate` ascending.
6. Attach the latest eight closed results (`Model_Fixture::fixturesResultsTest2` / `GameResults`) and up to eight upcoming fixtures (`fixturesUpcoming`).

Output: `application/modules/default/views/scripts/index/index.phtml` tabs Featured, International, National, Provincial. Each tournament links to `/tournaments/profile/tour/{TID}#logs`, `#matches`, `#teams`, and `#tournstats`.

On `origin/prod` (**Implemented on prod/qa, not on master**): the 2019 override and the debug print are commented out; empty `year` uses `date("Y")`; the SQL predicate uses an end-date window (`EndDate` between `{year}-01-01` and `{year}-12-31`, combined with `OR` as written in `IndexController`); the year bucket uses `EndDate` instead of `StartDate`.

Error behaviour: missing JSON or a database error is not given a dedicated user message in `indexAction`. `file_get_contents` on a missing file warns. `fixturesResultsTest2` can print the exception. Empty tournament set leaves `Status` as `NO` and the view receives no `Data`.

### WF-2 — Open a tournament

Trigger: `GET /tournaments/profile/tour/{id}`.

Input: route param `tour`.

Behaviour (**Implemented**, `TournamentsController::profileAction`):

1. Read `public/archives/tournaments/{id}.json`. If it decodes to a non-empty array, assign it as the tournament. There is no database fallback in this action when the file is missing or empty.
2. Refresh the log file `public/archives/Logs/{id}.json` via `logsUpdate` → `Model_Logs::getUpdates`.
3. Assign matches from `public/archives/tournaments/fixtures/{id}.json` (`matches()`). The database call `getfixtures` is overwritten by that file read.
4. Build scorer tables (`stats()`).
5. Load results through `fixturesResults` and upcoming fixtures through `fixturesUpcoming`.

Output: `tournaments/profile.phtml` with tabs LOG, MATCHES, TEAMS, STATS. A News tab is present in the markup and is not given a nav link (the link is commented out).

`profiletestAction` follows the same shape and is a test variant.

### WF-3 — Open a match

Trigger: `GET /tournaments/match/id/{fid}`.

Behaviour (**Implemented**, `matchAction`):

1. Load the fixture with `Model_Fixture::getFixtureLive`.
2. If `Hide == 1`, redirect to `/`.
3. Decode `MatchOfficials` JSON and resolve each id through `Model_Profile::getNameLive`. The view receives `UserID`, `boksmart`, and a display name.
4. Build the line-up (`lineup()`).
5. Attach recent results and upcoming fixtures for the fixture's `TID`.

On exception the action prints the exception and exits.

`matchtestAction` is the same flow using `lineuptest`.

### WF-4 — Commentary

Trigger: `GET /tournaments/commentary/id/{fid}` with optional `tab`.

Behaviour (**Implemented**): layout disabled. `Model_Fixture::commentrylist` joins `Fixture`, `ComentryAndCiting`, both squads, both `Match_Report` rows, and tournament point settings. The view receives commentary rows, score, and tab.

### WF-5 — Match report PDF

Trigger: `GET /tournaments/getmatchreport/fid/{id}` with optional `orientation`.

Behaviour (**Implemented**): layout and view renderer disabled. Fetch HTML from `https://footprintapp.net/pdf/matchreport/id/{id}`, render with DOMPDF on A4 at the requested orientation, and stream `{SquadName1} vs {SquadName2} Report.pdf`.

Failure behaviour when the remote HTML is empty or the request fails: **Unknown**. The action does not show a local error branch.

### WF-6 — Team squad

Trigger: `GET /tournaments/team/id/{sid}`.

Behaviour (**Implemented**, `teamAction` / `getmembers`):

1. Load the squad (`Model_Squad::getSquad`).
2. If `SquadType > 12`, player profiles come from `gir_profile` (`Model_GirProfile`). Otherwise they come from `profile`.
3. Players are the JSON array in `squad.Players`. Confirmed flags come from `ConfirmedPlayers`.
4. Per-player match totals (started, benched, tries, conversions, penalties, drop goals, time, yellow cards, red cards) come from `Model_MatchReport::getPlayersFixtureScore`.
5. Age is calculated from `DateOfBirth` (`age()`).
6. Results and upcoming fixtures for the squad's `TournamentID` are attached.

What `SquadType > 12` means in the product is **Inferred** as a split between profile tables. The business meaning of 12 is **Unknown / Requires Confirmation**.

### WF-7 — Refresh JSON cache

Trigger: `GET /ajax/getdata` (no login check in the action).

Behaviour (**Implemented**, `AjaxController::getdataAction`):

1. Read `public/archives/lastupdate.txt`.
2. Pull tournaments, logs, and squads whose `Updated` is after that timestamp.
3. Rewrite tournament JSON and JPEG files, log JSON, and squad JSON/JPEG files.
4. Write the current timestamp back to `lastupdate.txt`.
5. `exit`.

Memory limit for this action is 800M. On exception the action prints the exception.

### WF-8 — Login and password reset

Trigger: `GET/POST /login` (default module). Layout disabled.

Behaviour (**Implemented**, `LoginController::indexAction`):

1. If `Zend_Auth` already has an identity, redirect to `/`.
2. POST body is `data[Email]` and `data[Password]`. Both are required or the view shows "Please enter your login details".
3. Load `co_credetials` where `Username` equals the email and `FPLogin = 1`. If that row is missing, the email is replaced with `uniqid()` before authentication, so the attempt cannot match.
4. The password is MD5-hashed and checked against column `Password` through `Zend_Auth_Adapter_DbTable` on table `co_credetials` (spelled that way in code).
5. A submitted password equal to a fixed literal in this action skips the MD5 step and sends the stored hash as the credential. **Requires Confirmation** whether that literal is an accepted operational path. The literal is not copied here.
6. On success the `profile` row is stored as the identity, `LastLogin` is updated, `Status > 0` redirects to `/`, and any other status redirects to `/first`. No `FirstController` was found.
7. On failure, an existing credential row with an empty password, or a `profile.Email` match with no credential row, creates or updates `ResetKey` and emails a reset link. Otherwise the view shows "Email and/or password is invalid!".

Reset link: `http://{HTTP host}/login/reset/user/{key}`.

Mail: from `support@footprint.net`, display name `SARU E-Filing`, subject `Footprint Credentails` (spelled that way). Body says the Footprint password expired because it has not been changed in a while, and signs off as Footprint Team.

Logout (`/login/logout`) clears the identity, destroys the session, and redirects to `/`.

`forgotAction` is empty. `registerAction` loads `Model_DbTable_Countries`, and that class file is not in `application/models`.

### WF-9 — Blue Card consent

Trigger: `GET /bluecard` shows the form. `POST` fields are read in `submitAction` (the `isPost` guard is commented out, so a non-POST request still runs the action).

Inputs include applicant name, surname, email, roll, player name, player gender, union name, injury date, and a `Contact` list. Injury date defaults to today when missing or empty.

Behaviour (**Implemented**):

1. Build an email. If roll is `Player`, the player template is used. Otherwise the applicant template is used, pronoun `she` when `PlayerGender` is `Female`, otherwise `he`.
2. CC union emails for `UnionName`, extra contact emails, and the applicant when the roll is not Player.
3. Attach concussion PDFs and a spreadsheet from `public/archives/bluecard/` (filenames are listed in `submitAction`).
4. Send with `sendMail`, JSON-encode `Contact`, insert into `BlueCard_Consents`, then `exit`.

Incident causes offered on the form: Tackle, Ball Carry, Ruck, Scrum, Line-out, Open play, Collision, Unknown.

`testAction` sends the same attachment set to a hardcoded address and exits. That address is not copied here.

### WF-10 — Food menu

Trigger: `GET /menu` and `/menu/session/...`.

Behaviour (**Implemented**):

- Index loads venues with `Model_MenuVenues::getAllByID(284)`. The id is a literal.
- `dates` lists food-session dates for a venue id.
- `menus` lists sessions for that venue and date.
- `menu` lists `SessionFood_Items` for the session.
- `nutrition` disables the layout and shows a nutrition row plus allergens decoded from a JSON id list.

Who venue `284` is, and whether the menu is still used, is **Unknown / Requires Confirmation**.

## Integrations

| System | Direction | Trigger | Data | Auth | Failure | Provenance |
|---|---|---|---|---|---|---|
| MySQL `teamwizz_SARU` on `localhost` | App reads and writes | Page and cron actions | Tournaments, fixtures, squads, profiles, logs, news, blue-card rows, menu rows | Username and password in tracked `application.ini`. Not copied here | Methods often `return false` or print the exception | **Implemented** |
| `https://footprintapp.net/pdf/matchreport/id/{id}` | Outbound HTTP GET | Match report action | HTML in, PDF out | None in this call | **Unknown** | **Implemented** |
| `https://fsrugby.appwizard.co.za/scripts/AppWiz.dll` | Outbound HTTP POST | `CronController::senddata_to_Api` when queue row `AC == 14` | `G=Guest`, `A=acrSARugby`, barcode, name, surname, ID number, category, zone numbers, photo | None visible on the request | Curl result is not checked beyond `curl_exec` | **Implemented** call. Models it needs are missing from `application/models` |
| SMTP via `Zend_Mail` | Outbound mail | Login reset; Blue Card submit | HTML body, Blue Card attachments | Transport is not set in the actions read. Default PHP mail is **Inferred** | Login sets a view flag only when `send()` is true. Blue Card failure behaviour was not fully traced | **Implemented** |
| Google Analytics `UA-125030318-1` | Browser to Google | Every default-layout page | Page view | Public measurement id | Not handled in app code | **Implemented** |
| AWS CodeDeploy and S3 | CI pushes a zip and deploys | Push to `qa` or `prod` | Whole git tree | Bitbucket pipeline variables | Script prints the boto error and returns false | **Implemented** |
| SonarQube `https://scan.appmonitor.co.za` | Scanner to Sonar | Manual `sonarqube_scan.bat` | Source tree | `SONAR_TOKEN` or `SONARQUBE_TOKEN` in the environment | Batch file pauses on failure | **Implemented** as a local script, not as CI |
| Sportsline / WordPress theme assets | Served as static files | Layout links under `/wp-content` and `/wp-includes` | CSS, JS, banner images | None | Missing file is a browser 404 | **Implemented** static copies. A WordPress runtime was not found |

## Constraints

- Time zone in `public/index.php` and in tournament and news controllers: `Africa/Johannesburg`.
- `application.ini` locale default: `af_ZA`. The default layout is `lang="en-US"`. Which locale the visitor sees is **Inferred** as English markup with an Afrikaans Zend locale default. **Requires Confirmation.**
- Session cookie lifetime: `864000` seconds (10 days) in `application.ini`.
- `public/index.php` sets `display_errors` to 1. The ini sets `display_errors` to 0 for production. The front controller runs after the ini is loaded by Zend, but the `ini_set` in `index.php` happens first and Zend may override it from the ini. Effective production error display is **Unknown / Requires Confirmation**.
- A full-page maintenance message in `public/index.php` is commented out, so it does not run.
- `public/archives/` and `website/public/archives/` are gitignored. A fresh deploy has no cache until an action writes it.
- `application/configs/application.ini` is listed in `.gitignore` and is still tracked (`git ls-files` returns it). Ignore does not untrack a file that is already in git.
- Point values for tries, conversions, penalties, drop goals, penalty tries, and 22-metre tries are columns on `tournament`, not constants in PHP.
- Home and result lists are capped at 8 rows in `GameResults` and `getFixtureResults`.
- No rate limit, captcha, or CSRF token was found on login or Blue Card submit.

## Requirements discovered from the implementation

| ID | Statement | Provenance |
|---|---|---|
| FR-001 | The home page lists non-hidden tournaments in Featured, International (`Level` 1), National (`Level` 2), and Provincial (`Level` 3), and splits the level groups into Current, Upcoming, and Completed by start and end dates versus today | **Implemented** |
| FR-002 | On `master`, the home year is forced to 2019 and the year is printed on the page. On `origin/prod`, the year follows the request or the calendar year, and selection uses the tournament end date | **Implemented**, branch-specific |
| FR-003 | A tournament profile shows log, matches, teams, and stats for the id in the `tour` parameter, using JSON under `public/archives` | **Implemented** |
| FR-004 | A hidden fixture (`Hide == 1`) redirects the match page to `/` | **Implemented** |
| FR-005 | Match commentary is loaded from `ComentryAndCiting` for the fixture id | **Implemented** |
| FR-006 | A match report PDF is rendered from HTML fetched at `https://footprintapp.net/pdf/matchreport/id/{id}` | **Implemented** |
| FR-007 | Log standings include only closed fixtures. Sort is points-then-wins when `PointsSystem == 1`, otherwise wins-then-point-difference | **Implemented** |
| FR-008 | Player points on the stats tab are tries, conversions, penalties, drop goals, and 22-metre tries multiplied by that tournament's point columns | **Implemented** |
| FR-009 | Squads with `SquadType > 12` resolve players from `gir_profile`; other squads use `profile` | **Implemented**. Meaning of the threshold **Unknown** |
| FR-010 | Login requires a `co_credetials` row with `FPLogin = 1`, compares an MD5 password, stores the profile, and updates `LastLogin` | **Implemented** |
| FR-011 | A failed login for an empty stored password, or for a profile email with no credential row, emails a reset link | **Implemented** |
| FR-012 | Blue Card submit emails the applicant and union contacts with concussion attachments and inserts `BlueCard_Consents` | **Implemented** |
| FR-013 | The menu module lists food sessions, items, nutrition, and allergens for a hardcoded venue id 284 | **Implemented** |
| FR-014 | `/ajax/getdata` rewrites tournament, log, and squad JSON from rows changed since `lastupdate.txt` | **Implemented** |
| FR-015 | Pushes to `qa` and `prod` package the tree and create an AWS CodeDeploy deployment. `master` does not | **Implemented** |

Nothing is marked **Planned**. Commented-out code (maintenance page, news tab, `isPost` guard, `fixturesCompleted` query) is disabled behaviour, not a roadmap.

## Data the product manages

Application tables and views named in model SQL (database `teamwizz_SARU`):

| Name | Used for | Provenance |
|---|---|---|
| `tournament` | Competition name, dates, hide flag, level, images, points settings, structure | **Implemented** |
| `Fixture` | Match date, teams, officials, hide, closed, location | **Implemented** |
| `fixture_score` | Score row joined on commentary queries | **Implemented** |
| `squad` | Team name, images, players JSON, coaches, tournament id, squad type | **Implemented** |
| `Match_Report` | Half-time and full-time scores, scoring JSON, cards, substitutions | **Implemented** |
| `TournamentLogs` and views `TournamentLogs_view`, `Fixtures_Log_List` | Log / standings | **Implemented** |
| `ComentryAndCiting` | Commentary events | **Implemented** |
| `profile`, `gir_profile`, `co_credetials` | People and login | **Implemented** as table names in code. `Model_Credentials` file is absent |
| `News_Articles_SA_Rugby` | News list | **Implemented** |
| `ef_unions` | Unions and logos | **Implemented** |
| `stadiums` | Stadium name and capacity, joined on fixture location text | **Implemented** |
| `BlueCard_Consents` | Blue Card submissions | **Implemented** |
| `Menu_Venues`, `Tournament_Foodsessions`, `SessionFood_Items`, `Nutritions`, `Allergens` | Menu module | **Implemented** |
| `positions`, `countries`, `Ethnicity` | Reference data | **Implemented** as models |
| Accreditation and company tables (`Accreditation_Permits`, `Companies_Profile`, `Company_Members`, and others) | Company modules and cron | **Implemented** as models. Cron classes that use applications and queues are missing as files |

Views named in queries include `GameResults_view`, `GamesUpcoming_view`, `SARugbyListTourn_view`, `TournamentDetails_view`, `TopScorers_view`, `Check_Logs_Updates_view`, `FixtureUpdated`, `pool_names`. Their definitions are not in the repository. **Unknown** beyond the columns the PHP selects.

JSON caches under `public/archives` duplicate slices of this data for the public pages. The profile page prefers those files over a live tournament query.

## Out of scope for this document

- Deciding whether `master` or `prod` should be the line of development.
- Copying credentials, the login literal, or the Blue Card test recipient into this file.
- Treating `website/` as a second product without confirmation.
- Treating company, permit, and accreditation cron code as current Match Centre scope without confirmation.
