# PRD — smsa2020

Evidence date: 2026-10-03.

Source: `git@bitbucket.org:overdrive/smsa2020.git`, `master` at `6c61e14` (2020-02-10).

There is no prior PRD in Silicon Ark and no product document in the source repository. Requirements below are recovered from controllers, views, models, the committed `application.ini.bak`, deploy files, and Git history. Provenance is labelled.

Secrets in source (database password in `application.ini.bak`, SMTP password in `ApplicationController::sendMail`, a commented mailbox password in the same method) are not reproduced here.

## Product purpose

**Implemented.** A public web portal where a person applies for accreditation to one event, receives a reference number, signs back in with that reference, and can update the application until it is locked. The event identity is a hardcoded code, not a catalogue the visitor chooses.

**Documented** only by the HTML title and the terms sentence: "Vodacom Super Hero Sunday (2020) - Accreditation Portal", and by the repository name `smsa2020` plus commit `6c61e14` ("Changed for SMSA"). Those two names are not reconciled in the tree. See project plan.

The terms sentence says the pass holder accepts that Vodacom Super Hero Sunday 2020 decides the level of access, and that parties comply with stadium rules and the Safety of Sports and Recreational Events Act of 2010. That sentence is **Implemented** copy. It is not evidence that the Act is enforced in code.

## Users and interactions

| Actor | What they can do in this repository | Provenance |
|---|---|---|
| Applicant, not signed in | Open `/`, complete the wizard, submit, see a reference number | **Implemented** |
| Applicant with email and reference | Sign in at `/application/login`, open the profile | **Implemented** |
| Signed-in applicant | View status, messages, services, zones; edit fields while unlocked; replace the photo while unlocked; follow attendance links when a key is present | **Implemented** |
| Person who opens an attendance link | Confirm (`att/1`) or decline (`att/2`) without the session check used by login | **Implemented** |
| Event administrator | No screen in this repository. Status values Confirmed and Approved are displayed. Nothing here sets them except decline, which sets Declined | **Inferred** that approval happens elsewhere |
| Deploy operator | Push to `qa` or `prod` runs Bitbucket Pipelines CodeDeploy | **Implemented** pipeline |
| Browser that the layout classifies as IE, or that it does not recognise | Sees the "browser not supported" panel with Chrome and Firefox links | **Implemented** |

No user, role, or password table is used by the login action. Login matches a row in `AccreditationApplycation` on accreditation id, email, and `Ref`.

## Systems the product talks to

1. The applicant's browser.
2. MySQL. The backup ini names host `db.footprintapp.net`, database `teamwizz_SARU`, user `teamwizz_ADMIN`. The password is in that file and is omitted here. The live `application.ini` is gitignored, so this is the backup, not proof of the running server.
3. `smtp.gmail.com`, port 587, TLS, username `accreditation@magus.events`. From-name "Accreditation - Vodacom Super Hero Sunday 2020". Subject is always "Vodacom Super Hero Sunday 2020 Notification".
4. `https://www.footprintapp.net/accreditations/application/updateimage` — POST of `ac`, `ref`, and `pic` after a successful apply (`sendImage`).
5. `https://www.footprintapp.net/accre/photos/{Ref}.jpg` — profile photo read-back when that URL returns HTTP 200.
6. `http://supplier.sarugby.online` — link in a note, shown when the selected role option has attribute `reg` equal to 1. The portal does not call that site.
7. AWS S3 and CodeDeploy, from `qa` and `prod` pipelines only. `master` has no pipeline step.

Commented code still names `mail.footprintapp.net` and `accreditation@footprintapp.net`. Those lines are not executed.

## Major workflows

### WF-1 — Open the application form

`GET /` is `IndexController::indexAction`.

The action sets `$Code` to `5E37C335A16AA` (commit `6c61e14`; the previous literal was `5DEB6F11DDA93`).

It loads `Model_AccreditationList::getProfile`, which selects from view `Accreditations_view` where `aCode` equals that value.

If the row has no `AC`, the action prints "Sorry this page does not exits" and exits.

It loads linked accreditations (`LinkedAccreditations` where `AID` matches and `DateTo` is today or later), public categories, and roles.

If `CloseDate` plus the string ` 24:00:00` is not after the current time, the action renders `application/closed.phtml` instead of the form.

The form in `index/index.phtml` is `display: none` until the jQuery Steps wizard runs. A "Loading page" block is shown first. The form `action` is `/application/apply/code/{code}`, so submit is handled by `ApplicationController::applyAction`, not by the POST branch inside `IndexController::indexAction`.

Provenance: **Implemented**.

### WF-2 — Submit an application

`POST /application/apply/code/{code}`.

`applyAction` calls `isloggedIN` first. If session `user_session->AAID` is greater than 0, the user is redirected to `/application/profile/code/{session Code}` and the post is not saved.

Otherwise it resolves the event from the `code` route parameter (the form sends the same hardcoded code). Missing `AC` exits with the same warning text. If the event is past `CloseDate`, the action selects the closed view and then continues. A GET therefore shows the closed page. A POST still inserts. See FR-014.

On POST:

1. Reference is `strtoupper` of `bin2hex(mcrypt_create_iv(3, MCRYPT_DEV_URANDOM))` — 6 hex characters.
2. Posted `data[...]` is stored with `AC`, `TID`, and `FID` from the event profile, `DateApply` as now, `Ref`, and `History` = `None`.
3. If `User_pic` is an empty string, it is replaced with a hardcoded JPEG data-URI placeholder. The image bytes are not copied into this document.
4. If `DaysAttending` is absent, it is stored as an empty string. The current wizard in `index/index.phtml` has no days field.
5. The row is inserted into table `AccreditationApplycation` (`Model_AccreditationsApplications`).
6. On insert, `sendImage` POSTs the picture to footprintapp.net. `SavePhoto` is commented out on this path.
7. If the event profile's `EmailNotes` is non-empty, `EmailApplicant` fills that template and calls `sendMail`.
8. If posted `apply` compares greater than 0, each value is inserted again with `AC` replaced by that value. The public form sends `apply[]` checkboxes whose values are `OID` from linked rows. Whether an array satisfies `> 0` depends on the PHP version. **Requires confirmation** against the server's PHP.
9. The action echoes the reference and exits. The thank-you view and redirect that follow `exit` in the same method do not run.

The browser (`index/index.phtml` ajax submit) puts the response text into the thank-you modal as the reference, then reloads after 5 seconds. The modal tells the person to check their inbox and quotes `ContactTelephone` from the event row.

Provenance: **Implemented**.

### WF-3 — Sign in and open the profile

`GET /application/login` uses the same hardcoded code `5E37C335A16AA`. It ignores a `code` parameter. The layout link is `/application/login` with label "Sign-In".

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

Whitespace is stripped in the browser before the check for empty fields. The posted values are the originals, not the stripped copies. **Implemented.**

`GET /application/profile/code/{code}` requires `AAID > 0`. Otherwise it redirects to `/application/apply/code/{code}`.

The profile loads the application, the event, categories, roles, services (`getProfileServices`), and zones. Zones come from JSON in `aZones`. If that JSON is empty, `count($zones)` on a non-array is the behaviour of the installed PHP; the code does not guard it.

The photo is the footprint URL `{Ref}.jpg` when `remoteFileExists` gets HTTP 200, else `/accre/profile-icon.png`.

Provenance: **Implemented**.

### WF-4 — Update a profile

While the status is not locked, the profile form posts to `/application/update/aaid/{AAID}`. `updateAction` writes the posted `data` plus `LastUpdate` and does not check the session.

Photo change posts to `/application/updateimage` with `ac`, `ref`, and `pic`. That action also does not check the session. It forwards the picture with `sendImage`.

Locked statuses disable the inputs in the view (`disabled='true'`). The server does not reject an update for a locked row.

Provenance: **Implemented**.

### WF-5 — Attendance

If `AttendanceKey` is non-empty, the profile shows two links:

- `/application/attendance/key/{key}/att/1` — "Yes I am attending"
- `/application/attendance/key/{key}/att/2` — "No I am not attending"

`attendanceAction` loads the application with `getByAttandanceID`.

`att` 1: fills template `EmailAttandanceConfirmed`, sends it, sets `AttendanceKey` empty, `AttendanceRequest` 1, `AttendanceResponseDate` and `LastUpdate` to now. View message: "Thank you for confirming your attendance".

`att` 2: if `EmailRejected` is non-empty, sends the declined template with reason "Applicant confirmed NON-ATTENDANCE / CANCELLATION for this event." Updates `AttendanceRequest` 2, `Status` 2, `DeclinedDate`, `History` "Declined by Applicant", clears `AttendanceKey`. The view assigns `$message`, which is not set in this branch.

`testattendanceAction` repeats this with a try/catch that prints the exception. It is a separate route.

Provenance: **Implemented**.

### WF-6 — Closed event

`application/closed.phtml` heading: "Accreditation applications is now closed." It mailto-links `ContactName` and `ContactEmail` from the event row.

Provenance: **Implemented**.

## Functional requirements

### FR-001 — Single event on the public home

The home page and the sign-in page load event code `5E37C335A16AA`.

Trigger: `GET /` or `GET /application/login`.

Inputs: none from the visitor.

Behaviour: load `Accreditations_view` by `aCode`. Missing row: warning text and `exit`.

Output: wizard, or the warning.

Validation: none on the code; it is a literal.

Error: missing profile exits before the layout finishes the page.

Evidence: `IndexController::indexAction`, `ApplicationController::loginAction`.

Implementation: **Implemented**. Confidence: high.

### FR-002 — Wizard fields

The wizard steps are Personal, Organisation, Photo, Finish. An Apply step is inserted before Finish when at least one linked accreditation is returned.

| Field | Posted name | Browser rule |
|---|---|---|
| First name | `data[FirstName]` | class `required` |
| Surname | `data[Surname]` | class `required` |
| Email | `data[email]` | class `email required`; duplicate check below |
| Hidden confirm | `#email_comfirm` (no `name`) | class `email required`; filled only when the email is accepted |
| Mobile | `data[Mobile]` | class `required` |
| ID or passport | `data[IDnumber]` | class `required` |
| Organisation | `data[organisation]` | class `required` |
| Category | `data[Category]` | options from public categories; class `required` |
| Role | `data[Role]` | options filtered in the browser to the selected category's `ACID` |
| Producer name | `data[Producer_Fullname]` | required when the media block is shown |
| Producer email | `data[Producer_Email]` | required when the media block is shown |
| Reason | `data[Reason_for_pplication]` | class `required`. The label says accreditation approval and zone access depend on the description |
| Photo | `data[User_pic]` | class `required` unless the selected role has `photo == 1`, in which case the class is removed |
| Linked events | `apply[]` | optional checkboxes |
| Event note | `data[acceptnotes]` | checkbox only if `AccreditationNote` is non-empty; not class `required` |
| Terms | `data[acceptTerms]` | class `required`; value 1 |

The media block (`#MediaOnly`) is shown when the event's `MediaOnly` is 1, or, when `MediaOnly` is not greater than 0, when the selected category value equals `1`. Producer fields are then required. The invitation radios (Openfield / Roth Media) are inside an HTML comment and are not submitted.

Provenance: **Implemented** in `index/index.phtml`. Server-side Zend validators are not used. A client can post without the browser rules.

### FR-003 — One email per event, in the browser

`check_if_Email_Exist` GETs `/index/checkemail/email/{email}/ac/{AC}`.

`IndexController::checkemailAction` counts rows in `AccreditationApplycation` for that email and `AC`. Response `{"Ok": count}`.

If `Ok` is greater than 0, the view shows "Email already in use,please use different email address." and clears `#email_comfirm`. If `Ok` is 0, it copies the email into `#email_comfirm`.

The duplicate check is not repeated in `insertData`. Two submits can store the same email.

Provenance: **Implemented**.

### FR-004 — Photo file size message

The file input `accept` is `image/*`. If `size > 2097152`, the script alerts "File size must not be larger than 1.5mb" and returns. 2097152 bytes is 2 MiB. The help text also says "no larger than 1.5mb".

Provenance: **Implemented**. The 1.5 versus 2 mismatch is in the source.

### FR-005 — Role photo flag

`getCurrentRole` sets the photo textarea as not required when `AccreditationRoles_view.photo` equals 1, and required otherwise.

What `photo = 1` means in the database is **Unknown / Requires Confirmation**. The code behaviour is **Implemented**.

### FR-006 — Supplier registration note

When the selected role's `reg` attribute is 1, the page shows a note that an owner, director, or manager who needs the load-in/out schedule must register the company with SA Rugby at `http://supplier.sarugby.online`.

Provenance: **Implemented** copy. No registration call.

### FR-007 — Terms

The acceptance paragraph names Vodacom Super Hero Sunday 2020 and the Safety of Sports and Recreational Events Act of 2010.

The modal body is `$this->DATA["TermsAndConditions"]` from `Accreditations_view`. The text of those terms is data, not in Git.

`acceptTerms` must be checked for the browser validator. The server stores whatever was posted, including a missing checkbox.

Provenance: **Implemented**.

### FR-008 — Reference number

Generated as 3 random bytes, hex-encoded, uppercased. Uniqueness against existing `Ref` values is not checked before insert.

Provenance: **Implemented**.

### FR-009 — Confirmation mail

Sent only when `EmailNotes` on the event profile is non-empty.

Placeholders replaced: `[ACCREDITATIONNAME]`, `[CONTACTNAME]`, `[CONTACTTELEPHONE]`, `[CONTACTEMAIL]`, `[REFNUMBER]`, `[APPLICANTNAME]`.

`EmailApplicant` builds a subject "Application Confirmation - {title}" and does not pass it to `sendMail`. `sendMail` always sets the subject "Vodacom Super Hero Sunday 2020 Notification".

Mail failure is printed with `print_r` inside the catch. The apply action still echoes the reference.

Provenance: **Implemented**.

### FR-010 — Sign-in

Match on accreditation id, email, and reference. No password hash. The reference field is `type="password"` in the login view.

Session keys: `Code`, `AAID`. Cookie lifetime in the backup ini is `864000` seconds (10 days). **Inferred** duration from that integer.

Logout unsets `user_session` and redirects to `/application/login/code/{Code}`. `loginAction` then ignores that code and loads `5E37C335A16AA`.

Provenance: **Implemented**.

### FR-011 — Profile status labels

Evaluated in `profile.phtml` in this order:

| Condition | Label | Fields locked in the view |
|---|---|---|
| `Pending == 1` | Pending | no |
| else `Status == 0` and `DateApply < LastUpdate` | In Review | no (the lock line is commented out) |
| else `Status == 1` | Confirmed | yes |
| else `Status == 2` | Declined | yes |
| else `Status == 3` | Approved | yes |
| else | New Application | no |

Pending also shows heading "Requirement" and `RejectMessage`.

Declined shows "Application Declined" and `DeclinedMessage`.

"Service Approved" lists `Title` from the applicant's services, or "None".

"Zones Approved" lists `Number`. `Name`, or "None".

Provenance: **Implemented** labels. Who sets `Pending`, `Status` 1, and `Status` 3 is not in this repository.

### FR-012 — Profile edit while unlocked

Editable: first name, surname, email, mobile, ID number, organisation, category, role. Photo replace while unlocked.

Save posts to `/application/update/aaid/{AAID}`.

Provenance: **Implemented** view and action. Server does not test the lock.

### FR-013 — Attendance decision

See WF-5. Decline sets `Status` 2 and history "Declined by Applicant". Confirm does not change `Status`.

Provenance: **Implemented**.

### FR-014 — Closed applications

`IndexController::indexAction` and `ApplicationController::applyAction` compute close as `strtotime(CloseDate . " 24:00:00")` minus the current timestamp. If that difference is less than 1, the action sets the view script to `application/closed.phtml`. The method does not return.

On GET, that script is what the browser receives: the closed heading and the event contact email.

On POST to `applyAction`, the `isPost` block then calls `setNoRender`, inserts the row, echoes the reference, and exits. The close date changes the GET page. It does not refuse the insert.

Provenance: **Implemented**.

### FR-015 — Browser gate

`default.phtml` treats the user agent as unsupported when `get_user_browsers()` returns an empty string or `ie`. Chrome, Firefox, Safari, Opera, and Flock render the portal. The Safari test runs before the Chrome test. Chrome's user agent contains "Safari", so Chrome is labelled safari. Both labels are in the supported set, so the portal still renders.

Unsupported browsers see a panel linking to Google Chrome and Mozilla Firefox.

Provenance: **Implemented**.

### FR-016 — Event title line

`getAccreditationName` builds the heading from `AccreditationName`, then dates and location.

If `DateFrom` and `DateTo` are both set and equal, one date `j M Y`. If they differ, a range. Location is `Location`, or else `FixtureLocation`.

If `FID` is greater than 0, the heading is name, `SquadName1 VS SquadName2`, fixture date, and location. That branch overwrites the date-range heading.

Provenance: **Implemented**.

## Integrations

| System | Direction | Trigger | Data | Auth | Failure |
|---|---|---|---|---|---|
| MySQL `teamwizz_SARU` (backup ini) | App reads and writes | Every page and submit | Event view, application row, categories, roles, services, zones | Username and password in ini | `insertData` prints the exception and returns false. Apply then does not echo a reference |
| footprintapp.net updateimage | Outbound POST | Successful apply, and profile photo update | `ac`, `ref`, `pic` | None in the curl call | Return value is ignored |
| footprintapp.net photo URL | Outbound GET (HEAD via CURLOPT_NOBODY) | Profile view | `{Ref}.jpg` | None | Non-200 uses the local placeholder |
| Gmail SMTP | Outbound | Apply when `EmailNotes` set; attendance templates | HTML body | Login, TLS 587 | Exception printed; apply still returns the reference |
| supplier.sarugby.online | Link only | Role `reg == 1` | None | n/a | n/a |
| AWS CodeDeploy | Pipeline to AWS | Push to `qa` or `prod` | Zip of the tree | Bitbucket variables | Script exits 1 if S3 upload or deployment does not succeed |

## Important constraints

- One hardcoded event on `/` and on `/application/login`. Other events are reachable only if the caller knows `/application/apply/code/{aCode}`.
- No administrator UI in this repository.
- Client validation is not repeated on the server.
- `display_errors` is on in `public/index.php` and in the production section of `application.ini.bak`.
- Queries concatenate request values into SQL.
- Reference numbers are the sign-in secret. They are shown in the thank-you modal and emailed.
- `application.ini` is not in Git. Behaviour that depends on it is known only from the `.bak` file.

## Requirements that are assumptions

These are **Inferred**, not written as product rules:

- SMSA is the customer or event behind code `5E37C335A16AA`.
- Approval, zone assignment, and service assignment are done in another application that shares `teamwizz_SARU`.
- The deploy path `superhero2020.magus.events` is still the host for this code.

Each of those needs human confirmation.
