# PRD — checkin.mypass.zone (MyPass Check-in)

Evidence date: 2026-10-19. Source: `git@bitbucket.org:overdrive/checkin.mypass.zone.git`.

Default branch `master` at `09abf7a` (2022-07-01, empty message). Pipeline branch `prod` at `a78d45a` (2022-11-30, "Updated to accreditation id 479"). `ui-changes` at `be0f89bb` (2022-08-17) is noted where it changes the event id.

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

Secrets that are hardcoded (database password, SMTP password, shared PIN, a fixed identity number in a debug action) are not copied into this document.

## Product purpose

MyPass Check-in lets venue staff see accreditation applicants for one SA Rugby event and mark a ready applicant as collected.

The login page asks for a PIN. A matching PIN stores an event id in the session and opens the collection list. Provenance: **Implemented** (`LoginController::indexAction`).

The event id is branch-specific:

- `master`: `245`
- `ui-changes`: `367` (comment in the controller calls this PROD and calls `245` DEV)
- `prod`: `479` (comment in the controller calls this PROD and calls `245` DEV)

The visible event name is a separate hardcoded string in the layout, not the session id. `master` prints `Kyalami 9 Hour 2020`. `prod` prints `HSBC Cape Town 7s`. Provenance: **Implemented** (`application/layouts/default.phtml`). Whether either string still names the live event is **Unknown / Requires Confirmation**.

A second purpose is present in the same code: companies register, confirm email, maintain a profile and staff, and work with vehicle load-in or load-out permits. Emails describe that as SA Rugby accreditation and send the confirmation link to `supplier.sarugby.online`. Provenance: **Implemented** in `RegisterController` and `PermitsController`. That path is not connected to the PIN login. See FR-008.

## Users and roles

| Role | What they do | Evidence | Provenance |
|---|---|---|---|
| Collection operator | Enters the shared PIN and marks applicants collected | `LoginController::indexAction`, `IndexController` | Implemented |
| Supplier administrator | Registers a company, confirms email, would sign in with email and password | `RegisterController`, `LoginController::LoginNow` (not called by the login form) | Implemented, not reachable from the current login form |
| Company staff roles | Labels only: Administrator, Director, Management, Operations, Driver, Technician, General Staff | `UsersController::init` | Implemented as a dropdown list. No permission checks per role were found |
| Permit applicant | Submits, edits, duplicates, or cancels a permit and receives email | `PermitsController` | Implemented, but create, save, duplicate, and cancel read `Zend_Auth` identity, which the PIN login does not set |
| Anonymous visitor | Can open `/login`, `/login/forgot`, `/login/reset`, and `/register` | Those controllers do not require a session | Implemented |

The layout labels the signed-in person `Administrator` as static text. It does not read a user record. Provenance: **Implemented** (`default.phtml`).

`RegisterController` sets the new user's `Rol` to `Administrator`. Provenance: **Implemented**.

## Main workflows

### W1 — Open the collection list

Trigger: visitor opens `/` or any `IndexController` action.

Gate (`IndexController::init`): if session namespace `EVENT` has no `EID`, redirect to `/login/`.

Login (`LoginController::indexAction`):

- Layout is disabled. If `Zend_Auth` already has an identity, redirect to `/`. The PIN path never writes that identity, so this branch does not run after a successful PIN login.
- POST field `password`. The view label is "Please enter your PIN Number". The input placeholder is "Password".
- If the value equals the hardcoded string, set `EVENT.EID` to the branch's event id and redirect to `/`.
- Otherwise the page shows "Bad entered credentials".
- There is no username field, no lockout, and no captcha.

Home (`IndexController::indexAction`) renders an empty shell. The browser then GETs `/index/getapplicants` and replaces the table body.

`getapplicantsAction`:

1. Reads optional query parameter `srch`.
2. Loads rows from `Accreditation_Collections` where `AC` equals the session event id (`Model_AccreditationList::getApplications`).
3. When `srch` is non-empty, the query also matches `FirstName`, `Surname`, `FullName`, `organisation`, `IDNumber`, `Rol`, and `Barcode` with `LIKE`, and matches `Ref` with equality.
4. Groups rows in PHP by the column `ID`. The group label shown as Company is `organisation`. The collected count is how many people in the group have `ServiceStatus` `8`.
5. Returns a fragment with layout disabled.

The table columns are company, total people, collected count, and an expand control. The first company is expanded. Later companies are collapsed.

Provenance: **Implemented**.

The schema of `Accreditation_Collections` is not in this repository. Whether `ID` is a company id or a person id is **Unknown / Requires Confirmation**. If several people share an organisation name but not an `ID`, the screen shows more than one row with the same company name.

### W2 — Mark an applicant collected

Trigger: operator clicks Collect on a person whose `ServiceStatus` is `7`.

The button is rendered only for status `7`. The browser POSTs `/index/collect/id/{AASID}/status/8`.

`collectAction`:

- Requires the event session (same gate as the rest of `IndexController`).
- Does not render a view.
- Updates `Accreditation_Applicants_Services` for `AASID` equal to the `id` parameter: `Status` becomes the `status` parameter, `LastUpdate` and `Collected` become the current `Y-m-d H:i:s`.
- Does not check that the row belongs to the session event.
- Does not check that the previous status was `7`.
- Does not return a body. The browser removes the button and replaces the badge with "Collected" without reloading the company totals.

Provenance: **Implemented**.

Timezone for `date()` is `Africa/Johannesburg` (`public/index.php`).

### W3 — Search

Trigger: operator types in the search box and clicks Search, or presses Enter.

The page GETs `/index/getapplicants` with `srch` set to the box value and replaces the table body. An empty search loads every applicant for the session event.

Provenance: **Implemented** (`index/index.phtml`).

The first load initialises a DataTable on the table. The search response does not re-initialise it. Provenance: **Implemented**. Whether that leaves the plugin out of date after a search was not executed in a browser. **Unknown** as a runtime defect until someone runs it.

### W4 — Log out

Trigger: layout link `/login/logout`.

Clears `Zend_Auth` and destroys the PHP session, then redirects to `/login`.

Provenance: **Implemented**.

### W5 — Register a supplier company

Trigger: POST `/register/newcompany` with `P_data` (person) and `data` (company). GET `/register` does not require the event session. Layout is disabled.

Server order (`RegisterController::newcompanyAction`):

1. If `Companies_Profile.RegistrationNumber` already exists, JSON `OK=2` and a message that the account already exists and to contact SARU.
2. If `Company_Members` already has the ID number or the email, same `OK=2` message.
3. Otherwise insert the person: password stored as `md5`, `Rol = Administrator`, `DateCreated` today, `IDType` 1 when company country is exactly `SOUTH AFRICA`, otherwise 2, `Ref` is 3 random bytes as uppercase hex.
4. Insert the company: `RefNumber` is 6 random bytes as hex, `CreatedBy` and `AdminID` are the new member id, `CreatedDate` today, `Active = 1`.
5. Insert `Company_Events` with `CID`, `EID = 44`, `Status = 2`. This event id is not `245`, `367`, or `479`.
6. Store a validate key on the member and email a confirmation link to `http://supplier.sarugby.online/register/validateemail/id/{key}`.
7. JSON `OK=1` tells the browser that a verification email was sent. The copy says an administrator will review the company before the profile can be activated, and that activation can take up to 48 hours.

`/register/validateemail/id/{key}` on this host sets `Email_Confirmed = 1`, `AllowLogin = 1`, and clears the key when the key matches. The email link does not point at this host.

Provenance: **Implemented**. The 48-hour review is **Documented** in the email. No review queue exists in this repository.

### W6 — Read a vehicle permit

Trigger: `/permits/profile/key/{id}` after the event session exists. `PermitsController::init` checks `EVENT.EID` and does not check `Zend_Auth`.

`profileAction` loads `Accreditation_Permits` by id through `getPermit_admin`. A missing permit redirects to `/`. The action splits JSON `Accupants` into a driver (`Driver > 0`) and other occupants. `LOCKED` is `1` when `RequestDate` at 13:00 on the previous calendar day is already in the past.

Provenance: **Implemented**. The collection home page does not link to this URL. Provenance of "operators use it": **Unknown**.

Create, duplicate, cancel, add, and save on the same controller assign `identity->MID` and `identity->CID`. `init` never sets `$this->identity`. Provenance: **Implemented** code that cannot complete on the PIN session.

## Functional requirements

### FR-001 — Shared PIN opens one event

The system accepts one shared PIN and stores a single event id in the session.

| | |
|---|---|
| Who | Collection operator |
| Trigger | POST `/login` |
| Input | `password` |
| Behaviour | Equal to the hardcoded PIN: set `EVENT.EID` and redirect to `/`. Otherwise show "Bad entered credentials" |
| Output | Redirect or the login page |
| Validation | Exact string match. No length rule beyond the view `required` attribute |
| Errors | Mismatch message. No account lock |
| Dependency | PHP session. Cookie lifetime in `application.ini` is `864000` seconds |

Event id by branch: `master` `245`, `ui-changes` `367`, `prod` `479`.

Provenance: **Implemented**. The PIN value is omitted.

### FR-002 — Collection list is limited to the session event

`getApplications` adds `AC = '{session EID}'` on every load, including search.

Provenance: **Implemented**.

### FR-003 — Status badges

| `ServiceStatus` | Badge | Collect button |
|---|---|---|
| `7` | Ready To Collect | Shown |
| `8` | Collected | Hidden. Row counts toward the company collected total |
| Any other value, including missing | Pending | Hidden |

Provenance: **Implemented** (`getapplicants.phtml`). No other status names are defined in this view. Who sets status `7` is not in this repository. **Unknown / Requires Confirmation**.

On `prod` the Collect button uses CSS class `btn-primary`. On `master` it uses `btn-success`. The request is the same.

### FR-004 — Collect writes status, last update, and collected time

POST `/index/collect/id/{id}/status/{status}` updates `Accreditation_Applicants_Services` for that `AASID`.

The screen always sends status `8`. The action stores whatever `status` it receives. It does not confirm the row's event.

Provenance: **Implemented**.

### FR-005 — Search matches name, organisation, id number, role, barcode, and reference

Non-empty `srch` is applied as described in W3. `Ref` is equality. The other listed columns are `LIKE` with the text on both sides.

Provenance: **Implemented**. The value is concatenated into the SQL string. See technical documentation. This document does not repeat a query.

### FR-006 — Empty result

When no rows match, the fragment is a single empty table row.

Provenance: **Implemented**.

### FR-007 — Logout destroys the session

`/login/logout` clears auth storage and destroys the session so the next `/` returns to login.

Provenance: **Implemented**.

### FR-008 — Supplier registration is a separate login world

Registration, company profile, and staff management do not use the PIN session.

- `/company/*` and `/users/*` redirect to `/login/` unless `Zend_Auth` has an identity.
- `LoginNow` would authenticate table `CompaniesLogin` on email and password and would set `EVENT.EID` to `22`. The login form does not call it.
- After a PIN login, `Zend_Auth` is still empty, so `/company/profile` redirects back to the PIN form.

Provenance: **Implemented**. Whether `LoginNow` is used by another host is **Unknown**.

### FR-009 — Password reset

`/login/forgot` looks up `Company_Members` by email, stores an `md5` reset key, and emails a link `{host}/login/reset/user/{key}`.

`/login/reset` requires the key. POST stores `md5` of the new password, sets `AllowLogin` and `Email_Confirmed` to `1`, clears the key, and emails the new password in clear text.

Neither action is linked from the login form that was read. Provenance: **Implemented**, not linked from the PIN page.

The reset success mail interpolates `$userD`, which is not set in `resetAction`. Provenance: **Implemented** defect in that branch of the mail body. The forgot mail loads `$userD` before use.

### FR-010 — Page identity

| Surface | `master` | `prod` |
|---|---|---|
| Layout `<title>` | `SA Rugby - Accreditions Collections` | `MyPASS Checkin` |
| Login `<title>` | `SA Rugby - Load-in and Load-out` | `MyPASS Checkin` |
| Header event name | `Kyalami 9 Hour 2020` | `HSBC Cape Town 7s` |
| Footer | SA Rugby / SA RUGBY, link to `http://www.sarugby.co.za/` | `MyPass` |
| Login terms | Link to `http://www.sarugby.co.za/` | Modal filled by GET `https://footprintapp.net/api/mypass/gettermsandconditions/` |

Provenance: **Implemented**. The Accept button in the `prod` modal is `display: none`. No `accept_Terms` function is defined in that view. Recording acceptance is **not implemented** in this file.

### FR-011 — Menu entries that do not perform the labelled job

The sidebar links Dashboard to `/`. Company Profile links to `/organisation/profile`, whose action is empty and whose view is a Limitless demo profile page (Home, Statistics, Invoices, Schedule point at `#` or static demo URLs). Employees and Events items are `href="#"`.

Provenance: **Implemented** (`menu/main_menu.phtml`, `OrganisationController::profileAction`).

### FR-012 — Errors

Missing controller or action: HTTP 404 and message "Page not found". Other exceptions: HTTP 500 and "Application error". `displayExceptions` in `application.ini` is `0`. `public/index.php` and the feature controllers set `display_errors` to `1` before that.

Provenance: **Implemented** (`ErrorController`, `index.php`).

`EventsController.php` declares class `Companies_EventsController`. `OrgController.php` declares class `Company_OrgController`. Zend loads `{Controller}Controller` from those file names. Provenance: **Inferred** that `/events` and `/org` do not dispatch these classes. `EventsController` also calls `Model_AccreditationList::getEvents`, and `PermitsController::createAction` calls `getEvent`. Neither method exists on that model.

## Integrations

| System | Purpose | Direction | Trigger | Auth | Failure |
|---|---|---|---|---|---|
| MySQL `teamwizz_SARU` | Applicants, companies, members, permits | App reads and writes | Page actions | Username and password in `application.ini`. Host `localhost` on `master`, `db.footprintapp.net` on `prod` | Zend DB exception. Not given a product-level message in the collection actions |
| SMTP `mail.sarugby.co.za` port 25 | Registration and password mail from `accreditation@sarugby.co.za` | Outbound | Register, forgot, reset | Credentials in the controller. Not copied here | Exceptions are caught and discarded |
| `supplier.sarugby.online` | Email confirmation URL | Outbound link only | Registration email | None in this repo | This host's own `/register/validateemail` is a different URL |
| `footprintapp.net` terms API | Terms HTML for the login modal | Inbound JSON to the browser | `prod` login, click on the terms link | None in the page script | Not handled in the script that was read |
| `http://www.sarugby.co.za/` | Terms link on `master` | Browser navigation | Login footer | None | Not handled here |
| AWS CodeDeploy | Deploy the tree | Pipeline pushes a zip | Push to `qa` or `prod` | AWS keys from pipeline variables | `codedeploy_deploy.py` prints the client error and returns false |

No webhook, queue, or scheduled job is defined in the application.

## Constraints

- One shared PIN. No per-operator account on the collection path.
- One event id per branch, compiled into `LoginController`.
- Collection list reads `Accreditation_Collections` and writes `Accreditation_Applicants_Services`. Those objects are not created by a migration in this repo.
- Passwords that this app stores use `md5`.
- Random keys use `mcrypt_create_iv`. A PHP build without `mcrypt` cannot complete registration or reset.
- Session cookie lifetime is 10 days (`864000` seconds) unless the server overrides it.
- `public/archives/` is gitignored and is the `UPDATE_PATH` for uploaded images. `realpath` of a missing directory makes `UPDATE_PATH` empty.

## Out of scope for the collection desk

These exist in the tree and are not requirements of the collection screen unless a human says they are still in use:

- Tournament, fixture, squad, nutrition, and news models.
- WordPress directories under `public/wp-content` and `public/wp-includes`. No WordPress bootstrap is the application entry.
- The commented maintenance page at the top of `public/index.php`.

## Requires confirmation

- Live branch and live event id.
- Whether the header string must match the session event.
- Definition of `Accreditation_Collections`, including the meaning of `ID` and who sets `ServiceStatus` `7`.
- Whether registration and permits on this hostname are still operated.
- Whether search-after-DataTable behaviour is accepted.
