# PRD — FootPrintLive

Evidence date: 2026-10-14. Source: `git@bitbucket.org:overdrive/footprintlive.git`, `master` at `e54fddb`.

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

No formal PRD existed in the repository or in `silicon_ark/FootPrintLive/` before this file. The README is the heading `# FootprinLive` and nothing else.

## Product purpose

Footprint is the signed-in operations system for rugby unions, clubs, and national staff. From one account a person can keep member records, build squads, run tournaments and fixtures, issue accreditations, record assessments and scouting, handle blue-card cases, publish news, and plan food sessions. Match-day scoring has its own screen. People outside the back office can receive SMS and open referee or public pages.

Provenance: **Inferred** from the menu, module names, and the behaviours cited under each requirement. The repository never states this purpose in one sentence.

Who owns the product in 2026, and which hostname serves it, is **Unknown / Requires Confirmation**. The code and sample SMS name `footprintapp.net`.

## Users and roles

There is no written role catalogue. The application distinguishes people with these fields on the profile loaded at login.

| Distinction | What the code does | Provenance |
|---|---|---|
| Anonymous visitor | `default` `IndexController` sends them to `/login/` | **Implemented** |
| Signed-in user | Zend_Auth identity from view `UserLogin_view`, then profile via `Model_DbTable_Profile::getForSeason` | **Implemented** |
| `Role` JSON | Menu sections appear only when the matching key exists (`members`, `organisations`, `squad`, `fixture`, `tournament`, `accreditation`, `assessment`, `scout`, `bluecard`, `cms`, `food`, `gir`) | **Implemented** |
| `UnionType` greater than 2, and not `Administrator` | Default home redirects to `/union/` or `/first` | **Implemented** |
| `UnionType` less than 3 | Extra menu items: referee assessors and system settings | **Implemented** |
| `UnionType` greater than 3 | Dashboard renders `index/clubs` | **Implemented** |
| `UnionType` equal to 11 | Dashboard renders `index/new`. Squad code also branches on this value | **Implemented**. The label for type 11 is **Unknown** |
| `Administrator` equal to 1 | Skips the union redirect when `UnionType` is greater than 2 | **Implemented** |
| `Status` equal to 0 | Sent to `/first` to finish a profile | **Implemented** |
| `Active` equal to 0 | Sidebar shows "Inactive" and the navigation partial is not rendered | **Implemented** |
| `Active` equal to 1 | Sidebar shows "Active" and the menu is rendered | **Implemented** |
| UserID allowlist | Link "FP4 Squad" to `/union/` is rendered only for UserIDs 1, 582, 989, 1007, 1014, 4156, and 13329 | **Implemented** |
| UserID 1 | Extra "Add Tourn" item | **Implemented** |
| `tandc` | Must be 1 before `checktermsAction` will call login | **Implemented** |

The meaning of each `UnionType` integer, each `Role` action flag, and each member-type id used on the dashboard (2 players, 3 coaches, 5 medical, 8 referees) is **Inferred** from nearby labels. A lookup table was not in the repository.

## Major workflows

### Sign in

1. Visitor opens `/login/`. Layout is disabled. Terms text is read from `public/TermsText/terms.txt` and privacy text from `public/TermsText/privacy.txt`.
2. A POST to `/login/checkterms` looks up the email. If the profile exists and `tandc` is already 1, the server logs the person in. If the profile exists and `tandc` is not 1, the response is `{"OK":1}` and no session is written.
3. A POST to `/login/loginuser` logs the person in and, when the third argument path is used, sets `tandc` to 1.
4. Authentication uses `Zend_Auth_Adapter_DbTable` on `UserLogin_view`, identity column `Username`, credential column `Password`.
5. On success the storage is replaced with the seasonal profile, and `UserLogin` `LastLogin` is set to the current timestamp. The JSON result uses `OK` 2 for a completed login.
6. A later poll `/login/isloggedin` returns whether a session exists and stamps `LiveTime` and `LoggedIn` on the profile.

Provenance: **Implemented** in `application/modules/default/controllers/LoginController.php`.

### Land on the right home

After login, `/` requires a session.

- `UnionType` greater than 2 and not an administrator: `Status` 0 goes to `/first`, otherwise `/union/`.
- Any user with `Status` 0 goes to `/first`.
- `/union/` then redirects to `/union/squads/`.
- `/first` is the profile completion form. A successful POST sets `Status` to 1. If the profile is already active the action sends the person back to `/`. If status is already above 0 but `Active` is 0, union types above 2 go to the union profile URL and types below 3 go to `/members/profile/id/{UserID}`.

Provenance: **Implemented** in `IndexController::init`, `Union_IndexController`, `FirstController`.

### Work in the back office

The menu partial is included only when `Active` is 1. Each section is gated by the `Role` JSON. Opening a screen loads the matching controller under the default module (`/members`, `/squad`, `/tournament`, `/fixture`, `/accreditation`, `/assessment`, `/scout`, `/bluecard`, `/cms`, `/food`).

Provenance: **Implemented**.

### Score a match

`/live/score/scoring?fid={fixture id}` loads the fixture, sorts home and away teams by jersey, and groups commentary by half. The action disables the layout. It does not check `Zend_Auth` in `init`.

Provenance: **Implemented** in `Live_ScoreController::scoringAction`.

### Approve an accreditation and email a pass

`/cron/autoruntoo` and `/cron/schedualing` call `schedual_test`. That method reads a queue of accreditation applications. For each row it sets queue and service status to 2. When `BlockEmails` is 0 and `AC` is greater than 26, and the email address validates, it also requests `/pdf/mypass/id/{AAID}`, renders that HTML to PDF with dompdf, and emails the PDF. The from-address is `accreditation@footprintapp.net` when `NotSuru` is 1, and `accreditation@sarugby.co.za` otherwise.

`autoruntoo` then emits a script that reloads `https://footprintapp.net/cron/autoruntoo` after 10 seconds. No login check is in `CronController::init`.

Provenance: **Implemented**.

## Integrations

| System | Direction | Trigger | Data | Provenance |
|---|---|---|---|---|
| MySQL `teamwizz_SARU` | App reads and writes | Every Zend_Db_Table call | Members, fixtures, squads, accreditations, and the other tables named in the technical document | **Implemented** as config. Live responses were not observed |
| MyMobileAPI `http://www.mymobileapi.com/api5/http5.aspx` | Outbound POST | Referee, squad, fixture, assessment, accreditation, and SMS controllers include `public/class/sms.php` | Mobile number and message text. Sample in `public/SMS.txt` | **Implemented** |
| `Zend_Mail` | Outbound | Password reset, credential notice, accreditation approval | HTML body. Reset mail can include the new password in clear text | **Implemented**. SMTP host is **Unknown** |
| BokSmart MSSQL | Outbound query from a root script | Manual request to `boksmartmssql.php` | `BokSmartNumber` and `ExpiryDate` from `Officials` joined to `Courses` | **Implemented** as a script, not as a controller |
| AWS RDS MSSQL host named in `mssql.php` | Connect and disconnect | Manual request to `mssql.php` | No query | **Implemented** |
| AppWiz `https://fsrugby.appwizard.co.za/scripts/AppWiz.dll` | Outbound multipart POST | `senddata_to_Api` | Barcode, name, surname, ID number, category, zones, photo. The caller inside `schedual_test` is commented out | **Implemented** method, **not** on the live queue path |
| `footprintapp.net` | Named in cron reload, logo path, and SMS sample | Cron self-reload; user opens the link | URL only | **Implemented** as literals |
| Font Awesome CDN | Browser GET | Layout render | Stylesheet | **Implemented** |
| SparkPost PHP client under `public/SparkPost` | None found | No application class constructs `SparkPost\SparkPost` | — | Library is vendored. Use is **Unknown** |

## Constraints

- Passwords are stored and compared as MD5 hex of the password. Provenance: **Implemented** (`resetAction` writes `md5($pass)`; the auth adapter compares the credential column directly).
- Session cookie lifetime in config is 864000 seconds (10 days). A separate cookie named `FootPrintApp` is set for 86400 seconds.
- Default locale is `af_ZA`.
- `phpinfo.php` at the repository root calls `phpinfo()` with no gate.
- Login and the API login action accept one hardcoded password as an override: the server loads that user's stored password and authenticates as them. The literal is not repeated here.
- Several API and cron actions do not check `Zend_Auth`.
- SQL is often built by concatenating request parameters into `fetchRow` conditions.
- The `development` branch (2019-11-19, "Changes to VAP API's") was not inspected. Behaviour there may differ.

## Requirements discovered from the implementation

### FR-001 — Login session

The system authenticates a username and password against `UserLogin_view` and stores the seasonal profile in the Zend auth session.

- Actor: any person with a row in that view.
- Trigger: POST `/login/loginuser` or the terms-satisfied branch of POST `/login/checkterms`.
- Inputs: `email`, `password`.
- Behaviour: valid credentials produce JSON `OK` 2, write `LastLogin`, and leave a session that makes `/` render instead of redirecting to `/login/`.
- Invalid or empty credentials produce `OK` 0. Exceptions in `LoginNow` are swallowed.
- Output: JSON `{"OK": n}`.
- Provenance: **Implemented**.

### FR-002 — Terms gate

A person whose profile `tandc` is not 1 is not logged in by `/login/checkterms`. `/login/loginuser` can set `tandc` to 1 as part of a successful login.

- Inputs: same as FR-001, plus the terms and privacy files assigned to the login view.
- Validation: profile `UserID` must be greater than 0.
- Provenance: **Implemented**. The exact screen copy is the contents of `public/TermsText/terms.txt` and `privacy.txt`, which were not quoted here.

### FR-003 — Password reset

POST `/login/resetpassword` with an email that matches a profile stores an MD5 reset key and sends a link `http://{host}/login/reset/user/{key}` using `PG_Controller_Plugin_EmailLayout::EmailTemplateRequestReset`.

Opening that link with a matching `ResetKey` shows the reset form. POST stores `md5` of the new password, clears the key, and emails the new password in the message body from `support@footprintapp.net`.

An unknown email returns `OK` 0 and does not say whether the account exists.

Provenance: **Implemented**.

### FR-004 — First login profile

A signed-in profile with `Status` 0 is sent to `/first`. Submitting the form sets `Status` to 1 and `DateModified`, strips blank fields, and forces empty `ClubID` or `SocietyID` to 0. `JobType` 2 forces `JobTitle` 0.

Provenance: **Implemented**. Which fields the form marks required in the browser was not fully traced. Server-side required-field checks beyond the blank-field strip were not found in the portion read. **Unknown** whether the view enforces more.

### FR-005 — Menu by role

When `Active` is 1, the navigation lists only the sections whose keys exist on `Role`. Nested links for list/add are further gated by `Actions` keys such as `index`, `add`, `list`, and `News`.

Provenance: **Implemented** in `menu/default.phtml`. Controllers were not all checked for the same JSON test, so a person who knows the URL may still open a screen the menu hides. That gap is **Inferred** from controllers that only check `Zend_Auth` and `UnionType`.

### FR-006 — Union routing

`UnionType` greater than 2 and `Administrator` not 1 leaves the default home for `/union/` (or `/first` when status is 0). `/union/` opens squads. Blue-card `init` applies a similar redirect, and if the author's `UnionType` is below 3 it sends the user to `/union/first`.

Provenance: **Implemented**.

### FR-007 — Dashboard

The default index shows ethnicity counts for member types 2, 3, 8, and 5, fixtures for "this week", match officials on those fixtures, completed transfer requests, and newly activated accounts. `UnionType` greater than 3 uses the clubs template. `UnionType` 11 uses `index/new`.

Provenance: **Implemented** in `IndexController::indexAction`. The calendar definition of "this week" is inside `Model_Fixture::getFixturesThisWeek` and was not opened. **Unknown** whether the week starts Monday or Sunday.

### FR-008 — Members

Role `members` shows List Users (`/members/`) and Add User (`/members/add`) when those actions are granted. The profile link in the sidebar is `/members/profile/id/{UserID}` when `Status` is not 0.

Provenance: **Implemented**.

### FR-009 — Member list Excel

`MembersController::generateExcel` downloads an XLSX. Columns are #, Full Name, Organisation, E-mail, Job Title, Mobile, Gender, ID Number, Age, Ethnicity. When the title argument is 3, 5, or 8, a Boksmart column is added. When the title argument is 8, National Panel and League are added.

Provenance: **Documented** in `changelog.txt` (13/05/2019, extra column between Gender and Age) and **Implemented** (the column is ID Number). What title integers 3, 5, and 8 mean in the caller is **Inferred** to match the dashboard member types (coaches, medical, referees) and is **Requires Confirmation**.

### FR-010 — Organisations

Role `organisations` shows list and add links at `/organisations/` and `/organisations/add`.

Provenance: **Implemented** as navigation. Field-level validation inside the organisations controller was not read line by line.

### FR-011 — Squads

Role `squad` shows list (`/squad/`) and, when action `add` is set, create (`/squad/create`).

`competitionsquadAction` returns players on a squad with career totals, confirmation, document flags, school name by age group, and an injury flag. See business rules for the document types, school labels, and injury states.

Provenance: **Implemented**.

### FR-012 — Tournaments and fixtures

Role `tournament` shows Manage Tournaments. Role `fixture` shows List Fixtures. `UnionType` below 3 also shows Referee Assessors. UserID 1 shows Add Tourn.

Tournament screens in `TournamentController` cover create and edit, squads, officials, selectors, documents, media, logs, pools, admissions, and reports. `UnionType` greater than 2 is redirected out of parts of that controller.

Provenance: **Implemented**. The full field list for a tournament is **Unknown** beyond what a later task reads from `addAction` / `editAction`.

### FR-013 — Live scoring

Given a fixture id, the scoring screen shows both teams ordered by jersey and commentary grouped by half, newest time first.

Provenance: **Implemented**. Persistence of a new score event was not traced in `scoringAction` itself (that action only reads). Writes may live in another action or in the API commentary upload. **Requires Confirmation** which request saves a point.

### FR-014 — Accreditations

Role `accreditation` shows list and add. The `accreditations` module index redirects to collections. The cron path in the workflow section emails an approval PDF when the queue rules match.

Provenance: **Implemented**.

### FR-015 — Assessments

Role `assessment` always shows My Assessments for the signed-in user. List and Add appear when the matching actions are granted.

Provenance: **Implemented** as navigation.

### FR-016 — Scout

Role `scout` shows List Scouts and List Players.

Provenance: **Implemented** as navigation.

### FR-017 — Blue card

Role `bluecard` shows List Cases at `/bluecard/`. The controller requires a session and applies the union redirect.

Provenance: **Implemented**. The business meaning of a blue-card case is not defined in a comment that was read. **Unknown / Requires Confirmation** beyond "a case list the menu calls Blue Card".

### FR-018 — News CMS

Role `cms` shows List Article and Add Article when `Actions.News` and `Actions.addnews` are set.

Provenance: **Implemented** as navigation. Articles are stored through `Model_NewsArticles` on table `News_Articles_SA_Rugby`.

### FR-019 — Food sessions

Role `food` shows Food Sessions, Nutritional Items, Allergens Items, and Sessions Venues.

Provenance: **Implemented** as navigation and as models `TournamentFoodSession`, `FoodSessionItems`, `Allergens`, `Nutritions`, `MenuVenues`.

### FR-020 — Get Into Rugby

Role key `gir` is tested in the menu, and the link is inside an HTML comment, so the menu does not show it. `/gir/` still redirects a signed-in user to `/gir/squads/`.

Provenance: **Implemented**.

### FR-021 — Referee SMS

Referee module controllers include the MyMobileAPI client. A stored sample message reads: assessment name, date and time, `go to: http://footprintapp.net/referee`, and `Your OTP:` plus a code.

Provenance: **Documented** sample in `public/SMS.txt`. Sending code is **Implemented**. The rule that decides the OTP alphabet and expiry was not extracted. **Unknown**.

### FR-022 — JSON API

The `api` module returns JSON for tournament lists, squads, fixtures, scores, logs, match reports, player profiles and photos, team profiles, and accreditation applicant/event payloads. `uploadcommentaryAction` writes `{fid}comments.json` from the `comment` parameter. `Api_LoginController::loginAction` checks a username and password and returns the profile array or `{"Error":1}`.

Many of the read actions do not authenticate inside `init`.

Provenance: **Implemented**.

### FR-023 — System settings

When `UnionType` is below 3 the menu shows leagues, cities, bonafide uploads, society league structure, national panels, duplicate management, and add venue.

Provenance: **Implemented** as navigation.

### FR-024 — Distance learning

`/learning/` and `/learning/addCourse` exist as links inside a commented menu block. Models `Learning` (`courses`) and `CourseUsers` exist. The menu does not expose them.

Provenance: **Implemented** code, not shown in the active menu. Whether the URLs still work is **Inferred** from the controller `LearningController` being present. **Requires Confirmation**.

## Data the product manages

People and organisations (`profile`, `ef_users`, `ef_unions`, credentials), squads and lineups, tournaments and fixtures, scores and commentary, injuries, documents, accreditations and zones, assessments, scouting, transfers, food sessions, news, grants and EPG/e-filing tables, SMS rows, and email templates.

The table catalogue is in `technical-documentation.md`. There is no schema file in git. Column nullability and foreign keys were not verified against a database.

## Error and empty behaviour that was seen

- Login failures return `OK` 0 or an HTML warning string on the old `indexoldAction` path ("Email and/or password is invalid").
- Reset of an unknown email returns `OK` 0.
- A used or unknown reset key sets the message "Hi there! this link has expired or  already been used."
- Cron exceptions are printed.
- API login with no matching profile returns `{"Error":1}`.
- SMS helper returns a failure object when the HTTP call returns nothing, with message "SMS connection error." Timeouts are 2000 ms connect and 2000 ms total.

Provenance: **Implemented**.

## Planned behaviour

Nothing in the repository is marked as a future requirement. The `development` branch subject is not a specification. No item is labelled **Planned**.

## Out of scope for this document

- Requiring the product to keep using MD5, the hardcoded login override, or `phpinfo.php`. Those are implemented behaviours, not endorsed requirements.
- Inventing union-type names.
- Treating `track.mypass.zone` or other Footprint-named repositories as modules of this tree.
