# PRD — members.mypass.zone

Evidence date: 2026-10-09. Source: `git@bitbucket.org:overdrive/members.mypass.zone.git`, `master` at `7268c97` (2022-06-24, "Initial commit").

The git repository is named `members.mypass.zone`. The application it contains is the SA Rugby supplier portal ("SA Rugby - Load-in and Load-out"). Registration links and the CodeDeploy path use `supplier.sarugby.online`. Silicon Ark documents an earlier snapshot of that portal in `supplier.sarugby.online/` (`6f0e6c2`, 2021-04-29). This PRD is only for the tree at `7268c97`.

There is no requirements document in the repository. Requirements below are taken from implemented behaviour or from text the application shows. Provenance is marked on each item.

Secret values are not written here. `application.ini` contains a MySQL password. Each mail sender contains an SMTP password. `LoginController::indexAction` contains a login override string. Commented code contains an older mailbox password.

## Users

| User | Evidence | Provenance |
|---|---|---|
| Supplier company administrator | Registration stores the first person with role `Administrator`. They sign in and manage the company, staff, and permits. | Implemented |
| Other company staff | `UsersController` offers roles Administrator, Director, Management, Operations, Driver, Technician, General Staff. | Implemented |
| SA Rugby event administrator | Emails say an administrator reviews registration and permit requests. This repository does not implement that review. | Documented in email copy. The review application is **Unknown / Requires Confirmation**. |
| Venue operations / health and safety | FAQ names the Operations and Accreditations Centre, a health-and-safety file, and the addresses `HealthandSafety7s@sarugby.co.za` and `security@sarugby.co.za`. | Documented in the FAQ. Not a user of this web application. |

Login does not check role. Any row in `CompaniesLogin` that authenticates can open the same screens. Provenance: **Implemented**.

## Product purpose

A supplier company registers once, confirms an email address, and then requests a vehicle access permit for each day a vehicle needs to enter an SA Rugby event venue. The company can see the status of those requests and keep a staff list used as drivers and occupants.

Provenance: **Inferred** from the dashboard, permit form, registration email, and FAQ. No product statement exists as a document.

## FR-001 — Company registration

**Who:** An unauthenticated visitor.

**Trigger:** POST `/register/newcompany`.

**Inputs:** Posted `data` (company, including `RegistrationNumber`, `TradingName`, `Country`) and `P_data` (person, including `IDNumber`, `Email`, `password`, name, mobile).

**Behaviour:**

1. Reject the request if a company with that registration number already exists. Response JSON `OK` = 2 and the message that the account already exists and the user should contact SARU.
2. Reject the request if a person with that ID number or email already exists. Same `OK` = 2 message.
3. Store the person password as MD5. Set role `Administrator`. Set `IDType` 1 when country is `SOUTH AFRICA`, otherwise `IDType` 2. Generate a short `Ref`.
4. Insert a row in `AccreditationMemberProfile` (password, role, and login flag removed from that copy) and a row in `Company_Members`, linked by `AMID`.
5. Insert `Companies_Profile` with `Active` = 1, `AdminID` and `CreatedBy` set to the new member, a generated `RefNumber`, and `CreatedDate` of today.
6. Insert `Company_Events` with `CID`, `EID` = 44, `Status` = 2.
7. Store a `Validate_Key` on the member.
8. Email a link `http://supplier.sarugby.online/register/validateemail/id/{key}`. The body says an administrator will review the company before the profile can be activated, and that activation can take up to 48 hours. It also says the user must click the link, and that without additional documents they cannot sign up and connect to an event. Contact line: `accreditation@sarugby.co.za`. Registration contact centre `+27219287000`.
9. Return JSON `OK` = 1 and a thank-you message that an email was sent.

**Errors:** Any exception returns JSON `OK` = 0 and asks the user to contact SARU.

**Conflict:** The email says review can take up to 48 hours before the profile is activated. The insert sets `Companies_Profile.Active` = 1 immediately. Email confirmation then sets `AllowLogin` = 1. Provenance of the conflict: **Documented** email versus **Implemented** insert.

**Evidence:** `RegisterController`. Confidence: High.

`IndexController::newcompanyAction` is a second, older registration path. Its validation link is `http://www.sarugby.online/company/index/validateemail/id/{member id}`. `IndexController::validateemailAction` is empty. `IndexController::init` redirects unauthenticated users to `/login/`, so this action is not reachable without a session. Provenance: **Implemented**, and it disagrees with FR-001.

## FR-002 — Email confirmation

**Who:** The person who received the registration email.

**Trigger:** GET `/register/validateemail/id/{Validate_Key}`.

**Behaviour:** If the key matches `Company_Members.Validate_Key`, clear the key, set `Email_Confirmed` = 1, `Email_Confirmed_Date` to today, and `AllowLogin` = 1. The page says the account is now active and links to `/login`. A missing or already-used key shows an error and the same login link. A request with no id redirects to `/login/`.

**Evidence:** `RegisterController::validateemailAction`. Confidence: High.

Login (FR-003) does not read `AllowLogin` or `Email_Confirmed`. Provenance: **Implemented**.

## FR-003 — Sign in and sign out

**Who:** A company user.

**Trigger:** POST `/login` with `email` and `password`. GET `/login` shows the form. An existing identity is redirected to `/`.

**Behaviour:**

1. If the typed password equals a hardcoded string, load the stored password for that email from `CompaniesLogin` via `CompanyProfile::adminLogin` and use that stored value as the credential. Otherwise MD5 the typed password.
2. Authenticate with `Zend_Auth_Adapter_DbTable` against table `CompaniesLogin`, identity column `Email`, credential column `password`.
3. On success, set session namespace `EVENT`, property `EID`, to 61. Store the identity. Update `LastLogin` on `Companies_Profile` for that `MID`. Redirect to `/`.
4. On failure, the view message is "Bad entered credentials".
5. `LoginNow` echoes the text `LOGIN` before authenticating.

**Sign out:** GET `/login/logout` clears the identity, destroys the session, and redirects to `/login`.

**Evidence:** `LoginController`. Confidence: High.

The login override is **Implemented**. It lets anyone who knows the string and an email sign in as that account. Human review is required. The string is not copied here.

## FR-004 — Forgot and reset password

**Who:** A person who knows the account email.

**Trigger:** POST `/login/forgot` with `Email`. POST `/login/reset/user/{ResetKey}` with `password`.

**Behaviour:**

1. Look up the email on `Company_Members`. If found, store `ResetKey` as the MD5 of 4 random bytes (upper-case hex) and email an `http://{current host}/login/reset/user/{key}` link. The mail says to contact the site if the request was not made. Enquiries: `accreditation@sarugby.co.za`.
2. If the email is not found, the action does not say so.
3. Reset requires a matching `ResetKey`. The new password is stored as MD5. `ResetKey` is cleared. `AllowLogin` and `Email_Confirmed` are set to 1. The email includes the new password in clear text and addresses the person with `FirstName` twice (the surname variable is not the one used in the greeting).

**Evidence:** `LoginController::forgotAction`, `resetAction`. Confidence: High.

## FR-005 — Dashboard

**Who:** A signed-in user. `IndexController::init` redirects to `/login/` when there is no identity.

**Trigger:** GET `/`.

**Behaviour:**

- Count staff for the identity `CID` (`UserProfile::TotalCount`).
- Count permits for that `CID` and the session `EID`.
- Count `Company_Events` rows for that `CID`.
- Load permits for the dashboard and group them by `LoadType`, then by the timestamp of `RequestDate`.
- Load the accreditation row `Accreditation_List` where `AC` equals the session event id.
- Load loading zones for that event id.
- Build selectable load-in and load-out dates from the **last** permit visited in the loop (the variable `PERMITID` is overwritten each time). Dates in the future are kept unless the cutoff (noon on the previous day) has passed. The view assigns this combined list as `LoadOutArray`.

**Evidence:** `IndexController::indexAction`, `index/index.phtml`. Confidence: High for the counts and the grouping. The last-permit date list is **Implemented** and looks unintentional. **Requires Confirmation** whether the dashboard date list was meant to come from the accreditation row instead.

The dashboard header contains a commented event `<select>` with the same three events as the layout. A visible copy of that `<select>` is in `application/layouts/default.phtml` on every authenticated page. See FR-006.

The view labels permit statuses. Provenance: **Implemented** in `index/index.phtml`:

| Status value | Label shown |
|---|---|
| below 4 | Pending |
| 4 | Approved |
| 5 | Cancelled |
| 6 | Declined |
| 7 | Checked-In |
| 8 | Delayed |
| 9 | Checked-Out |

This application sets only 2 (new or duplicated request) and 5 (cancel). Who writes 4, 6, 7, 8, and 9 is **Unknown / Requires Confirmation**.

## FR-006 — Switch event

**Who:** A signed-in user, on a page that has loaded `index/index.phtml`.

**Trigger:** Change the header `<select id="selectEvents">`.

**Options (hard-coded):** 0 "Select Event", 61 "HSBC Cape Town Sevens", 62 "Kyalami 9 Hour", 66 "The Sun Met 2020".

**Behaviour:** `selectEventChange` in `index/index.phtml` POSTs `/index/updatesession/eid/{id}` and reloads. The action writes `EVENT.EID` and exits. It does not check that the company is linked to that event.

**Gap:** The function is defined only in the dashboard view. The `<select>` is in the layout, which is used on Company, Staff, and FAQ as well. On those pages the function is not defined. Provenance: **Implemented**.

Login still overwrites `EID` to 61 on the next sign-in (FR-003). Registration still attaches the company to event 44 (FR-001). Those three sources disagree. **Requires Confirmation.**

## FR-007 — Request a permit

**Who:** A signed-in user.

**Trigger:** GET `/permits/create`, then POST `/permits/addpermit`.

**Inputs:** `data` (permit fields), `driver`, and `Accupant` rows (name and, when type is 1, an ID number).

**Behaviour:**

1. The create screen loads service types, vehicle types, and future load-in / load-out dates from the accreditation row for the session event. A date is omitted when noon on the previous day has already passed. The cutoff on the profile screen uses 13:00 instead of 12:00. Provenance: **Implemented**. Whether 12:00 and 13:00 are both intentional is **Requires Confirmation**.
2. Occupants with a name are kept. Type 1 ("permanent") looks up `Company_Members` by ID number and, on a hit, replaces name, surname, and ID with the stored member and sets `MID`. The driver is appended with `Driver` = 1. The list is stored as JSON in `Accupants`.
3. The action assigns `PermitNr` from `getRef` with only the event id before insert. `getRef` requires the new id and the event id, so that first call does not match the method signature. After a successful insert it calls `getRef` again with the new id and the session event, stores that value, and updates the row. `getRef` upper-cases the accreditation prefix plus the id padded to 6 digits.
4. Email the address returned with the permit. The body says the request will be reviewed, quotes the permit number, and says cancellations and changes may be submitted until 1pm daily and will not be accepted after that for the next day. It says administrators may change the date and time. Contacts: `permits@sarugby.co.za` and `accreditation@sarugby.co.za`. Subject is "BIL 2021 Notification".

**Evidence:** `PermitsController::createAction`, `addpermitAction`. Confidence: High.

The mail body chooses "Load-Out" when `$permit["Surname"] == 2`, otherwise "Load-In". Comparing a surname to 2 is **Implemented**. It does not match the FAQ, which distinguishes load type, not surname. **Requires Confirmation** (likely a defect; the intended field is not confirmed from this line alone).

## FR-008 — View and edit a permit

**Who:** A signed-in user.

**Trigger:** GET `/permits/profile/key/{id}`. POST `/permits/savepermit`.

**Behaviour:** The permit is loaded with `getPermit($id, CID)`. A missing row redirects to `/`. The form is locked when 13:00 on the day before `RequestDate` has passed. Save updates occupants the same way as create, stamps `ApplicantModifiedDate` and `ApplicantModifiedBy`, and does not change `Status`.

**Evidence:** `PermitsController::profileAction`, `savepermitAction`. Confidence: High.

`profiletestAction` is a second profile renderer. It is not linked from the layout.

## FR-009 — Duplicate a permit

**Who:** A signed-in user.

**Trigger:** POST `/permits/duplicatepermit/id/{id}` with `date` and `ref`.

**Behaviour:** Load the permit by id without a company filter (`getPermitData`). Copy the row, drop approval, decline, cancel, gate, and zone fields, set `Status` = 2, a new 13-digit serial, `EID` from the session, `RequestDate` rearranged from a `d/m/Y` post into `m/d/Y`, and `YourRefNumber` from `ref`. Generate a new `PermitNr`. The action prints nothing on success and exits.

**Evidence:** `PermitsController::duplicatepermitAction`. Confidence: High.

The read is not limited to the signed-in company. Provenance: **Implemented**. Whether that is accepted is **Requires Confirmation**.

## FR-010 — Cancel a permit

**Who:** A signed-in user.

**Trigger:** POST `/permits/cancel/id/{id}` with `message`.

**Behaviour:** Set `Status` = 5, `CancelledDate`, `CancelledBy` = the member id, and `CancelMessage`. The action `print_r`s the update array and exits. A comment says "email supplier for cancelling". No email is sent. The update is not limited to the signed-in company.

**Evidence:** `PermitsController::cancelAction`. Confidence: High.

## FR-011 — Permit PDF

**Who:** A signed-in user (`IndexController::init`).

**Trigger:** GET `/index/getpermitpdf/id/{id}`.

**Behaviour:** Fetch `https://www.footprintapp.net/pdf/getpermit/id/{id}`, convert encoding, render landscape A4 with Dompdf, write `permit_{id}.pdf` under `AC_PATH/loadin/pdf/`, and stream it. `AC_PATH` is not defined in `index.php` or `application.ini`. A failure prints the exception.

**Evidence:** `IndexController::getpermitpdfAction`. Confidence: High that the fetch and render exist. Whether `AC_PATH` is defined on the server is **Unknown / Requires Confirmation**.

The FAQ says a permit document can be downloaded only after approval, from a menu icon next to the approved field. The PDF action itself does not check status. Provenance: **Documented** versus **Implemented**.

## FR-012 — FAQ

**Who:** A signed-in user (`PermitsController::init`).

**Trigger:** GET `/permits/questions`.

**Behaviour:** Render the static question list in `PermitsController::Questions()`. Topics include: a printed permit and Vehicle Access Pass are required; one permit per vehicle per day; duplicate is allowed; a health and safety file is required to work; the file checklist (37(2) agreement, insurance, induction, letter of good standing, and the rest of the list in code); delivery permits are at most 2 hours and work permits need the file; load-in versus load-out; identification; courier deliveries under the company name; contracted companies included in the company's file; contacts `HealthandSafety7s@sarugby.co.za` and `security@sarugby.co.za`. One answer says the health-and-safety contact is "Danie xxxxxx" (the name is already redacted in source). One answer says event-day accreditation for HSBC World Rugby Sevens Series is applied for on a separate accreditation portal, and the URL is the placeholder `[www]`.

**Evidence:** `PermitsController::Questions()`. Confidence: High that the text is shown. The rules in that text are **Documented**. Several are not enforced by this application (see business rules).

## FR-013 — Company profile

**Who:** A signed-in user.

**Trigger:** GET `/company/profile`. POST the same action with `data`.

**Behaviour:** Load `Companies_Profile` for the identity `CID`, plus B-BBEE levels, company types, and countries. A post updates that company with the posted `data` array and returns JSON `OK` 0 when the model returns the same id, otherwise `OK` 1. There is no field allow-list.

**Logo:** POST `/company/upload` with `img` (a data URL) writes `{CID}_image.jpg` under `public/archives/companies/` and stores the file name in `Pic`.

**Evidence:** `CompanyController`. Confidence: High.

`getmembersAction` is empty.

## FR-014 — Staff

**Who:** A signed-in user.

**Trigger:** GET `/users`. POST `/users/add`. GET and POST `/users/profile/id/{id}`.

**Behaviour:**

- The list is `UserProfile::getAll` for the identity `CID`.
- Add splits the post into `Company_Members` (company id, role, password, login flag) and `AccreditationMemberProfile` (name, ID, mobile, email, ref). If `AllowLogin` is posted, a password of 3 random bytes is MD5-stored. Otherwise `AllowLogin` is set to 0. A posted image is saved as `{MID}_image.jpg` under `public/archives/accreditation/`.
- Profile loads by id with no company check. Enabling login when it was off generates a new MD5 password. The generated password is not emailed from this action.
- Roles offered: Administrator, Director, Management, Operations, Driver, Technician, General Staff.

**Evidence:** `UsersController`. Confidence: High.

The FAQ says only one administrator is allowed. The add action does not count existing administrators. Provenance: **Documented** versus **Implemented**.

The layout links "My profile" to `/users/profile/id/{MID}`.

## FR-015 — Company picture without the usual session check

`LoginController::init` disables the layout and does not require an identity. `companypicAction` serves `{id}_image.jpg` from `public/archives/companies/` or a built-in placeholder, with `Content-type: image/gif`. `uploadremotecompanyAction` writes a company image for the posted `cid` from posted `img` and does not check the session.

**Evidence:** `LoginController`. Confidence: High. Whether the remote upload is still called by another system is **Unknown / Requires Confirmation**.

## Workflows

### Register and sign in

1. Visitor opens `/register` (no session required; layout disabled).
2. POST `/register/newcompany`.
3. User opens the email link on `supplier.sarugby.online`.
4. User signs in at `/login`. Session event becomes 61.
5. User lands on the dashboard.

### Request, change, or cancel a permit

1. User opens `/permits/create` or the dashboard "Request Permit" link.
2. POST `/permits/addpermit`. Status becomes 2. An email is sent.
3. User can POST `/permits/savepermit` until the cutoff, POST `/permits/duplicatepermit` onto another date, or POST `/permits/cancel`.
4. Approval and the printed document are described in the email and FAQ. Approval is not implemented in this repository.

## Integrations

| System | Purpose | Direction | Auth as implemented | Failure |
|---|---|---|---|---|
| MySQL `teamwizz_SARU` on `localhost` | All persisted data | Read and write | Username and password in `application.ini`. Password not repeated here. | Zend and PHP exceptions. Several actions print the exception. |
| Office 365 SMTP | Registration, reset, and permit mail | Outbound | Username `accreditations@sarugbyevents.co.za` in the controllers. Password not repeated here. | Caught and often swallowed. `PermitsController::sendMail` prints the exception. |
| footprintapp.net PDF | Permit document HTML | This server fetches | No credential in the URL | Exception printed |
| footprintapp.net email endpoint | Alternate mail sender | POST, not used by the live permit path | None in the request | Exception printed |

## Constraints

- Time zone is `Africa/Johannesburg` (`public/index.php`).
- Default locale in `application.ini` is `af_ZA`. The screens and emails inspected are English. Provenance: **Implemented** configuration versus **Implemented** English copy. Whether Afrikaans resources exist and are unused is **Unknown**.
- Session cookie lifetime is 864000 seconds (10 days) in `application.ini`.
- `public/index.php` turns `display_errors` on. `application.ini` turns them off for every environment, including development. The front controller wins for PHP display once the request is running only where the ini settings are applied first; the index file sets display on before bootstrap. Provenance: **Implemented**. Both files disagree.
- HTTPS is forced by `Default_Bootstrap` when the port is not 443. Registration and reset links that the application itself writes use `http://`.

## Empty or mismatched controllers

| File | Class declared | What it does |
|---|---|---|
| `EventsController.php` | `Companies_EventsController` | `indexAction` prints events from `AccreditationList::getEvents(2)` and redirects unauthenticated users to `/companies/login/`. Zend maps the file name `EventsController` to class `EventsController`, so this class is not the one the router loads. |
| `OrgController.php` | `Company_OrgController` | Empty `indexAction`. Same class-name mismatch. |
| `OrganisationController.php` | `OrganisationController` | Empty `profileAction`. No auth check. |
| `AuthonController.php` | `AuthonController` | Empty `indexAction` and `newcompanyAction`. |
| `PermitsController - Copy.php` | duplicate of the permits controller | Not routed. File name contains a space. |

Provenance: **Implemented**. These files are not product features a user can reach through the normal router, except `OrganisationController` and `AuthonController` if those URLs are requested.
