# PRD — SuperHero2020

Evidence date: 2026-10-02. Source: `git@bitbucket.org:overdrive/superhero2020.git`, `master` at `ab4effc` (2020-02-07, "Merge branch 'prod'").

There is no prior PRD in the repository or in Silicon Ark. Every requirement below was recovered from the code, views, and deployment files. Provenance is marked on each item. Passwords found in source are not copied here.

## Product purpose

An applicant uses a browser to request accreditation for Vodacom Super Hero Sunday 2020, receive a reference number, return to edit the application, upload a photo, and confirm or cancel attendance.

Provenance: **Implemented**. A written product statement was not found. The page title is "Vodacom Super Hero Sunday (2020) - Accreditation Portal".

## Users and roles

| Role | What they do | Evidence | Provenance |
|---|---|---|---|
| Applicant | Completes the wizard, signs in with email and reference number, edits an unlocked profile, confirms attendance | `ApplicationController`, `apply.phtml`, `login.phtml`, `profile.phtml` | Implemented |
| Anonymous visitor | Opens `/`. The homepage loads one hardcoded accreditation code and shows the wizard | `IndexController::indexAction` | Implemented |
| Event contact | Named on the closed page and the thank-you copy. Address and phone come from the accreditation row (`ContactName`, `ContactEmail`, `ContactTelephone`) | `closed.phtml`, `thankyou.phtml` | Implemented |
| Staff / accreditation administrator | Not given a login in this repository. `Accreditation_Administrators` is modelled only | `AccreditationAdministrators.php` | Unknown. **Inferred** that staff use Footprint, not this site |

There is no password for applicants. The secret is the reference number issued at submit time. The login form renders that field as `type="password"`.

## Main workflows

### W1 — Open the application form

Trigger: browser opens `/` (`IndexController::indexAction`) or `/application/apply/code/{code}` (`applyAction`).

The homepage does not read `code` from the URL. It loads accreditation public code `5DEB6F11DDA93` from `Accreditation_List` via view `Accreditations_view` (`Model_AccreditationList::getProfile`). `/application/login` uses the same hardcoded code. `/application/apply` uses the `code` parameter.

If the row has no `AC`, the response is HTML: "Sorry this page does not exits", then `exit`.

If `CloseDate` plus `24:00:00` is not after the current Africa/Johannesburg time, the closed view is rendered.

Otherwise the view receives the profile, public categories (`Teams = 0` and `hidden = 0`), live roles, and linked accreditations whose `DateTo` is today or later.

Provenance: **Implemented**.

The homepage view `index/index.phtml` posts to `/application/apply/code/{code}`. The apply view `application/apply.phtml` posts to `/accreditations/application/apply/code/{code}`. This repository has no `accreditations` module. Whether a front server rewrites that path is **Unknown / Requires Confirmation**. As checked in, a submit from `/application/apply` would not hit `applyAction`.

### W2 — Submit an application

Trigger: POST of `data[...]` to the apply action. `IndexController::indexAction` contains a similar POST block, but the homepage form's `action` is `/application/apply/...`, so the POST is handled by `ApplicationController::applyAction`. The index POST block also calls `random_id`, `SavePhoto`, and `EmailApplicant`, which are not methods of `IndexController`. A POST that did reach `IndexController` would fatal. Provenance: **Implemented** (dead relative to the form action).

Behaviour of `applyAction` on POST:

1. Build a reference with `strtoupper(bin2hex(mcrypt_create_iv(3, MCRYPT_DEV_URANDOM)))` — 6 hex characters.
2. Set `AC`, `TID`, and `FID` from the accreditation profile, `DateApply` to now, `History` to `None`, `Ref` to that reference.
3. If `DaysAttending` is missing, store an empty string.
4. If `User_pic` is empty, store a built-in JPEG data-URI placeholder.
5. Insert the row into `AccreditationApplycation`. Local `SavePhoto` is commented out.
6. On insert, POST `ac`, `ref`, and `pic` to `https://www.footprintapp.net/accreditations/application/updateimage`.
7. If the profile `EmailNotes` is not empty, send that template to `data[email]`.
8. If `apply` is posted and compares as greater than 0, insert the same payload again for each value, changing only `AC`. The form posts `apply[]` checkboxes of linked event ids (`OID`).
9. Echo the reference and `exit`.

The thank-you view render and the redirect after `exit` never run. The browser script (`ajaxForm` complete handler) puts `xhr.responseText` into `#refnumber`, shows `#thankyou`, and reloads after 5 seconds.

Provenance: **Implemented**.

`applymultipleAction` echoes the reference before the insert, does not call `sendImage`, and on success renders `thankyou` without `exit`. It does not call `isloggedIN`. `applytestAction` matches `applyAction` except it calls `SavePhoto` instead of `sendImage`.

### W3 — Sign in and open the profile

Trigger: Sign-In in the layout goes to `/application/login`. If session `user_session.AAID` is already greater than 0, `isloggedIN` redirects to `/application/profile/code/{session code}`.

The login form posts JSON to `/application/loginnow/ac/{AC}` with `emailaddress` and `refnumber`. `UserLogin` selects `AccreditationApplycation` joined to `Accreditation_List` where `AC`, `Ref`, and `email` all match. On a row, the session stores `Code` (`aCode` from the list) and `AAID`. Response `{"OK":1}`. Otherwise `{"OK":0}` and the page shows "Bad credentials."

`profileAction` requires `AAID > 0`. Otherwise it redirects to `/application/apply/code/{code}`. It loads the application, the event, public categories, live roles, services (`getProfileServices`), and zones decoded from `aZones`. The portrait is fetched from `https://www.footprintapp.net/accre/photos/{Ref}.jpg` when that URL returns HTTP 200. Otherwise the placeholder is `/accre/profile-icon.png`.

Logout clears `user_session` and redirects to `/application/login/code/{previous code}`. The login action then ignores that code and loads `5DEB6F11DDA93` again.

Session cookie lifetime in the ini backup is `864000` seconds (10 days).

Provenance: **Implemented**.

### W4 — Edit the profile

`profile.phtml` allows editing when `status_locked` is 0. Locked statuses are Confirmed (`Status == 1`), Declined (`Status == 2`), and Approved (`Status == 3`). Pending (`Pending == 1`) and In Review stay unlocked. The In Review lock assignment is commented out.

Save posts to `/application/update/aaid/{AAID}` with `data`. `updateAction` writes those fields and sets `LastUpdate` with `date("Y-m-d H:s:i")` (minutes and seconds are swapped in that format string). It does not check the session.

A new photo posts to `/application/updateimage`, which forwards the image to Footprint the same way as apply. It does not check the session. The call to `SavePhoto` is commented out.

Provenance: **Implemented**.

### W5 — Confirm or cancel attendance

Trigger: `/application/attendance/key/{AttendanceKey}/att/{1|2}`. Lookup is `AccreditationApplycation.AttendanceKey`.

- `att=1`: send `EmailAttandanceConfirmed` after placeholder replacement, set `AttendanceRequest` to 1, set `AttendanceResponseDate` and `LastUpdate`, clear `AttendanceKey`. Message: "Thank you for confirming your attendance".
- `att=2`: if `EmailRejected` is not empty, send the decline mail with reason "Applicant confirmed NON-ATTENDANCE / CANCELLATION for this event." Set `Status` to 2, `History` to "Declined by Applicant", `DeclinedDate`, clear `AttendanceKey`.
- Unknown or already-cleared key: "Please note: this link does not exist or has been used already".

`testattendanceAction` repeats this with a try/catch that prints the exception. The decline branch of `attendanceAction` assigns `$message`, which is not set in that action.

Provenance: **Implemented**. Profile links to these URLs only when rendering the attendance choice (`profile.phtml`).

### W6 — Application closed

Trigger: close timestamp is not in the future (`applyAction`, `applymultipleAction`, `applytestAction`, `IndexController::indexAction`).

View text: "Accreditation applications is now closed." Queries go to `ContactName` and `ContactEmail` on the event row.

Provenance: **Implemented**. An empty `CloseDate` makes `strtotime` run on `" 24:00:00"`. The resulting close behaviour is **Unknown**.

## Functional requirements

### FR-001 — Event identity on the form

The form shows `AccreBanner` and a title built by `getAccreditationName`.

- If `AccreditationName` is set, that name is used.
- If `FID > 0`, the title includes `SquadName1 VS SquadName2`, the fixture date, and location.
- Otherwise, if `DateFrom` and `DateTo` are set, a single day or a range is shown.
- Location is `Location`, or `FixtureLocation` when `Location` is empty.

Provenance: **Implemented**.

### FR-002 — Required applicant fields

The wizard marks these required in the browser (`class="required"` plus jQuery Validate):

- First name, surname, email, mobile, ID or passport number (`data[FirstName]`, `data[Surname]`, `data[email]`, `data[Mobile]`, `data[IDnumber]`).
- Organisation, category, role (`data[organisation]`, `data[Category]`, `data[Role]`).
- Reason for application (`data[Reason_for_pplication]` — the column name is spelled that way in the form).
- Terms checkbox `data[acceptTerms]` must be 1. The acceptance sentence names SA Rugby's Tournament and Competitions Accreditations Policy and the Safety at Sports and Recreational Events Act. The popup body is `TermsAndConditions` from the event row.
- If `AccreditationNote` is not empty, an extra checkbox `data[acceptnotes]` is shown. It is not given the `required` class.

The server insert does not re-validate these fields. Provenance: **Implemented** in the browser. Server-side validation: absent.

A second email input `email_comfirm` is positioned off-screen. The duplicate-email script copies the address into it only when the check returns `Ok == 0`.

### FR-003 — Categories and roles

Categories on the public form come from `AccreditationCategory_view` where `AID` is the event, `Teams = 0`, and `hidden = 0`, ordered by category name. Option value is `ACID`.

Roles come from `AccreditationRoles_view` for that `AID`, ordered by role name. Option value is `ARID`. Changing category keeps only roles whose `ACID` matches.

Provenance: **Implemented**.

### FR-004 — One email per event

On each keyup of a valid email, the apply view calls `GET /accreditations/application/checkemail/email/{email}/ac/{AC}`. The index view calls `GET /index/checkemail/...`. The profile view calls `GET /application/checkemail/...`.

`checkemailAction` on both `IndexController` and `ApplicationController` counts `AccreditationApplycation` rows with that email and `AC`, and returns `{"Ok": count}`. If `Ok > 0`, the label is "Email already in use,please use different email address."

The check is not repeated in `insertData`. Provenance: **Implemented** in the browser against whichever URL the view calls. The apply view's URL is not a route in this repository (see W1).

### FR-005 — Media and producer fields

The media block (days attending, editor/producer name, editor/producer email) is shown when event `MediaOnly == 1`.

When `MediaOnly` is not greater than 0, choosing category id `1` shows the same block and adds the `required` class. Other categories hide it.

Day checkboxes are rendered only when `DaysDuration > 1`. Values are `1..DaysDuration`. They are copied into `data[DaysAttending]` by script (`countDaysSelected`). The exact join format of that string was not fully traced. Provenance: **Implemented** for showing the fields. The stored day format is **Inferred** from the checkbox values and was not confirmed against a sample row.

### FR-006 — Photo

The applicant crops an image in the browser (Cropper). Files larger than 1.5 MB are rejected in script with "File size must not be larger than 1.5mb".

`getCurrentRole` removes the `required` class from the photo field when the selected role has `photo == 1`, and adds it otherwise. The flag is not reset at the start of the function, so after a `photo == 1` role has been selected once, later roles keep the photo optional. Whether `photo == 1` is meant to mean "photo not required" is **Unknown / Requires Confirmation**.

`applyAction` stores the data-URI on the row and also POSTs it to Footprint. `SavePhoto` (used by `applytestAction` and defined for a local write) resizes to 500×500 and writes `{AC_PATH}/photos/{Ref}.jpg`. `AC_PATH` is not defined in this repository. Provenance: **Implemented** call. The constant's runtime value is **Unknown / Requires Confirmation**.

### FR-007 — Linked events

When linked rows exist with `DateTo >= today`, an extra wizard step lists them as checkboxes `apply[]` valued with `OID`, labelled `Acc_Secondary` and the date. Copy: "Please select one or multiple events that you would like to apply for". Each selected id causes another insert with that `AC`.

`applymultipleAction` loads links without the date filter.

Provenance: **Implemented**.

### FR-008 — Supplier registration notice

When the selected role has `SupplierReg == 1` (`reg` attribute `1`), the form shows: the owner, director, or manager of a services company who needs the load-in/out schedule before event day must register the company with SA Rugby at `http://supplier.sarugby.online`.

The notice does not block submit. Provenance: **Implemented**.

### FR-009 — Email templates

`EmailApplicant` sends `EmailNotes` when that column is not empty. `EmailApplicantDeclined` sends `EmailRejected`. Attendance confirmation sends `EmailAttandanceConfirmed`.

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

`sendMail` always sets the subject to "Vodacom Super Hero Sunday 2020 Notification" and the from-address to `accreditation@magus.events`. A computed subject argument inside `EmailApplicant` is not passed into `sendMail`. Transport is `smtp.gmail.com` port 587 with TLS. A previous Footprint mailbox is commented out. The SMTP password is in source and is not recorded here.

On SMTP failure the exception is printed. Provenance: **Implemented**.

`testAction` sends a fixed message to `andre.ferraz@evolveav.co.za`. Provenance: **Implemented** test action, not an applicant workflow.

### FR-010 — Application status shown to the applicant

`profile.phtml` labels:

| Condition | Label | Form |
|---|---|---|
| `Pending == 1` | Pending | Unlocked |
| else `Status == 0` and `DateApply < LastUpdate` | In Review | Unlocked (`status_locked = 1` is commented out) |
| else `Status == 1` | Confirmed | Locked |
| else `Status == 2` | Declined | Locked |
| else `Status == 3` | Approved | Locked |
| else | New Application | Unlocked |

`getApproved` and `getAllRef` treat `Status = 3` as approved. Non-attendance sets `Status` to 2. Who sets Pending, 0, 1, and 3 is not in this repository. Provenance: **Implemented** labels. The writers of those values, other than the attendance decline path, are **Unknown / Requires Confirmation**.

### FR-011 — Profile services and zones

The profile loads `Accreditation_Applicants_Services` for the `AAID` and zones from JSON `aZones` via `AccreditationsZones`. Provenance: **Implemented** as read-only display. Creating services or zones is not in this repository.

## Integrations

| System | Direction | Trigger | Data | Failure |
|---|---|---|---|---|
| MySQL `teamwizz_SARU` on `db.footprintapp.net` | App reads and writes | Every page that loads an event or saves an application | Accreditation tables and Footprint/SARU tables named in the technical document | Models `print_r` the exception and often `exit` or return false. No user-facing retry |
| `www.footprintapp.net` photo update | Outbound POST | Successful `applyAction` insert; profile image update | `ac`, `ref`, `pic` | curl result is not checked. Timeout is set to 86400 seconds |
| `www.footprintapp.net` photo read | Outbound HEAD then GET | Profile render | `{Ref}.jpg` | Missing file falls back to `/accre/profile-icon.png` |
| Gmail SMTP | Outbound | Application email, decline email, attendance email, `testAction` | HTML body | Exception printed to the response |
| `supplier.sarugby.online` | Browser link only | Role `SupplierReg == 1` | None from this app | Not handled |

Authentication to MySQL and SMTP is a username and password in the repo (ini backup and `sendMail`). Values are not copied into this PRD. Whether those credentials are still valid is **Unknown / Requires Confirmation**.

## Constraints

- Time zone is `Africa/Johannesburg` in `public/index.php` and `ApplicationController`.
- Default locale in the ini backup is `af_ZA`. The views inspected are English. Whether Zend translates any string is **Unknown**. No translation files were found in the application tree.
- `public/index.php` turns `display_errors` on. The production section of `application.ini.bak` also sets `display_errors = 1` and `displayExceptions = 1`. The development and testing sections turn them off.
- Reference generation depends on `mcrypt`.
- Applicant routes that change data (`update`, `updateimage`, `loginnow`, `checkemail`, `attendance`) are not shown with a CSRF token or a staff permission check.

## Error conditions

| Case | Behaviour | Provenance |
|---|---|---|
| Unknown accreditation code | "Sorry this page does not exits" and `exit` | Implemented |
| Applications closed | Closed view. Contact email from the event row | Implemented |
| Duplicate email | Browser warning. Insert still possible if the check URL is skipped or fails | Implemented client-side |
| Bad login | `OK: 0`, "Bad credentials." | Implemented |
| Profile without a session | Redirect to `/application/apply/code/{code}` | Implemented |
| Attendance key missing or already used | "this link does not exist or has been used already" | Implemented |
| Unknown controller or action | HTTP 404, "Page not found" | Implemented (`ErrorController`) |
| Other exception | HTTP 500, "Application error". Exception object is shown when `displayExceptions` is true | Implemented |
| Insert or mail exception | Printed with `print_r` on several paths | Implemented |

## Requirements that are assumptions

- That this site and the Footprint accreditation module are meant to share `teamwizz_SARU`, and that staff approval happens in Footprint. **Inferred** from the shared table names and the photo URL. Not written down as a requirement.
- That category id `1` means media. **Inferred** from the script that reveals media fields for that id.
- That the product is limited to the 2020 Super Hero event. The mail subject and the page title say that. The apply action itself is generic and loads any `aCode`. **Requires confirmation** if the same code was reused for other events.
