# PRD — psl2020

Evidence date: 2026-10-06. Source: `git@bitbucket.org:overdrive/psl2020.git`, `master` at `007bc5a` (2020-08-04).

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

`qa` (`65697aa`) and `prod` (`3c79d1d`) differ from this tree only in favicon bytes and, on `qa`, the database host. Product behaviour below is the same on all three tips.

## Product purpose

A person opens the PSL Operations (2020) Accreditation Portal, completes a stepped application, and receives a reference number. They can return, sign in with email and that reference, and see whether the application is new, pending, in review, confirmed, declined, or approved. While it is still editable they can change their details and replace the headshot.

The portal is one event site. The home page and the sign-in page always load accreditation code `5F27014B26D5F` from `Accreditations_view`. A direct request to `/application/apply/code/{code}` loads whichever code is in the URL, if that code exists.

**Unknown / Requires Confirmation:** the business name of the database row behind `5F27014B26D5F`, and whether other event codes are meant to be used on this host.

## Users and interactions

| Actor | What they do | Evidence | Status |
|---|---|---|---|
| Applicant | Completes the wizard and submits | `index/index.phtml`, `ApplicationController::applyAction` | **Implemented** |
| Returning applicant | Signs in with email and reference, views profile, edits until status locks | `login.phtml`, `loginnowAction`, `profileAction`, `updateAction` | **Implemented** |
| Applicant with an attendance link | Opens `/application/attendance/key/{key}/att/1` or `att/2` | `attendanceAction` | **Implemented** |
| Accreditation record (data, not a user in this app) | Supplies banner, dates, categories, roles, terms, email templates, close date | `Accreditations_view` via `Model_AccreditationList::getProfile` | **Implemented** as data. Who edits it is not in this repo |
| Deploy operator | Pipeline on `qa` or `prod` | `bitbucket-pipelines.yml` | **Implemented** as scripts. No UI |
| Reviewer / approver | Not present | Status labels exist; no approve action except applicant self-decline | **Unknown / Requires Confirmation** — **Inferred** to live in Footprint |

There is no separate admin role, security group, or staff login in this repository. `application/index.phtml` lists roles from `Model_AccreditationRoles::getAll()` and is not the public wizard.

## Main workflows

### W1 — Apply from the home page

1. Visitor opens `/`. `IndexController` loads code `5F27014B26D5F`.
2. If the row has no `AC`, the page prints "Sorry this page does not exits" and stops.
3. If `CloseDate` at 24:00 is already past, the view switches to `application/closed.phtml`.
4. Otherwise the visitor sees the jQuery Steps wizard: Personal, Organisation, Photo, Apply (linked events), Finish.
5. Finish posts the form with ajax to `/application/apply/code/{code}`. The response body is the reference number, shown in a thank-you modal. The page reloads after 5 seconds.

### W2 — Return and edit

1. Visitor opens Sign-In (`/application/login`). The same hard-coded code loads the banner and title.
2. They submit email and reference. `loginnowAction` looks up `AccreditationApplycation` for that email, reference, and accreditation id.
3. On success a Zend session `user_session` stores `AAID` and `aCode`, and the browser goes to `/application/profile/code/{code}`.
4. Profile shows status, zones, services, and an edit form. Confirmed, declined, and approved lock the fields.

### W3 — Confirm or cancel attendance

1. Visitor opens `/application/attendance/key/{AttendanceKey}/att/1` (attend) or `att/2` (do not attend).
2. The key must match `AccreditationApplycation.AttendanceKey`.
3. Attend sends the `EmailAttandanceConfirmed` template and clears the key. Cancel sets status to declined and, when `EmailRejected` is not empty, sends that template.

## FR-001 — Open the event application

**Requirement:** A visitor who opens `/` sees the accreditation named by code `5F27014B26D5F`: banner, title (name, dates, location, or fixture teams when `FID > 0`), categories, roles, and the application form.

**Who:** Anonymous visitor.

**Trigger:** GET `/` (`IndexController::indexAction`).

**Inputs:** None from the user. The code is hard-coded.

**Behaviour:** Load `Accreditations_view` where `aCode` equals that code. If the row has no `AC`, print "Sorry this page does not exits" and stop. Load public categories (`Teams = 0` and `hidden = 0`) and roles for that accreditation. Load linked accreditations whose `DateTo` is today or later. If `CloseDate` at 24:00 is already past, render `application/closed.phtml` instead of the form.

**Output:** HTML wizard, or the closed page.

**Validation:** Missing profile stops with the warning text. Empty `CloseDate` is not guarded.

**Errors:** Database failures surface as PHP errors. `display_errors` is on in `public/index.php` and in the production section of `application.ini.bak`.

**Provenance:** **Implemented.**

**Note:** `IndexController` also contains POST handling that calls `random_id`, `SavePhoto`, and `EmailApplicant`. Those methods are not defined on `IndexController`. The form in `index/index.phtml` posts to `/application/apply/code/{code}`, so a normal submit does not hit that POST block. A POST to `/` would fail. Provenance: **Implemented** (dead path).

## FR-002 — Open an application by event code

**Requirement:** GET `/application/apply/code/{code}` shows the wizard for that accreditation code.

**Who:** Anonymous visitor. If a `user_session` already has `AAID > 0`, the action redirects to `/application/profile/code/{session code}` before the form is shown.

**Trigger:** `ApplicationController::applyAction`.

**Behaviour:** Same load, category, role, link, and close-date rules as FR-001, using the URL code instead of the hard-coded code. Unknown code prints the same "does not exits" warning.

**Output:** `application/apply.phtml`.

**Provenance:** **Implemented.**

**Discrepancy:** `apply.phtml` posts to `/accreditations/application/apply/code/{code}`. The homepage form posts to `/application/apply/code/{code}`. A person who opens the apply URL directly submits to the Footprint-style prefix. Whether this host serves that prefix is **Unknown / Requires Confirmation**.

The close-date branch changes the view script to `closed.phtml` and does not return. A POST in the same request still inserts (FR-003). Provenance: **Implemented.** Whether that is acceptable is **Unknown / Requires Confirmation**.

## FR-003 — Submit an application

**Requirement:** The visitor submits the wizard. The system stores one row per selected event and returns a reference number.

**Who:** Anonymous visitor (or a session that was not redirected).

**Trigger:** POST `/application/apply/code/{code}` with `data[...]` and optional `apply[]`.

**Inputs (from `index/index.phtml`):**

| Field | Required in the browser |
|---|---|
| `data[FirstName]`, `data[Surname]`, `data[email]`, `data[Mobile]`, `data[IDnumber]` | Yes (jQuery Validate `required`; email format on email) |
| `data[organisation]`, `data[Category]`, `data[Role]` | Yes |
| `data[Reason_for_pplication]` | Yes. Helper text says approval and zone access depend on an accurate description of roles |
| `data[User_pic]` | Class `required` is added or removed in script. See FR-004 |
| `data[acceptTerms]` | Yes, checkbox value 1. Terms text names PSL Operations and the Safety of Sports and Recreational Events Act of 2009 |
| `data[acceptnotes]` | Checkbox bound to `AccreditationNote`. Not marked `required` |
| `data[Producer_Fullname]`, `data[Producer_Email]` | Required only when `MediaOnly == 1` |
| `data[Invitefrom]` | Openfield / Roth Media radios are inside an HTML comment. Not submitted |
| `apply[]` | Optional checkboxes of linked event ids (`OID`) |

**Behaviour:**

1. Build a 6-character hex reference with `strtoupper(bin2hex(mcrypt_create_iv(3, MCRYPT_DEV_URANDOM)))`.
2. Set `AC`, `TID`, and `FID` from the accreditation profile, `DateApply` to now, `History` to `None`, `Ref` to the new reference. If `DaysAttending` is absent, store an empty string. If `User_pic` is empty, store a hard-coded placeholder JPEG.
3. Insert into `AccreditationApplycation`. Local `SavePhoto` is commented out. On success, POST the picture to `https://www.footprintapp.net/accreditations/application/updateimage` with `ac`, `ref`, and `pic`.
4. If `EmailNotes` on the accreditation is not empty, send that template to the applicant (FR-008).
5. If `apply` is greater than 0, insert the same payload again for each selected linked id, changing only `AC`. The photo POST and the email run only for the first insert.
6. Echo the reference and exit. The thank-you view after that echo is unreachable.

**Validation:** Browser validation only. The insert does not check required fields, terms, or a duplicate email.

**Errors:** Insert exceptions are printed by the model. Mail exceptions are printed by `sendMail`. The ajax caller treats the whole body as the reference number.

**Provenance:** **Implemented.**

## FR-004 — Headshot rules in the browser

**Requirement:** The photo step uses Cropper.js at a 1:1 ratio. On finish, the cropped data URL is written into `data[User_pic]` and the form is posted with ajax (`ajaxForm`).

**File size:** The script rejects files larger than 2,097,152 bytes and alerts "File size must not be larger than 1.5mb". Provenance: **Implemented.** The number and the message disagree.

**Role flag:** `getCurrentRole` parses `<?=$Roles?>` as JSON. If the selected role has `photo == 1`, the image field loses the `required` class. Otherwise the class is added. A Zend rowset echoed into that script is not valid JSON. Whether the role list filters at runtime is **Unknown / Requires Confirmation**. The `photo` flag's business meaning is **Inferred** and looks inverted relative to the class it toggles.

**Supplier note:** If the selected option has `reg="1"` (`SupplierReg`), the page shows a note that a services-company owner must register at `http://supplier.sarugby.online`. Provenance: **Implemented** copy. Whether that SA Rugby link is still intended on a PSL portal is **Unknown / Requires Confirmation**.

## FR-005 — Duplicate email warning

**Requirement:** While the applicant types an email, the page calls `/index/checkemail/email/{email}/ac/{AC}`.

**Behaviour:** `Model_AccreditationList::checkEmails` counts rows in `AccreditationApplycation` with that email and accreditation id. The JSON is `{"Ok": <count>}`. A count above 0 shows "Email already in use,please use different email address." and clears the confirm field. A count of 0 copies the email into the confirm field.

**Validation:** This check is not repeated on insert. The same action exists at `/application/checkemail`.

**Provenance:** **Implemented.**

## FR-006 — Sign in and sign out

**Requirement:** Sign-in accepts an email and a reference number for the hard-coded accreditation.

**Who:** Returning applicant.

**Trigger:** POST `/application/loginnow/ac/{AC}` with `emailaddress` and `refnumber`. The login page sends those names. The reference input is `type="password"`.

**Behaviour:** `UserLogin` selects `AccreditationApplycation` joined to `Accreditation_List` where `AC`, `Ref`, and `email` all match. On a row, session `user_session.Code` is `aCode` and `user_session.AAID` is `AAID`. Response is `{"OK":1}`. Otherwise `{"OK":0}` and the page shows "Bad credentials."

**Session:** Cookie lifetime in `application.ini.bak` is `864000` seconds (10 days). Provenance: **Documented** in that file. Whether the live ini matches is **Unknown / Requires Confirmation**.

**Sign out:** `logoutAction` clears `user_session` and redirects to `/application/login/code/{code}`. `loginAction` ignores the URL code and loads `5F27014B26D5F` again.

**Errors:** Exceptions are printed. There is no lockout, captcha, or password.

**Provenance:** **Implemented.**

## FR-007 — View and update a profile

**Requirement:** A signed-in applicant sees their application and can change it until it is confirmed, declined, or approved.

**Trigger:** GET `/application/profile/code/{code}`. POST `/application/update/aaid/{AAID}` with `data[...]`. Photo changes POST `/application/updateimage`.

**Behaviour:**

- If `user_session.AAID` is not greater than 0, redirect to `/application/apply/code/{code}`.
- Load the application by `AAID`, then the accreditation by that row's `aCode` (the URL code is replaced).
- Status label, in order:
  - `Pending == 1` → Pending (still editable).
  - else `Status == 0` and `DateApply < LastUpdate` → In Review (still editable; the lock line is commented out).
  - else `Status == 1` → Confirmed, fields disabled.
  - `Status == 2` → Declined, fields disabled, `DeclinedMessage` shown.
  - `Status == 3` → Approved, fields disabled.
  - otherwise New Application.
- Zones come from JSON in `aZones`, looked up in `AccreditationsZones`. Services come from `Accreditation_Applicants_Services` via `getProfileServices`.
- The headshot is fetched from `https://www.footprintapp.net/accre/photos/{Ref}.jpg` when that URL returns HTTP 200. Otherwise the image is `/accre/profile-icon.png`.
- Update writes the posted `data` array plus `LastUpdate`. The timestamp format is `Y-m-d H:s:i` (`s` is seconds, not minutes). There is no session check on `updateAction` or `updateimageAction`.

**Provenance:** **Implemented.** Who sets Pending, Status, zones, and services is **Unknown / Requires Confirmation**.

## FR-008 — Email the applicant

**Requirement:** Mail uses the template stored on the accreditation row, with placeholders replaced.

**Placeholders:** `[ACCREDITATIONNAME]`, `[CONTACTNAME]`, `[CONTACTTELEPHONE]`, `[CONTACTEMAIL]`, `[REFNUMBER]`, `[APPLICANTNAME]`. Decline mail also replaces `[REASONNOTE]`.

**When:**

| Event | Template column | Condition |
|---|---|---|
| Application saved | `EmailNotes` | Column not empty |
| Attendance confirmed (`att=1`) | `EmailAttandanceConfirmed` | Row found. Subject variable is unused |
| Attendance cancelled (`att=2`) | `EmailRejected` | Column not empty. Reason text is "Applicant confirmed NON-ATTENDANCE / CANCELLATION for this event." |

**Transport:** `Zend_Mail` via `smtp.gmail.com` port 587, TLS, from `accreditation@magus.events`, display name "Accreditation - PSL Operations", subject "PSL Operations Notification". The SMTP password is in source and is not recorded here.

**Failure:** The exception is printed into the response. The application row is already saved.

**Provenance:** **Implemented.**

`/application/test` sends a fixed message to a hard-coded address. Provenance: **Implemented** test action, not a product screen.

## FR-009 — Attendance response

**Requirement:** A link with a stored attendance key records that the applicant will attend or will not.

**Trigger:** `/application/attendance/key/{key}/att/{1|2}`.

**Behaviour:**

- Unknown or already-cleared key: `attendance.phtml` says the link does not exist or has been used already.
- `att=1`: email the confirmed template, set `AttendanceRequest` to 1, `AttendanceResponseDate` and `LastUpdate` to now, clear `AttendanceKey`. Message: "Thank you for confirming your attendance".
- `att=2`: if `EmailRejected` is not empty, send decline mail; set `Status` to 2, `History` to "Declined by Applicant", `DeclinedDate` to now, clear the key. The message variable `$message` is never set on this branch.

**Who creates `AttendanceKey`:** not this repository. **Unknown / Requires Confirmation.**

`testattendanceAction` repeats the same updates and prints exceptions. Provenance: **Implemented** duplicate.

## FR-010 — Closed applications

**Requirement:** After `CloseDate` 24:00 the visitor sees "Accreditation applications is now closed." and a mailto link to `ContactName` / `ContactEmail` from the accreditation row.

**Provenance:** **Implemented** in `closed.phtml`. The grammar is the string in the view.

## FR-011 — Linked events

**Requirement:** The Apply step lists rows from `LinkedAccreditations` where `AID` is this accreditation and `DateTo` is today or later (`applyAction` and the home page). Each checkbox value is `OID`. On submit, each selected id is inserted as another application with the same reference and personal data.

`applymultipleAction` loads links with no date filter and reads `$this->DATA`, which is not set. Its view posts to the `/accreditations/` prefix and uses `app[][...]` fields that `applyAction` does not read. Provenance: **Implemented** alternate action. It is not linked from the layout.

## FR-012 — Browser support gate

**Requirement:** `default.phtml` renders the portal when `get_user_browsers()` returns something other than an empty string or `ie`. Otherwise it shows a panel linking to Chrome and Firefox.

**Behaviour:** The function inspects `HTTP_USER_AGENT` in this order: MSIE → `ie`, Firefox, Safari, Chrome, Flock, Opera. The first match wins. An empty result or `ie` sets `$notSupported` to false and shows the unsupported panel. Any other recognised token sets `$notSupported` to true and renders the portal.

Chrome's user-agent also contains `Safari`, and Safari is tested first, so Chrome is classified as `safari`. That classification is still allowed through. A user-agent that matches none of the patterns (empty result) is blocked. Provenance: **Implemented**.

**Theme:** When the accreditation row has `Theme == 2`, the wizard step colours switch to `#5C0605` / `#9B0E17`. Otherwise they stay `#011B4C`. Provenance: **Implemented**.

## Constraints

- Time zone is `Africa/Johannesburg` in `public/index.php` and `ApplicationController`.
- Locale in the bak ini is `af_ZA`. The views inspected are English.
- One hard-coded event on `/` and on sign-in.
- Reference numbers are 6 hex characters. Collision handling is absent.
- No automated tests, no admin UI, no documented SLA.

## Integrations

See `technical-documentation.md`. Summary: MySQL `teamwizz_SARU` on Footprint, photo POST to Footprint, Gmail SMTP, AWS CodeDeploy from Bitbucket Pipelines on `qa` and `prod`.
