# PRD — track.mypass.zone (MyPass Track)

Evidence date: 2026-10-01. Source: `git@bitbucket.org:overdrive/track.mypass.zone.git`, `master` at `ec4cf5d` (2022-08-25).

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

Secrets that are hardcoded (database password, SMTP password, shared PIN, a sample identity number, a personal mailbox in a test action) are not copied into this document.

## Product purpose

MyPass Track shows vehicle access permits for one SA Rugby event so venue operations can see who is scheduled to load in or load out on a chosen date.

The login page asks for a PIN. A matching PIN stores event id `367` in the session and opens the dashboard. Provenance: **Implemented** (`LoginController::indexAction`, commit `b8652a3`).

A second purpose is present in the same code: companies register, confirm email, maintain a profile and staff, and submit load-in or load-out permit requests. The emails and FAQ describe that as SA Rugby / MyPASS accreditation for suppliers. Provenance: **Implemented** in `RegisterController` and `PermitsController`, and **Documented** in the email bodies and `Questions()`. That path is not connected to the PIN login. See FR-001.

## Users and roles

| Role | What they do | Evidence | Provenance |
|---|---|---|---|
| Operations viewer | Enters the shared PIN and reads the event permit dashboard | `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 the actions read `Zend_Auth` identity, which the PIN login does not set |
| Event administrator | Approves, declines, or changes a request; modifies date and time to balance load | Email body in `addpermitAction` | Documented in email copy. No approve or decline action exists in this repository |
| Health and safety officer | Approves a health and safety file at the venue | FAQ answers | Documented. Not implemented in this repository |
| Anonymous visitor | Can open `/login` and `/register` | Controllers disable layout and do not require a session, except where noted | Implemented |

`RegisterController` sets the new user's `Rol` to `Administrator`. The FAQ says only one administrator is allowed. The users screen can still add further people. Provenance: **Documented** versus **Implemented** conflict. See business rules.

## Main workflows

### W1 — Open the tracking dashboard

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

Gate (`IndexController::init`, `PermitsController::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".
- If the value equals the hardcoded string, set `EVENT.EID = 367` and redirect to `/`.
- Otherwise the page shows "Bad entered credentials".
- There is no username field, no lockout, and no captcha.

Dashboard (`IndexController::indexAction`), for the session event:

1. Load accreditation row `Accreditation_List` where `AC` is the event id, plus gates (`EventGates.AC`) and loading zones (`Accreditation_Loading_Zones.AID`).
2. Build the date list from JSON fields `LoadinActiveDate` and `LoadOutActiveDate`. Stored dates are `d/m/Y` strings. The first date becomes the default request date. If today's date is in the list, today is selected instead.
3. Load permits from view `Permits_view` where `EID` matches, `LoadType` is `1` (load-in), and `RequestDate` is that default. Order: request date, time, trading name.
4. The view counts permits and renders lists grouped in the template (checked-in, checked-out, and the remaining rows with status badges). Zone cards estimate weight as vehicle weight plus trailer weight against parking-bay capacity times max weight.

Changing the date or load type posts to `/index/getdata` with `type` and `date` and replaces the dashboard fragment (`index/getdata.phtml`). Layout is disabled for that action. Non-POST requests render an empty fragment.

Provenance: **Implemented**.

### W2 — Read a permit

Trigger: dashboard link `/permits/profile/key/{PID}/...`.

`PermitsController::profileAction` loads `Permits_view` by `PID` only (`getPermit_admin`). It does not filter by company. Missing permit redirects to `/`.

The action splits JSON `Accupants` into a driver (`Driver > 0`) and other occupants, and builds load-in and load-out date lists from the permit row.

`LOCKED` is `1` when `RequestDate` at 13:00 on the previous calendar day is already in the past. Timezone is `Africa/Johannesburg` (`public/index.php`).

Provenance: **Implemented**. The dashboard row uses a different lock time (10:30 the previous day) when status is below 7. The confirmation email says changes are accepted until 1pm. See BR-008.

### W3 — Register a supplier company

Trigger: POST `/register/newcompany` with `P_data` (person) and `data` (company). The register page itself is GET `/register` and 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 was not updated when the PIN session moved to 367.
6. Store `Validate_Key` (8 random bytes as hex) and `CID` on the member.
7. Email the person a link `http://supplier.sarugby.online/register/validateemail/id/{key}`. The message says an administrator will review the company and activation can take up to 48 hours.
8. JSON `OK=1` thanks the user and asks them to open the email. Exceptions return `OK=0`.

Email confirmation (`validateemailAction`): if the key matches, clear the key and set `Email_Confirmed = 1`, `Email_Confirmed_Date` today, `AllowLogin = 1`. The page then says the account is active and links to `/login`. A missing or unknown key shows an error, or redirects to `/login/` when the id parameter is absent.

Provenance: **Implemented**. The 48-hour review is **Documented** in the email and is not enforced: `Active` is already 1 at insert, and `AllowLogin` flips on email confirm with no administrator step in this repository.

`IndexController::newcompanyAction` is a second registration implementation. Its validation link points at `http://www.sarugby.online/company/index/validateemail/id/{member id}` and `validateemailAction` on `IndexController` is empty. `check_Company` calls `Create_Company` with a variable that is not in scope. Provenance: **Implemented** dead path. **Inferred** defect.

### W4 — Reset a password

`/login/forgot` posts `Email`. If `Company_Members` has that email, store `ResetKey` as `md5` of 4 random bytes and email `http://{SERVER_NAME}/login/reset/user/{key}`.

`/login/reset` requires `user`. Unknown key shows `SENT=2`. A posted password is stored as `md5`, `ResetKey` is cleared, `AllowLogin` and `Email_Confirmed` are set to 1, and a second email repeats the new password in clear text.

Provenance: **Implemented**. The reset page uses `$userD` in the email greeting before that variable is set. **Inferred** defect: the success email greeting can fail or be blank while the rest of the message still includes the password.

The PIN login does not use `Company_Members.password`. A reset password does not unlock the dashboard.

### W5 — Submit a permit request

Screens: `/permits/create` loads the event from `Accreditations_view`, service types, vehicle types, and the event's load-in and load-out dates. `/permits/questions` renders the FAQ.

`addpermitAction` (POST):

- Occupants in `Accupant` with a non-empty name are kept. Type `1` is looked up on `Company_Members.IDNumber`. A match replaces name, surname, and ID with the stored profile and sets `MID`. Driver flag is 0 for occupants.
- A driver with a non-empty name is looked up the same way, flagged `Driver = 1`, and appended.
- Occupants are stored as JSON on `Accupants`.
- `PermitNr` is first taken from `getRef` (accreditation prefix plus a zero-padded count). After insert, `PermitNr` is replaced with `PM` plus the new `PID` zero-padded to 6 digits.
- `PermitRefSerial` is a random 13-digit string.
- `Status` is set to 2. `EID` is the session event. `MID` and `CID` are read from `Zend_Auth` identity.
- An HTML email is sent through `mail.sarugby.co.za`. Subject is "SARugby Notification". The body is signed "MyPASS Administration" (since `b8652a3`). It states the request will be reviewed, quotes the permit number as `{PermitNr}/{year}`, and says cancellations or changes may be submitted until 1pm for the next day.

`savepermitAction` updates occupants and sets `ApplicantModifiedBy` / `ApplicantModifiedDate` from the auth identity. It does not change status and does not send mail.

`duplicatepermitAction` copies a permit, drops approval, decline, cancel, and gate/zone fields, sets `Status` 2, assigns a new `PermitNr` from `getRef` (it does not apply the later `PM` rewrite), and sets `MID` / `CID` from the auth identity.

`cancelAction` sets `Status` 5, `CancelledDate`, `CancelledBy` from the auth identity, and `CancelMessage`. A comment says "email supplier for cancelling". No email is sent.

Provenance: **Implemented**. Because the PIN login never authenticates, these actions dereference a missing identity on the current login path. **Inferred** failure if used on track.mypass.zone after PIN login. Whether another deployment still calls `LoginNow` is **Unknown / Requires Confirmation**.

### W6 — Download a permit PDF

`IndexController::getpermitpdfAction` reads query `id`, fetches `https://www.footprintapp.net/pdf/getpermit/id/{id}`, converts encoding, and streams an A4 landscape PDF named `permit_{id}.pdf` via Dompdf. It also writes the file under `AC_PATH/loadin/pdf/`. `AC_PATH` is not defined in `public/index.php` or `application.ini`. Provenance: **Implemented** fetch and stream. The save path is **Unknown** at runtime if the constant is missing.

The action is not blocked by an extra check beyond the controller `init` session gate. Any PIN holder who knows a permit id can request the PDF. The remote URL is not limited to event 367 inside this repository.

### W7 — Company profile and staff

`CompanyController` and `UsersController` redirect to `/login/` unless `Zend_Auth` has an identity.

Company profile updates `Companies_Profile` for `identity.CID` and accepts a base64 image written to `UPDATE_PATH/companies/{CID}_image.jpg`. `UPDATE_PATH` is `public/archives/` (`public/index.php`). That directory is gitignored.

Staff list is `Company_Members` for the company. Add and profile accept role, login flag, and a base64 image under `public/archives/accreditation/{id}_image.jpg`. When login is enabled on create, a random password is `md5`'d and stored. The plain password is not emailed. JSON `OK=0` means the save matched; `OK=1` means it did not.

Provenance: **Implemented**, and disconnected from the PIN login in the same way as W5.

`LoginController::companypicAction` and `uploadremotecompanyAction` read or write company images by id with no session check. Provenance: **Implemented**. **Inferred** as an unauthenticated file endpoint.

## Functional requirements

### FR-001 — Session gate versus company login

Requirement: The dashboard and permit controllers require session `EVENT.EID`. The login form sets that value to `367` when the shared PIN matches. Company, user, and permit write actions require `Zend_Auth` identity (`MID`, `CID`). `LoginNow` would authenticate `CompaniesLogin.Email` / `password` and set `EVENT.EID` to `44`, but nothing in the login form calls `LoginNow`.

Expected behaviour on this revision: PIN opens the read dashboard only.

Evidence: `LoginController`, `IndexController::init`, `PermitsController::init`, `UsersController::init`, `CompanyController::init`.

Implementation: Implemented.

Confidence: High.

Unknown: whether production still depends on `LoginNow`. Requires confirmation.

### FR-002 — Dashboard contents

Requirement: For the session event, show permits for one load type and one request date, with counts, status badges, zone load, and gates.

Status badges in `index/index.phtml`:

| `Status` | Badge |
|---|---|
| less than 4 | Pending |
| 4 | Approved |
| 5 | Cancelled |
| 6 | Declined |
| 7 | Checked-In |
| 8 | Delayed |
| 9 | Checked-Out |

The summary counters at the top of the same file also increment a "declined" count when status is 4, and a "pending" count for every status below 4 (including 0, which is also counted as saved). The badge column is the behaviour a person sees on each row. The summary numbers do not match the badges for status 4.

`addpermitAction` and `duplicatepermitAction` write status 2 (Pending). `cancelAction` writes status 5. This repository does not write 4, 6, 7, 8, or 9.

Evidence: `index/index.phtml`, `PermitsController`.

Implementation: Implemented.

Confidence: High for the badge table. The writer of the other statuses is unknown.

### FR-003 — Date list

Requirement: Selectable dates are the union of the event's `LoadinActiveDate` and `LoadOutActiveDate` JSON arrays. Format in storage is day/month/year. The option value sent back is month/day/year.

Evidence: `IndexController::indexAction`.

Implementation: Implemented.

Confidence: High.

### FR-004 — Registration uniqueness

Requirement: Reject a registration when the company registration number exists, or when the person's ID number or email exists on `Company_Members`.

Evidence: `RegisterController::check_Company`, `check_IDNumber`.

Implementation: Implemented.

Confidence: High.

The queries concatenate request values into SQL. Validation is existence only. Format of the ID number, email, and registration number is not checked on the server in the portion read.

### FR-005 — Email confirmation

Requirement: Registration email contains a key link on `supplier.sarugby.online`. Opening `/register/validateemail/id/{key}` on this application marks the email confirmed and allows login.

Evidence: `RegisterController`.

Implementation: Implemented.

Confidence: High that the link host and the confirm action differ. Whether `supplier.sarugby.online` is this codebase is **Unknown / Requires Confirmation**.

### FR-006 — Permit identity fields

Requirement: A request captures load type (1 load-in, 2 load-out), service type, the applicant's own reference, request date, time in, time out, job description, vehicle type, make and model, licence, weight, height, optional trailer licence, weight, and height, driver name, surname, ID, and mobile, and additional occupants.

Evidence: `permits/create.phtml`.

Implementation: Implemented as a form. Server-side required-field checks were not found in `addpermitAction`. The FAQ lists a minimum set (date and times, vehicle licence, driver name, surname and ID, vehicle type and weight, occupants). That minimum is **Documented**, not enforced in the action.

### FR-007 — Permit number

Requirement: After insert, the stored `PermitNr` is `PM` plus the numeric id padded to 6 characters. The email shows that number, a slash, and the year of `RequestDate`.

Evidence: `PermitsController::addpermitAction`.

Implementation: Implemented for new requests. Duplicates keep the `getRef` value instead.

Confidence: High.

### FR-008 — Terms

Requirement: The login page link "Terms & Conditions" loads HTML from `https://footprintapp.net/api/mypass/gettermsandconditions/` into a modal. The form can still be submitted without accepting. The "I Accept" button is `display: none` and no script defines `accept_Terms`.

Evidence: `login/index.phtml`, commit `c6e1ef9`.

Implementation: Implemented as a viewer. Acceptance is not stored.

Confidence: High.

### FR-009 — Logout

Requirement: `/login/logout` clears `Zend_Auth` and destroys the session, then redirects to `/login`.

Evidence: `LoginController::logoutAction`. The layout links here.

Implementation: Implemented. This also drops `EVENT.EID`.

Confidence: High.

## Integrations

### MySQL

Purpose: accreditation, companies, members, permits.

Trigger: each page load that reaches a model.

Direction: application to `db.footprintapp.net`, database `teamwizz_SARU`, adapter `pdo_mysql`.

Authentication: username and password in `application/configs/application.ini`. Values omitted.

Failure: several model methods print the exception. There is no user-facing retry.

Tables and views this application queries from controllers:

- `Accreditation_Permits`, `Permits_view`, `Permit_Status` (model method `getStatus`; the dashboard does not call it)
- `Accreditation_List`, `Accreditations_view`
- `Accreditation_Loading_Zones`, `EventGates`, `Accreditation_Load_Services_Types`, `vehicletypes`
- `Companies_Profile`, `Company_Members`, `Company_Events`, `CompaniesLogin` (only in unused `LoginNow`)
- `Accre_Company_Types`, `BBEE_Level`, `countries`

### SMTP

Purpose: registration, password reset, permit submission.

Trigger: those actions when an email address is present.

Direction: outbound to `mail.sarugby.co.za` port 25, from `accreditation@sarugby.co.za`.

Failure: exceptions are swallowed in register, login, and index mailers. `PermitsController::sendMail` prints the exception.

### footprintapp.net

| Call | Trigger | Response used |
|---|---|---|
| GET `/api/mypass/gettermsandconditions/` | User opens terms | JSON field `termsandconditions` injected as HTML |
| GET `/pdf/getpermit/id/{id}` | PDF action | HTML body rendered to PDF |
| POST `/email/send` | Not called; the call site is commented | Would post `Email`, `FullName`, `Content` |

Failure behaviour for the live GET calls is the PHP or browser error that results. It is not handled.

### AWS CodeDeploy

See `technical-documentation.md`. Not a user-facing workflow.

## Constraints

- One shared PIN selects a single event id compiled into `LoginController`. Changing event requires a code change. The last such change set id `367` (`b8652a3`).
- Registration still attaches the company to event id `44`.
- Locale in config is `af_ZA`. The screens that were read are English.
- Session cookie lifetime in config is `864000` seconds (10 days).
- `Default_Bootstrap::_initForceSSL` redirects to HTTPS when `SERVER_PORT` is not 443. The class file lives at `application/modules/default/controllers/Bootstrap.php`. Zend module bootstrap classes are normally `application/modules/{module}/Bootstrap.php`. **Inferred:** this hook does not run. The root `.htaccess` HTTPS redirect is commented out.
- `public/index.php` contains a commented maintenance page (typos included: "curretly", "difficilties") that is not active.

## Error behaviour

`ErrorController` returns 404 "Page not found" for a missing route, controller, or action, and 500 "Application error" otherwise. Exception detail is shown only when `displayExceptions` is true. Config sets that flag to 0 for every environment. `public/index.php` still sets `display_errors` to 1, so PHP notices can still be emitted before the error controller.

`EventsController.php` declares class `Companies_EventsController`. `OrgController.php` declares class `Company_OrgController`. Zend will not route `/events` or `/org` to those classes. Provenance: **Inferred** from the class names versus the file names. The actions themselves only `print_r` a list or are empty.

## Planned behaviour

No roadmap, TODO issue list, or "planned" comments that describe a future release were found. Commented blocks in `IndexController::indexAction` previously showed company totals (users, permits, events) and are not active.

The FAQ sentence "The requirements for the RWC Sevens 2022..." describes an accreditation portal at "[www]" with the URL left as a placeholder. Provenance: **Documented** incomplete link.
