# PRD — Kyalami9Hour2019

Evidence date: 2026-10-10. Source: `git@bitbucket.org:overdrive/kyalami9hour2019.git`, `master` at `5b60e52` (2020-10-28).

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` (`e0f870a`) and `prod` (`0950599`) differ from this tree in the mail From address, in whether `application.ini` is tracked, and in one confirm-email line (`FN-55`). Those differences are called out where they change behaviour. The rest of the product behaviour below is the same on all three tips.

## Product purpose

A person opens the Kyalami 9 Hour 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 `5F9298188128B` 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 `5F9298188128B`, and whether other event codes are meant to be used on this host. Commit `afcd3f3` is titled "Update to event ID 245" and changes the hard-coded code; the number 245 is not in the diff.

## 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 |
| Supplier company user | Code exists under `application/modules/supplier/` | Not linked from the Kyalami header | **Implemented** as unused-on-this-journey code. See FR-014 |

There is no separate admin role, security group, or staff login in the `default` module.

## Main workflows

### W1 — Apply from the home page

1. Visitor opens `/`. `IndexController::indexAction` loads code `5F9298188128B`.
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 (only when linked events exist), 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 in the browser.

### 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` via `getByAttandanceID`.
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 `5F9298188128B`: 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:** GET or POST `/application/apply/code/{code}` (`ApplicationController::applyAction`).

**Inputs:** URL parameter `code`.

**Behaviour:** Same load and close-date rules as FR-001, using the URL code instead of the hard-coded one. Missing code or missing `AC` prints "Sorry this page does not exits" and stops.

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

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

**Constraint:** The home page does not offer a code picker. Any code that exists in `Accreditations_view` can be opened by URL.

## FR-003 — Submit an application

**Requirement:** Submitting the wizard stores one row in `AccreditationApplycation` and returns a reference number. When the visitor ticks linked events, one extra row is stored per ticked value, reusing the same reference and photo.

**Who:** Anonymous visitor.

**Trigger:** POST `/application/apply/code/{code}` from `#Mainform` (`ajaxForm` in `index/index.phtml`).

**Inputs:** `data[...]` fields from the wizard, optional `apply[]` checkboxes, accreditation profile already loaded for `{code}`.

**Behaviour:**

1. Reference is `strtoupper(bin2hex(mcrypt_create_iv(3)))` — 6 hex characters.
2. The row is given `AC`, `TID`, and `FID` from the accreditation profile, `DateApply` as now, `History` = `None`, and `Ref` as the new reference.
3. If `DaysAttending` is absent it is stored as an empty string. No days control was found on `index/index.phtml`.
4. If `User_pic` is an empty string, the action substitutes a hard-coded JPEG data URI before insert.
5. `insertData` inserts the posted array. On success the action calls `sendImage`, which POSTs `ac`, `ref`, and `pic` to `https://www.footprintapp.net/accreditations/application/updateimage`.
6. If `EmailNotes` on the accreditation is not empty, `EmailApplicant` sends that template.
7. If `apply` is greater than 0, each value is written into `AC` and inserted again.
8. The response body is the reference. Code after `exit` (thank-you view, `sleep`, redirect) does not run.

**Output:** Plain-text reference. The browser puts it in `#refnumber` and shows `#thankyou`, then reloads after 5 seconds.

**Validation:** Browser validation is jQuery Validate on fields marked `required` (see business rules). The server insert does not check required fields, duplicate email, terms, or close date again. A POST after close still inserts, because the close check only swaps the view renderer and does not return.

**Errors:** Insert exceptions are printed by `insertData`. Mail exceptions are printed by `sendMail`. The ajax `complete` handler still treats the body as the reference.

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

## FR-004 — Headshot rules in the browser

**Requirement:** The Photo step shows a cropper. Choosing a file larger than 2,097,152 bytes alerts "File size must not be larger than 1.5mb" and does not accept the file. On finish, the cropped data URL is written into `data[User_pic]`.

**Who:** Applicant.

**Trigger:** File input change and wizard `onFinished` in `index/index.phtml`.

**Behaviour:** The textarea starts with class `required`. `getCurrentRole` removes that class when the selected role has `photo = 1`, and adds it otherwise. `required` is a page-level variable that is set to 1 and never set back to 0, so after one photo-flagged role is chosen the field stays optional.

**Validation:** Size check is client-side only. The alert text (1.5 MB) and the threshold (2 MB) do not match.

**Provenance:** **Implemented.** **Requires Confirmation:** whether `photo = 1` was meant to require a photo. The class toggle does the opposite.

## FR-005 — Duplicate email warning

**Requirement:** While typing an email, the page asks `/index/checkemail/email/{email}/ac/{AC}`. If any `AccreditationApplycation` row already has that email and `AC`, the page shows "Email already in use,please use different email address." and clears the off-screen field `#email_comfirm`.

**Who:** Applicant.

**Trigger:** `keyup` on the email input (`check_if_Email_Exist`).

**Inputs:** Email text and `Accreditations_view.AC`.

**Behaviour:** Response is JSON `{"Ok": <count>}`. `checkEmails` selects from `AccreditationApplycation` where `email` and `AC` match, using concatenated SQL. A count above 0 adds class `email_error` and clears `#email_comfirm`. Otherwise it copies the email into `#email_comfirm`.

`#email_comfirm` is `required` and sits in a box positioned off screen (`top: -1000px`). It is not `:hidden`, so jQuery Validate still checks it. On `master`, a duplicate leaves it empty, so the step cannot finish. The server still inserts a duplicate if a client skips the script.

On `qa` and `prod`, commit `d80a39c` also sets `#email_comfirm` to the typed email when a duplicate is found. The warning label still appears, and the hidden confirm field then has a value.

**Provenance:** **Implemented.** The `qa`/`prod` line is **Implemented** on those branches only.

## FR-006 — Sign in and sign out

**Requirement:** Sign-in accepts an email and a reference for the hard-coded accreditation. A match starts session `user_session`. Sign-out clears that session and returns to `/application/login/code/{code}`.

**Who:** Returning applicant.

**Trigger:** Button "Login Now" posts to `/application/loginnow`. Sign-out is `/application/logout`.

**Inputs:** `emailaddress`, `refnumber`, `ac` (the accreditation id, not the code). Whitespace is stripped in the browser before submit.

**Behaviour:** `UserLogin` joins `AccreditationApplycation` to `Accreditation_List` and requires `AC`, `Ref`, and `email` to match. On a row, session `Code` becomes `aCode` and `AAID` becomes the application id. Response JSON is `{"OK":1}` or `{"OK":0}`. The login view then navigates to `/application/profile/code/{hard-coded code}`.

`loginAction` ignores any `code` in the URL and always loads `5F9298188128B`. If the session already has `AAID > 0`, `loginAction` redirects to the profile before rendering the form.

Failed login shows "Bad credentials." Exceptions from `loginnowAction` are printed.

**Output:** JSON, then a browser redirect.

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

## FR-007 — View and update a profile

**Requirement:** A signed-in applicant sees their reference, status, headshot, messages, approved services, zones, and an edit form. Confirmed, declined, and approved applications disable the fields and the photo changer in the browser.

**Who:** Applicant with `user_session.AAID > 0`. Anyone else is redirected to `/application/apply/code/{code}`.

**Trigger:** GET `/application/profile/code/{code}`. Save posts to `/application/update/aaid/{AAID}`. Photo change posts to `/application/updateimage`.

**Behaviour:**

- Profile row comes from `UserProfile($AAID)`. The URL code is replaced by that row's `aCode`.
- Headshot is loaded from `https://www.footprintapp.net/accre/photos/{Ref}.jpg` when that URL returns HTTP 200. Otherwise the image is `/accre/profile-icon.png`.
- Zones come from JSON column `aZones`. Services come from `Accreditation_Applicants_Services` for the `AAID`.
- Status label rules are in `business-rules.md`. `status_locked` is 1 for Status 1, 2, and 3. Pending and in-review stay editable. The in-review lock line is commented out.
- `updateAction` writes the posted `data` array onto that `AAID` and sets `LastUpdate`. It does not read the session and does not read `Status`.
- `updateimageAction` accepts `ac`, `ref`, and `pic` and forwards them to Footprint. It does not read the session.

**Output:** HTML profile, or an empty response from the update actions.

**Errors:** A missing photo URL is treated as "no photo". Curl and database errors are not given a product message.

**Provenance:** **Implemented.** The missing server-side lock is **Implemented** behaviour, not a documented requirement.

## FR-008 — Email the applicant

**Requirement:** When an accreditation row has `EmailNotes`, a successful insert sends that HTML body to `data[email]`. Tokens in the template are replaced.

**Who:** System, after insert.

**Trigger:** `EmailApplicant` from `applyAction` when `EmailNotes` is not empty.

**Tokens:** `[ACCREDITATIONNAME]`, `[CONTACTNAME]`, `[CONTACTTELEPHONE]`, `[CONTACTEMAIL]`, `[REFNUMBER]`, `[APPLICANTNAME]`.

**Behaviour:** `sendMail` always sets the subject to "Kyalami 9 Hour 2020 Notification". A local `$subject` variable ("Application Confirmation - …") is built and not used. Body is HTML.

On `master`, SMTP is `smtp.gmail.com`, port 587, TLS, username `accreditation@kyalami9hour.com`, From "Accreditation - Kyalami 9 Hour 2020". On `qa` and `prod`, username and From address are `accreditation@magus.events` with the same display name. Passwords are hard-coded and are not reproduced.

**Errors:** A send exception is printed into the HTTP response, which the thank-you modal would show as the reference.

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

## FR-009 — Attendance response

**Requirement:** A link with a valid attendance key records that the applicant will attend, or declines the application.

**Who:** Holder of the link. The profile also renders the two links when `AttendanceKey` is not empty.

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

**Behaviour:**

- `att=1`: replace tokens in `EmailAttandanceConfirmed`, send it, set `AttendanceKey` empty, `AttendanceRequest` = 1, `AttendanceResponseDate` and `LastUpdate` to now. Page message: "Thank you for confirming your attendance". Status is not changed.
- `att=2`: set `Status` = 2, `DeclinedDate` now, `History` = "Declined by Applicant", clear the key, `AttendanceRequest` = 2. If `EmailRejected` is not empty, send it with `[REASONNOTE]` = "Applicant confirmed NON-ATTENDANCE / CANCELLATION for this event."
- Unknown key: the view gets an empty `DATA` array.

`testattendanceAction` repeats this and prints the exception. It is a separate URL.

**Errors:** On `att=2`, `attendanceAction` assigns `$message` which is not set on that path.

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

## FR-010 — Closed applications

**Requirement:** After `CloseDate` at 24:00 the wizard is replaced by a closed page. The page shows the banner, the accreditation title, the sentence "Accreditation applications is now closed.", and the contact name and email from the accreditation row.

**Who:** Visitor.

**Trigger:** Close check in `IndexController::indexAction` and `ApplicationController::applyAction`.

**Provenance:** **Implemented.** Sign-in and profile do not run this check.

## FR-011 — Linked events

**Requirement:** When `LinkedAccreditations` has rows for this accreditation with `DateTo` on or after today, the wizard adds an Apply step. Each row is a checkbox labelled `{Acc_Secondary} - {DateFrom} - {Location}`. Ticked values are posted as `apply[]` and stored as additional applications (FR-003).

**Who:** Applicant.

**Provenance:** **Implemented.** **Unknown / Requires Confirmation:** whether checkbox value `OID` is an accreditation id. The code assigns it to `AC`.

## FR-012 — Browser support gate

**Requirement:** The layout shows the portal for Firefox, Safari, Chrome, Flock, and Opera user-agent strings. An empty user agent or a string matching `MSIE` shows an unsupported-browser image with links to Chrome, Firefox, and Safari download pages.

**Who:** Any visitor.

**Trigger:** `get_user_browsers()` in `application/layouts/default.phtml`.

**Behaviour:** The flag is named `$notSupported` and is set true by default. It becomes false only for an empty result or `ie`, and the portal is rendered when the flag is true. Chrome's user agent also matches `Safari`, which is tested first, so Chrome is classified as Safari. Both are allowed, so the page still renders.

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

## FR-013 — Terms

**Requirement:** The Finish step shows a required checkbox "I have read and agree to the Terms and Conditions". The modal body is `TermsAndConditions` from the accreditation row. The checkbox is posted as `data[acceptTerms]` = 1.

**Who:** Applicant.

**Validation:** jQuery Validate `required` on the checkbox. The server does not read `acceptTerms`.

**Provenance:** **Implemented** in the browser. A fixed sentence in the Finish step also says Kyalami 9 Hour 2020 decides access and cites the Safety of Sports and Recreational Events Act of 2010. That sentence is **Documented** in the view, not loaded from the database.

## FR-014 — Supplier module

**Requirement:** None was found for the Kyalami public site.

**Behaviour present in code:** `application/modules/supplier/` contains login, registration, company profile, users, events, and permits. Login compares an MD5 password against `Companies_Profile`. One hard-coded password, not reproduced here, skips that check and loads the stored hash instead (`Supplier_LoginController::indexAction`). Password-reset mail is addressed as SA Rugby Event Administration.

The public header's supplier link is commented out and pointed at `http://supplier.sarugby.online/register`. A note on the organisation step tells a services-company owner to register at that same host when the selected role has `SupplierReg = 1`.

Config names a layout `supplier`. The file is not in `application/layouts/`. `LayoutPicker` would select it for the `supplier` module. `Supplier_LoginController` disables the layout.

**Provenance:** **Implemented** code, **Inferred** not to be the Kyalami applicant journey. **Unknown / Requires Confirmation:** whether `/supplier/...` was ever served on this host.

## Constraints

- Time zone in `public/index.php` and `ApplicationController` is `Africa/Johannesburg`.
- Session cookie lifetime in config is 864000 seconds (10 days).
- Locale in config is `af_ZA`. The views inspected are English. **Inferred:** the locale does not select copy.
- Reference generation depends on `mcrypt`.
- The application does not ship a schema. Tables and views are named in `technical-documentation.md`.
- `master` does not contain a usable `application.ini` in Git (it is gitignored). `public/index.php` requires that file.

## Integrations

| System | Direction | What moves | Provenance |
|---|---|---|---|
| MySQL `teamwizz_SARU` on Footprint DB hosts | Read and write | Accreditation profile, applications, categories, roles, zones, services | **Implemented** |
| `www.footprintapp.net` photo URL and `updateimage` | This app calls Footprint | Headshot bytes and `{ac, ref, pic}` | **Implemented**. No credential is attached to the POST |
| Gmail SMTP | This app sends mail | HTML notification | **Implemented** |
| AWS CodeDeploy / S3 | Pipeline pushes a zip | Whole working tree | **Implemented** on `qa` and `prod` only |
| `supplier.sarugby.online` | Link only | No API call | **Documented** as a link in the view |

Failure behaviour for Footprint and SMTP is to print the PHP exception. There is no retry queue in this repository.
