# Product requirements — FootprintApp.net

Evidence date: 2026-10-14.

Inspected revision: `master` at `629b199a` (2022-12-13). See `project-plan.md` for branch divergence. `origin/prod` is later and was not the tree these requirements were read from.

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

There is no requirements document in the repository. Requirements below are taken from implemented behaviour unless a label says otherwise. The one-line README `# FootprinLive` is the only **Documented** product name, and it does not match the UI title `Footprint`.

## Product purpose

**Inferred.** Footprint is a web application for rugby operations staff. A signed-in user works with organisations and people, builds squads, runs tournaments and fixtures, accredits people for events, records assessments and scouting, handles blue-card cases, publishes news, and manages food sessions. Some users also use e-filing (forms and grants) and a Team Zone workspace. Mobile and partner clients call a separate `api` module for fixtures, accreditation scans, health results, and registration.

Who owns the product, and which unions or events are in scope, is **Unknown / Requires Confirmation**. The database name `teamwizz_SARU` and BokSmart fields support a South African rugby reading. That reading is **Inferred**.

## Users and roles

The application does not ship a role catalogue. Roles are data.

| Actor | How the code treats them | Provenance |
|---|---|---|
| Anonymous visitor | `IndexController::init` redirects to `/login/` when `Zend_Auth` has no identity. `SmsController::init` exits with no body when there is no identity. | **Implemented** |
| Signed-in user | Identity is the season profile from `Model_DbTable_Profile::getForSeason`, stored in the Zend_Auth session. `Role` is JSON. The default menu shows a section only when that JSON has the matching key (`organisations`, `members`, `fixture`, `tournament`, `squad`, `accreditation`, `assessment`, `scout`, `bluecard`, `cms`, `food`, `gir`). | **Implemented** |
| User with `UnionType` > 2 and `Administrator` not 1 | If `Status` is 0, redirect to `/first`. Otherwise redirect to `/union/`. | **Implemented** in `IndexController::init` |
| User with `Status` 0 | Redirect to `/first`, including when `UnionType` is not greater than 2. | **Implemented** |
| User with `UnionType` < 3 | System Settings menu (leagues, cities, bonafide uploads, society leagues, national panels, duplicate users, add venue) and an "Online Users" count. | **Implemented** in the menu partial |
| User with `UnionType` > 3 | Dashboard renders `index/clubs` instead of the default index script. | **Implemented** |
| User with `UnionType` == 11 | Dashboard renders `index/new`. | **Implemented** |
| User ids 1, 582, 1007, 989 | InSight menu and Efiling menu, regardless of the `organisations` role check (that check is commented out). | **Implemented** |
| User ids 13329, 1, 989, 582, 4156, 1007, 1014, 2153, 13305 | "Team Zone" link to `/union/`. | **Implemented** |
| User id 1 only | "Add Tourn" popup. | **Implemented** |
| `Administrator` == 1 | Exempt from the `UnionType` > 2 redirect in `IndexController::init`. | **Implemented** |
| `Active` == 1 | Default layout prints the navigation partial only when the profile is active. | **Implemented** |
| API caller | `api` controllers expose actions without the same menu gate. Per-action authentication was not fully traced. Treat API access control as **Unknown** except where a specific action is cited. | **Unknown** |

`user_type` rows have `parent`, `TypeName`, and a `squad` flag. `Model_UserType` can exclude `TypeName` `Player` and can exclude `TypeID` 200. The meaning of each `TypeID` and `UnionType` number is **Unknown** except for the comparisons above. Ethnicity counts on the dashboard use type ids 2 (players), 3 (coaches), 8 (referees), and 5 (medical). Those mappings are **Implemented** as arguments to `Model_EthnicityCount::getCount`.

`Model_AccessControl` lists `AccessControl` rows ordered by `Title`. When the signed-in user's `UnionType` is not 1, the query adds `Private = 0`. What `Private` means to a user is **Inferred** as "hidden from non-type-1 unions".

## Major workflows

### Sign in

1. The user posts `email` and `password` to `default` `LoginController::loginuserAction`.
2. The action looks up `UserLogin_view`. A normal password must match `md5` of the submitted password (`Model_Profile::UserLogig_Check_Terms`).
3. A separate branch looks the account up by username only when the submitted password equals a fixed string in source. That string is omitted here. See BR-001.
4. `LoginNow` then authenticates with `Zend_Auth_Adapter_DbTable` against `UserLogin_view` (`Username`, `Password`) using the stored password value as the credential, with no extra credential treatment.
5. On success the response JSON is `{"OK":2}`. The profile `tandc` flag is set to 1 and `LastLogin` is updated. On failure the JSON `OK` value stays 0. Exceptions are swallowed.
6. Logout (`login/logout`) clears the identity, destroys the session, and redirects to `/`.

Password reset (`login/resetpassword`) stores an `md5(uniqid())` key on the credentials row and emails an `http://` link to `/login/reset/user/{key}`. The reset action stores `md5` of the new password.

New members created in `MembersController` receive a generated password stored as `md5`, and the plaintext password is placed in the email body.

### Home dashboard

`IndexController::indexAction` loads ethnicity counts, this week's and last week's fixtures, match officials for those fixtures, completed transfer requests, and new activation accounts. Rendering switches on `UnionType` as in the table above. The home controller itself redirects users who are not allowed to stay on `/` (see init).

### Organisations, users, squads, tournaments

The menu routes are:

- Organisations: `/organisations/`, `/organisations/add`, `/organisations/view/id/{UnionID}`
- Users: `/members/`, `/members/add`, `/members/transfers`
- Tournaments: `/tournament/` when the role has `tournament`; `/fixture/` when the role has `fixture`
- Squads: `/squad/`, `/squad/create`
- Profile: `/members/profile/id/{UserID}` when `Status` is not 0, except on host `admin.magus.events`

Creating and editing rules inside those large controllers were not line-reviewed in full. Screen presence is **Implemented**. Field-level validation beyond what `business-rules.md` cites is **Unknown**.

### Accreditation

The default menu links to `/accreditation/` and `/accreditation/addtest/` when the role allows `list` and `add`. `default` `AccreditationController` also generates permits and PDFs (`getpermitAction`, `getpermitsAction`, `getpermitpdfAction`). The `accreditations` module has application, checkout, and collections controllers. The `api` `AccreditationController` serves scanner clients (applicants, VAP, scan log, images). `api` `MypassController` posts and reads accreditation, registration, health certificates, symptom checks, events, and organisation styling.

### E-filing

Visible only for the four user ids listed above. Links:

- `/eftournaments/superset` labelled Transformation
- `/efforms/index/year/{current year}` and `/efbuilder/`
- `/efplan/`, `/efplan/manage`, `/efcategories/`, `/efclassifications/`

Jotform menu entries are commented out. `EfjotformController` and `EfbuilderController::jotcronAction` still exist.

Superset screens (`supersetrugby`, `supersetpivot`, `supersetcustom`, `supersettourn`, `supersetorg`) are under the InSight menu for the same four user ids. How Superset is embedded (iframe, API, or local HTML) was not fully traced. **Unknown** beyond the routes.

### Team Zone, live scoring, referee, public

Team Zone is the `union` module, linked for specific user ids and also the redirect target for many union users. It has its own squad, fixture, tournament, member, permit, and blacklist controllers.

`live` exposes fixture scoring (`Live_ScoreController::scoringAction`) with home and away teams ordered by jersey number, plus commentary. The action disables the layout. No auth check was seen in `init`.

`pub` and `referee` modules expose calendar and referee controllers. `printers` is a thin module with an index controller.

### Scheduled work

`CronController` actions include `autoruntoo`, `schedualing`, `queingold`, `matches`, `logs`, `getfixtues`, `bluecardconsentscleanup`, and `participationgenerate`. What invokes them (cron, HTTP, both) is **Unknown**. `public/cron.txt` exists; its schedule was not treated as an active crontab because a text file in `public/` is not evidence of an installed scheduler.

## Functional requirements

### FR-001 — Sign-in

Requirement: A user submits email and password and receives JSON `OK` of 2 only when `UserLogin_view` contains that username and the stored password equals `md5` of the submitted password, and the following auth-adapter check also succeeds.

Who: anonymous user. Trigger: POST `login/loginuser`. Inputs: `email`, `password`. Output: JSON `{"OK": 0|2}`.

Validation: the lookup concatenates the username into SQL. There is no visible rate limit or captcha.

Error: any exception in `LoginNow` returns 0. A missing profile after a valid adapter result also returns 0.

Evidence: `LoginController::loginuserAction`, `LoginNow`, `getAuthAdapter`, `Model_Profile::UserLogig_Check_Terms`.

Implementation: **Implemented**.

Confidence: High for the success path. The bypass in BR-001 is a second path.

### FR-002 — Session and logout

Requirement: A successful login writes the season profile into Zend_Auth. Logout clears it and sends the browser to `/`.

The ini session cookie lifetime is 864000 seconds. `IndexController` also sets a cookie named `FootPrintApp` for 86400 seconds. Default timezone in `public/index.php` is `Africa/Johannesburg`. Default locale is `af_ZA`.

Evidence: `LoginNow`, `logoutAction`, `application.ini`, `public/index.php`.

Implementation: **Implemented**.

Confidence: High.

### FR-003 — Password reset and new-user password

Requirement: Reset email contains an `http` link with a reset key. Saving a reset password stores `md5` of the new password and clears the key. Creating a member stores `md5` of a generated password and emails the plaintext password.

Evidence: `LoginController::resetpasswordAction` and `resetAction`; `MembersController` inserts into credentials and builds an email body that includes `Password:`.

Implementation: **Implemented**.

Confidence: High.

### FR-004 — Landing redirects

Requirement: After login, the home controller sends the user to `/login/` (no session), `/first` (`Status` 0, with the union-type conditions in `IndexController::init`), or `/union/` (`UnionType` > 2, not an administrator, `Status` not 0).

Evidence: `IndexController::init`.

Implementation: **Implemented**.

Confidence: High. The `/first` screen's required fields are **Unknown** (controller exists: `FirstController`).

### FR-005 — Menu follows the Role JSON

Requirement: Navigation sections render only when the profile `Role` JSON contains the section key, except the hardcoded user-id menus (InSight, Efiling, Team Zone, Add Tourn) and System Settings (`UnionType` < 3).

Evidence: `application/modules/default/views/scripts/menu/default.phtml`.

Implementation: **Implemented** for display. Whether each controller checks the same JSON before acting is **Unknown** — the menu is not proof of enforcement. A December 2022 commit says access rights displayed on other user profiles were fixed (`df32796a`); the enforcement gap remains unconfirmed.

Confidence: High for the menu. Medium for server-side checks.

### FR-006 — Dashboard contents

Requirement: The default dashboard shows ethnicity counts for players, coaches, referees, and medical staff; this week and last week's fixtures; match officials on those fixtures; completed transfer requests; and new activations. `UnionType` > 3 uses the clubs view. `UnionType` 11 uses the new view.

Evidence: `IndexController::indexAction`.

Implementation: **Implemented**.

Confidence: High.

### FR-007 — Organisations and users

Requirement: Users with the `organisations` role can open the organisation list, add form, and their own organisation profile. Users with the `members` role can list users, add a user, and open transfers when `add` is set.

Evidence: menu partial. Controllers: `OrganisationsController`, `MembersController`.

Implementation: **Implemented** as routes and screens. Detailed field rules: see business rules that were actually read; the rest is **Unknown**.

Confidence: High for navigation. Medium for complete behaviour.

### FR-008 — Tournaments, fixtures, squads

Requirement: Tournament management is `/tournament/` when role key `tournament` is set. Fixture list is `/fixture/` when role key `fixture` is set. Squad list and create are `/squad/` and `/squad/create` when the squad role actions allow them.

Squad spreadsheets include a BokSmart column when player data has `boksmart`. FP3 squad code on this revision sets a default position and marks the player unassigned when no position is set (commit `cbe06447`, message only; the line was not re-read).

Evidence: menu; `union` and `default` squad controllers; commit `cbe06447`.

Implementation: **Implemented**.

Confidence: High for routes. Medium for BokSmart display rules.

### FR-009 — Accreditation and permits

Requirement: Role `accreditation` shows list and add. Permit HTML/PDF actions exist on `PdfController` and `AccreditationController`. Scanner APIs exist for applicants, Android variants, VAP, scan logs, and images.

Evidence: menu; `api/AccreditationController`; `PdfController::getpermitAction`; `AccreditationController::getpermitpdfAction`.

Implementation: **Implemented**.

Confidence: High that the endpoints exist. Business approval rules inside the 16,000-line accreditation controller were not fully extracted.

### FR-010 — MyPass and health APIs

Requirement: `api/MypassController` exposes login, password reset, profile update, accreditation post, registration post, events, organisation, styling, terms, health certificate, symptom check, and screening results. `api/AmpathController::pushresultsAction` is commented as results pushed from Ampath (POST).

Evidence: action names and the Ampath method comment.

Implementation: **Implemented** as actions. The contract (auth header, payload schema, idempotency) is **Unknown** beyond those names.

Confidence: Medium.

### FR-011 — E-filing and InSight

Requirement: Four hardcoded user ids see InSight (several Superset routes) and Efiling (transformation Superset, forms, form templates, projects, grant categories, grant classifications).

Evidence: menu partial; `EftournamentsController`, `EfformsController`, `EfbuilderController`, `EfplanController`, `EfgrantsController`, `EfcategoriesController`, `EfclassificationsController`.

Implementation: **Implemented** as screens for those ids.

Confidence: High for visibility. Low for grant approval rules (not line-reviewed).

### FR-012 — Assessments, scout, blue card, CMS, food

Requirement: Each appears in the menu when its role key is set.

- Assessments: my assessments, list, add.
- Scout: list scouts, list players.
- Blue card: `/bluecard/`.
- CMS: list article, add article.
- Food: sessions, nutritional items, allergens, session venues.

Evidence: menu partial and matching controllers (`AssessmentController`, `ScoutController`, `BluecardController`, `CmsController`, `FoodController`).

Implementation: **Implemented**.

Confidence: High for entry points.

### FR-013 — Live scoring

Requirement: `live/score/scoring` loads a fixture by `fid`, splits home and away JSON lineups, sorts by jersey, and loads commentary.

Evidence: `Live_ScoreController::scoringAction`.

Implementation: **Implemented**. Auth on this action was not seen.

Confidence: High for the load. Auth is **Unknown**.

### FR-014 — Email and SMS

Requirement: Application mail uses Zend_Mail over SMTP to `smtp.gmail.com` port 587, from `support@footprintapp.net` with display name Footprint. Accreditation mail uses from `accreditation@magus.events` with display name Magus.Events. Tournament SMS actions call `MyMobileAPI::sendSms` and store a row through `Model_Sms`.

Evidence: `library/PG/Controller/Plugin/EmailLayout.php`; `SmsController`; `public/class/sms.php` (`http://www.mymobileapi.com/api5/http5.aspx`).

Implementation: **Implemented**.

Confidence: High. Credentials omitted.

### FR-015 — Terms, privacy, cookies

Requirement: Controllers exist for terms (`TermsconditionsController::indexAction`), privacy (`PrivacypolicyController::indexAction`), and cookies (`CookiesController`). Login can set `tandc` to 1 on a successful `loginuser` call that passes `terms=1` into `LoginNow`. `checktermsAction` is a separate POST that performs the same password lookup.

Whether the user must accept terms before the session is useful is **Unknown** — `loginuserAction` always calls `LoginNow` with `terms=1`, which writes `tandc` on success.

Evidence: those controllers and `LoginNow`.

Implementation: **Implemented** as pages and a flag write. The legal text was not reviewed.

Confidence: Medium.

### FR-016 — Deploy

Requirement: Pushes to `qa` and `prod` zip the working tree and run `codedeploy_deploy.py`, which uploads to S3 and creates a CodeDeploy deployment. The app is installed to `/mnt/data/www/html/footprintapp.net/footprintapp.net/` as `apache`.

Evidence: `bitbucket-pipelines.yml`, `appspec.yml`, `codedeploy_deploy.py`.

Implementation: **Implemented** as pipeline definition. Whether the AWS applications still exist is **Unknown**.

Confidence: High for the script. Unknown for the live account.

## Integrations

| System | Direction | Trigger | Data | Auth as seen | Failure |
|---|---|---|---|---|---|
| MySQL `teamwizz_SARU` | App to database | Every request that uses the default adapter | Profiles, fixtures, accreditation, and the other models | Username and password in `application.ini` (omitted here) | Zend exceptions; many are caught and return false or empty |
| MySQL `teamwizz_API` | Config only | Keys `apilogs.db.params` | Named API log database | Same host, credentials omitted | **Unknown** whether any class reads `apilogs` |
| Gmail SMTP | App to Google | `EmailLayout::sendMail` | HTML mail | SMTP login in source (omitted) | `resetpasswordAction` catches a misspelled `exeption` and still may return `Send` 0 |
| Magus accreditation mail | App to SMTP | Second send method in `EmailLayout` | HTML mail | Separate from-address | **Unknown** |
| MyMobileAPI | App to HTTP | `SmsController` tournament actions | Mobile number and message | Credentials inside `public/class/sms.php` (not copied) | Not traced |
| Ampath | Partner to app | POST `api/ampath/pushresults` | Results (comment in source) | **Unknown** | **Unknown** |
| MyPass clients | Client to app | `api/mypass/*` | Accreditation, health, registration, events | **Unknown** | **Unknown** |
| BokSmart MSSQL | Standalone script | Manual run of `boksmartmssql.php` | Officials joined to Courses for a BokSmart number and expiry | Hardcoded in the script (omitted) | Prints a failure string and stops |
| MSSQL RDS | Standalone script | Manual run of `mssql.php` | Connectivity only | Hardcoded (omitted) | Prints a failure string |
| AWS CodeDeploy | Pipeline to AWS | Branch `qa` or `prod` | Zip of the tree | Env vars in Bitbucket | Script prints the boto error and returns false |
| track.mypass.zone | Other app to this host | Documented in that project's Silicon Ark | Terms JSON, permit HTML, email send | Not re-tested | See that project's docs |

A commented bootstrap in `application/Bootstrap.php` names an older MySQL database `evolveci_sarugry` on host `footprintapp.net`. It is not active. Provenance: **Implemented** as a comment, not as runtime.

## Constraints

- Zend Framework 1.11.10 and PHP of that era (pipelines do not install PHP; the host must already have it).
- Timezone `Africa/Johannesburg`. Locale `af_ZA`.
- Front controller does not display exceptions in production, staging, testing, or development. All four ini sections keep `display_errors` at 0. `development` still inherits production database settings.
- Root `.htaccess` rewrites every request that is not already under `/public/` into `public/`. HTTPS and www redirects in that file are commented out.
- `public/.htaccess` sends non-file requests to `public/index.php`.
- Passwords are `md5`, not a modern password hash.
- Queries that were read build SQL by concatenating request values.

## Non-requirements (not in this revision)

- No Composer-managed dependencies (`composer.json` is `{}`).
- No PHPUnit suite.
- No Docker or infrastructure-as-code beyond CodeDeploy hooks.
- No pipeline on `master` or `dev`.
- GIR and distance-learning menu items are commented out. The `gir` module and `LearningController` remain in the tree. Whether they are still offered is **Unknown**.
- `union_old` is retained code, not a second product menu.

## Open questions

See the confirmation list in `project-plan.md`. In addition:

- The numeric meaning of every `UnionType` and `user_type.TypeID`.
- Whether API actions require a session, a key, or nothing.
- Whether `apilogs` is used.
- Which cron URL is actually scheduled in production.
