# Product requirements — media.accreditations.sarugby.online

Evidence date: 2026-10-09. Source: `git@bitbucket.org:overdrive/media.accreditations.sarugby.online.git`, `master` at `773e00f` (2020-01-10).

There is no product requirements document in the source repository. The requirements below are recovered from the implementation. Each one is labelled:

- **Implemented Behaviour** — the code does this.
- **Inferred Behaviour** — a reasonable reading of the code, not an explicit requirement.
- **Documented** — stated in a template, comment, or commit, not only in logic.
- **Unknown / Requires Confirmation** — not established.

Provenance of a requirement is **Implemented Behaviour** unless the entry says otherwise.

## Product purpose

A desk user who knows the shared PIN opens a list of media accreditations for one fixed SA Rugby event and marks people as collected. The header names the event "HSBC Cape Town Sevens - Media".

The repository also contains supplier registration and vehicle-permit screens from a larger accreditation application. Those screens are not the path the login form opens.

## Users

| User | Evidence | Confidence |
|---|---|---|
| Collection-desk operator | PIN form, collections table, Collect button | High for the implemented path |
| Supplier company administrator | Register, company profile, staff, permits | Present in code; whether this host is used for that role is **Unknown / Requires Confirmation** |
| SA Rugby accreditation mailbox | From-address and enquiry address `accreditation@sarugby.co.za` | Implemented in mail bodies |

No role names are stored for the PIN session. The PIN does not create a `Zend_Auth` identity.

## FR-001 — PIN gate

**Requirement:** A visitor who is not already authenticated with `Zend_Auth` posts a password to `/login`. If the password matches the shared PIN hardcoded in `LoginController::indexAction`, the application stores event id `63` in the `EVENT` session and redirects to `/`. Any other password shows "Bad entered credentials". The form label is "Please enter your PIN Number".

**Who:** Anyone who can open the host.

**Trigger:** GET or POST `/login`.

**Inputs:** POST field `password`.

**Outputs:** Redirect to `/` on success. HTML message on failure.

**Validation:** Exact string match. No lockout, expiry, or second factor.

**Error conditions:** Wrong PIN stays on the login page. The failure message is assigned to `Dispaly_Message`. The login template that was inspected does not print that variable, so the failure text may not be visible. Provenance: **Inferred Behaviour**.

**Dependencies:** PHP session. Cookie lifetime in `application.ini` is `864000` seconds (10 days).

**Notes:** If `Zend_Auth` already has an identity, `indexAction` redirects to `/` without checking the PIN. The PIN path itself never writes a `Zend_Auth` identity, so that branch is only reached after a different login (`LoginNow`) that the login form does not call.

The PIN value, the SMTP password, and the database password are hardcoded. They are not repeated in this document. **Unknown / Requires Confirmation** whether they are still valid and whether they should be rotated. They are in Git history.

## FR-002 — Session required for the dashboard

**Requirement:** `IndexController::init` redirects to `/login/` when the `EVENT` session has no `EID`. When `EID` is set, the view receives that id and `currentpage` = `Home_link`.

**Who:** Collection-desk operator after FR-001.

**Trigger:** Any `IndexController` action.

## FR-003 — Accreditations by company

**Requirement:** The home page title in the card is "Accreditations By Company". On load, the browser GETs `/index/getapplicants` and replaces the table body with the returned HTML, then initialises a DataTable. A search box GETs the same URL with `srch`.

**Who:** Collection-desk operator.

**Trigger:** Document ready, Search button, or Enter in the search box.

**Inputs:** Optional `srch`.

**Expected behaviour:** `IndexController::getapplicantsAction` reads `Model_AccreditationList::getApplications` for the session event id. Rows come from `Accreditation_Collections` where `AC` equals that event id, ordered by organisation, first name, surname. Results are grouped by `ID`. Each group shows the organisation name, the number of people, and how many of those people have `ServiceStatus` `8`.

**Outputs:** HTML table rows. The first company is expanded. Later companies are collapsed until the chevron is clicked.

**Empty state:** A single row with an empty cell.

**Search:** When `srch` is non-empty, the query adds a LIKE/equality filter on `FirstName`, `Surname`, `FullName`, `Ref`, `organisation`, `IDNumber`, `Rol`, and `Barcode`. The search text is concatenated into SQL. Provenance: **Implemented Behaviour**. This is a defect risk, not an approved business rule.

Person columns: status badge, `Ref`, `FullName`, `Rol`, `ServicesText`, and an action cell.

## FR-004 — Collection status display

**Requirement:** For each person:

| `ServiceStatus` | Badge | Action |
|---|---|---|
| `7` | Ready To Collect | Collect button |
| `8` | Collected | No button |
| Any other value, including missing | Pending | No button |

**Who:** Collection-desk operator.

**Trigger:** Render of `/index/getapplicants`.

**Unknown / Requires Confirmation:** The meaning of statuses other than `7` and `8`, and which other system sets status `7`.

## FR-005 — Mark collected

**Requirement:** The Collect button POSTs `/index/collect/id/{AASID}/status/8`. `IndexController::collectAction` updates `Accreditation_Applicants_Services` for that `AASID`, setting `Status` to the posted status, `LastUpdate` to the current timestamp, and `Collected` to the current timestamp. The action disables the layout and exits. The browser removes the button and replaces the badge with Collected. It does not reload the company collected count.

**Who:** Collection-desk operator.

**Trigger:** Click Collect. There is no confirmation dialog.

**Inputs:** `id` (AASID), `status` (the button always sends `8`).

**Validation:** The action requires the `EVENT` session (FR-002). It does not check that the current status is `7`, that the row belongs to event `63`, or that `status` is `8`. The id is concatenated into the SQL predicate.

**Error conditions:** No JSON error body. Database failure behaviour is **Unknown / Requires Confirmation**. The button text changes to "Busy ..." and is removed when the HTTP call returns, including when the response is an error page. Provenance of the last sentence: **Inferred Behaviour** from the jQuery success-only callback (jQuery's default `$.post` does not run the callback on HTTP error).

**Outputs:** No response body intended for the user. Database row updated.

## FR-006 — Logout

**Requirement:** `/login/logout` clears `Zend_Auth` and destroys the PHP session, then redirects to `/login`. The default layout links Logout to that URL.

**Who:** Anyone with the layout.

**Note:** Destroying the session also drops the `EVENT` namespace, so the PIN gate is required again.

## FR-007 — Password reset for company members

**Requirement:** `/login/forgot` accepts an email, looks up `Company_Members` (`Model_UserProfile`) by `Email`, stores an `md5` reset key, and emails a link `/login/reset/user/{key}` from `accreditation@sarugby.co.za` via SMTP `mail.sarugby.co.za`. The sign-off is "SA Rugby Event Administration". `/login/reset` loads the member by `ResetKey`. A posted password is stored as `md5`, `ResetKey` is cleared, and `AllowLogin` and `Email_Confirmed` are set to `1`. A confirmation email includes the new password in clear text.

**Who:** A person whose email is on `Company_Members`. The PIN form does not link to forgot-password. The templates `login/forgot.phtml` and `login/reset.phtml` exist.

**Error conditions:** Unknown email does not set a sent flag. Missing `user` on reset redirects to `/login/`. A reset key that does not match a `MID` sets view `SENT` to `2`. Mail failures are swallowed.

**Note:** `LoginNow` authenticates `CompaniesLogin.Email` / `CompaniesLogin.password` with `Zend_Auth` and would also set event id `63`, but nothing in `indexAction` calls `LoginNow`.

## FR-008 — Company registration

**Requirement:** `RegisterController::newcompanyAction` accepts posted company and person fields. If the registration number is new and the ID number and email are new, it inserts a `Company_Members` row with role `Administrator`, `md5` password, and a generated `Ref`, then a `Companies_Profile` row, then a `CompanyEvents` row with `EID` `44` and `Status` `2`. South Africa sets `IDType` `1`; any other country sets `IDType` `2`. A validation key is emailed. `validateemailAction` confirms the address.

**Who:** Public registrant, if the route is used.

**Conflict:** The collection desk uses event `63`. Registration attaches the company to event `44`. **Unknown / Requires Confirmation** which event id is correct for this host.

`RegisterController::indexAction` loads B-BBEE levels, accreditation company types, and countries for the registration form. It does not require the PIN session.

## FR-009 — Company profile and staff

**Requirement:** `CompanyController` and `UsersController` require a `Zend_Auth` identity and redirect to `/login/` when it is missing. Profile save posts company fields and returns JSON `OK` `0` or `1`. Staff add and profile update insert or update `Company_Members` for the identity's `CID`, optionally write a JPEG under `UPDATE_PATH/accreditation/`, and return the same JSON shape. User types offered to the view are Administrator, Director, Management, Operations, Driver, Technician, General Staff. `AllowLogin` generates an `md5` password from `mcrypt` random bytes when login is newly allowed.

**Who:** A company user who has a `Zend_Auth` identity.

**Constraint:** The PIN login does not create that identity, so these actions redirect back to the PIN form. Provenance: **Implemented Behaviour**. Whether another host or an uncommitted login was meant to open them is **Unknown / Requires Confirmation**.

Image upload for a company picture also exists on `LoginController::uploadremotecompanyAction` and `companypicAction`, keyed by a request `cid`, without the `Zend_Auth` check.

## FR-010 — Supplier permits and FAQ

**Requirement:** `PermitsController` requires the `EVENT` session (the PIN session satisfies this) and does not require `Zend_Auth`.

| Action | Behaviour |
|---|---|
| `questionsAction` | Renders a supplier FAQ (vehicle access pass, health and safety file, delivery versus work permit). Reachable after the PIN login. |
| `createAction` | Calls `Model_AccreditationList::getEvent`, which is not defined on that model. Opening this action fatals. |
| `addpermitAction`, `savepermitAction`, `duplicatepermitAction`, `cancelAction`, `profileAction`, `profiletestAction` | Read `$this->identity->CID` or `MID`. `init` never sets `identity`. These actions error if reached. New and duplicated permits would set `Status` `2` and `EID` from the session. Cancel sets `Status` `5`. |
| `emailtestAction` | Sends a test message through the accreditation SMTP account to a hardcoded personal mailbox. The address is not recorded here. |
| `postEmailToFp` | Builds a POST to `https://footprintapp.net/email/send`. The call site in `addpermitAction` is commented out. |

**Inferred Behaviour:** Permit screens were copied from the supplier portal and were not finished for the media collections host.

The home page contains a cancel-permit modal that calls `cancelPermitNow`, which is not defined in `index/index.phtml`. Provenance: **Implemented** leftover markup.

## FR-011 — Layout and branding

**Requirement:** Authenticated-session pages (any controller that does not disable the layout) use `application/layouts/default.phtml` because `PG_Controller_Plugin_LayoutPicker` sets the layout name to the module name `default`. The header text is fixed: "HSBC Cape Town Sevens - Media". The document title is "SA Rugby - Accreditions Collections". The footer is "SA Rugby". Logout links to `/login/logout`. Login uses no layout and a green background (`#004625`).

`application.ini` also names layouts `admin`, `union`, `referee`, `gir`, and `accreditations`. Those layout files are not in the repository. `application/layouts/layout.phtml` titles itself "Footprint - SA Rugby Tournament Management Portal" and is not selected by the plugin.

Locale `af_ZA` is configured. No translation catalogues were found. UI copy that was read is English.

## FR-012 — Deployment

**Requirement:** Pushes to Bitbucket branches `qa` and `prod` zip the tree and run `codedeploy_deploy.py`, which uploads the zip to S3 and creates a CodeDeploy deployment. `appspec.yml` copies the revision to `/mnt/data/www/html/sarugby.online/media.accreditations.sarugby.online/` as `apache:apache`, installs `httpd` before install, removes old CodeDeploy revisions on application stop, and starts `httpd` on application start.

**Documented** in those files. The cloned remote has only `master`, so this pipeline does not run for the branch that exists. **Unknown / Requires Confirmation** whether `qa` and `prod` are created at deploy time outside this clone.

On 2020-01-10 the database host changed from `qadb.footprintapp.net` to `db.footprintapp.net`. **Inferred Behaviour:** that commit pointed this host at the production database. Whether the host is still deployed is **Unknown / Requires Confirmation**.

## Constraints

- Timezone is `Africa/Johannesburg` (`public/index.php`).
- `APPLICATION_ENV` defaults to `production`. Staging, testing, and development sections inherit production. Display errors are off in `application.ini` and forced on in `public/index.php` and in controllers.
- `UPDATE_PATH` is `public/archives`.
- Passwords for company members are `md5`, not a slow hash.
- One database, one module (`default`).

## Out of scope for the implemented collection path

Approving accreditations, printing passes, taking payment, and choosing an event in the UI are not implemented on the dashboard. Event id `63` is fixed in the PIN success branch.
