# PRD — Sunmet2020

Evidence date: 2026-10-02.

Source: `git@bitbucket.org:overdrive/sunmet2020.git`, `master` at `10caee0` (2020-08-04). Behaviour that differs on `prod` (`afeb66d`, 2020-01-13) is the config filename only, called out in the project plan.

There is no prior PRD in the source repository or in `silicon_ark`. Requirements below are recovered from the implementation. Provenance is labelled. Passwords and SMTP secrets present in the source are not reproduced.

## Product purpose

**Implemented.** A person opens The Sun Met (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 `5DA997A51439E` 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 `5DA997A51439E`, 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 | `apply.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 | **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.

## FR-001 — Open the event application

**Requirement:** A visitor who opens `/` sees the accreditation named by code `5DA997A51439E`: 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 live roles. 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 ini section.

**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 or broken 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.

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

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 `apply.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[Producer_Fullname]`, `data[Producer_Email]`, `data[DaysAttending]` | Only while the media block is visible and those inputs carry `required`. See business rules |
| `data[User_pic]` | A cropped image data-URI. The `required` class is removed when the selected role's `photo` flag is `1` |
| `data[acceptTerms]` | Yes, value `1`. Copy cites SA Rugby Tournament and Competitions Accreditations Policy and the Safety at Sports and Recreational Events Act of 2009 |
| `data[acceptnotes]` | Shown only when `AccreditationNote` is non-empty. Not marked `required` |
| `apply[]` | Optional checkboxes for linked events (`LinkedAccreditations.OID`), shown when at least one link has `DateTo` today or later |

**Server behaviour:**

1. Build a 6-character uppercase hex reference from 3 random bytes (`mcrypt_create_iv`).
2. Set `AC`, `TID`, and `FID` from the accreditation profile, `DateApply` to now (`Africa/Johannesburg`), `History` to `None`, and `DaysAttending` to an empty string when absent.
3. Insert the `data` array into table `AccreditationApplycation`.
4. POST `ac`, `ref`, and `pic` to `https://www.footprintapp.net/accreditations/application/updateimage`.
5. If `EmailNotes` on the accreditation profile is non-empty, send that template to the applicant (FR-008).
6. If `apply` is non-empty, insert the same payload again for each selected id, replacing `AC` with that id. The reference stays the same. The photo POST and the email are not repeated inside the loop.
7. Echo the reference and `exit`. The thank-you view and redirect after that `exit` never run.

**Output:** Plain-text reference number. The page script shows a thank-you modal (view copy: "Thank you").

**Validation on the server:** None of the browser rules are repeated. There is no check that terms were accepted, that the email is unused, or that the close date has passed. Unknown columns in `data` are passed to `Zend_Db_Table::insert`. A column the table does not have makes `insertData` print the exception and return false, and the action then sends no reference.

**Errors:** Insert failure produces no reference. Mail failure prints the exception inside `sendMail` and the action still echoes the reference if the insert succeeded. The photo POST has a timeout of 86400 seconds and its response is ignored.

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

Parallel actions `applymultipleAction` and `applytestAction` repeat this flow. `applytest.phtml` and `applymultiple.phtml` exist. The public form posts to `apply`, not to those actions.

## FR-004 — Reject a duplicate email in the browser

**Requirement:** While the applicant types an email, the page asks `/application/checkemail` (and `IndexController::checkemailAction`, same behaviour). The response is JSON `{"Ok": n}` where `n` is the number of `AccreditationApplycation` rows with that email and that `AC`. If `n > 0`, the page shows "Email already in use,please use different email address."

**Who:** Applicant, during the wizard or profile edit.

**Validation:** Browser only. `applyAction` does not call `checkEmails`.

**Provenance:** **Implemented** in the browser and in `Model_AccreditationList::checkEmails`. Server enforcement is absent. Whether uniqueness is a business rule is **Unknown / Requires Confirmation**.

## FR-005 — Closed applications

**Requirement:** After `CloseDate` 24:00:00 in the default timezone, the form view is replaced by "Accreditation applications is now closed." and a mailto link to `ContactName` / `ContactEmail` from the accreditation row.

**Provenance:** **Implemented** as a view switch in `applyAction` and `IndexController::indexAction`. See FR-002 for the POST gap. `closed.phtml` reads `$this->AccreditationName` and `$this->DATA`. On the apply action those are assigned after the view switch, so the closed page can still show them on GET.

## FR-006 — Sign in and sign out

**Requirement:** Sign-In (`/application/login`) shows email and reference for the hard-coded event. The button POSTs `emailaddress` and `refnumber` to `/application/loginnow/ac/{AC}`.

**Behaviour:** On POST, `UserLogin` selects `AccreditationApplycation` joined to `Accreditation_List` where `AC`, `Ref`, and `email` match. On a row, session namespace `user_session` stores `Code` (`aCode`) and `AAID`. JSON `{"OK":1}`. Otherwise `{"OK":0}` and the page shows "Bad credentials." Success navigates to `/application/profile/code/{code}`.

**Sign out:** `/application/logout` clears `user_session` and redirects to `/application/login/code/{previous code}`. `loginAction` then overwrites the code with `5DA997A51439E`.

**Validation:** Both fields must be non-empty in the browser after spaces are stripped. The server does not check format. The reference input is `type="password"`. The match is exact, including case.

**Errors:** Exceptions in `loginnowAction` print the exception and still `exit`.

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

**Constraint:** Login is bound to the hard-coded event's `AC`. An application stored under a different `AC` (including a linked-event copy from `apply[]`) does not match this login. Provenance: **Implemented.** Whether linked-event applicants are expected to sign in here is **Unknown / Requires Confirmation**.

There is no password, captcha, lockout, or session regeneration.

## FR-007 — Application profile

**Requirement:** `/application/profile/code/{code}` shows the signed-in application: reference, status, photo, services, zones, and an edit form.

**Who:** A browser whose `user_session.AAID` is greater than 0. Otherwise redirect to `/application/apply/code/{code}`.

**Behaviour:**

- Load the application by `AAID`, then the accreditation profile by that row's `aCode`.
- Status label (first match wins):
  - `Pending == 1` → Pending (orange).
  - else `Status == 0` and `DateApply < LastUpdate` → In Review (orange).
  - else `Status == 1` → Confirmed (blue), form locked.
  - `Status == 2` → Declined (red), form locked.
  - `Status == 3` → Approved (green), form locked.
  - else New Application, form editable.
- Photo: HTTP HEAD `https://www.footprintapp.net/accre/photos/{Ref}.jpg`. On HTTP 200, the image is inlined as a data-URI. Otherwise `/accre/profile-icon.png` (file `public/accre/profile-icon.png`).
- Services from `Accreditation_Applicants_Services` for the `AAID`. Zones from JSON column `aZones` resolved through `AccreditationsZones`.
- When unlocked, Save posts the form to `/application/update/aaid/{AAID}`. Photo replacement POSTs to `/application/updateimage`.

**Provenance:** **Implemented** in `profile.phtml` and `profileAction`.

`updateAction` writes the posted `data` array onto that `AAID` with `LastUpdate` set by `date("Y-m-d H:s:i")` (the `s` token is seconds, used in the minute position). It does not check the session. Any caller who knows an `AAID` can post. Provenance: **Implemented.** This is a defect relative to the profile page's own lock, not a documented requirement.

`updateimageAction` also has no session check. It forwards `ac`, `ref`, and `pic` to Footprint.

## FR-008 — Email the applicant

**Requirement:** Outbound mail uses `Zend_Mail` over SMTP.

**From:** `accreditation@magus.events`, display name "Accreditation - The Sun Met".

**Subject:** Always "The Sun Met Notification" inside `sendMail`. `EmailApplicant` builds a subject "Application Confirmation - {title}" and does not pass it to `sendMail`, so that subject is unused.

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

**When:**

| Event | Template column | Condition |
|---|---|---|
| Application inserted | `EmailNotes` | Column non-empty |
| Attendance `att=1` | `EmailAttandanceConfirmed` | Attendance key matches a row |
| Attendance `att=2` | `EmailRejected` | Column non-empty. Reason text: "Applicant confirmed NON-ATTENDANCE / CANCELLATION for this event." |

**Failure:** The exception is printed. The application row is already stored.

**Provenance:** **Implemented.** SMTP host, port, and username are in `sendMail`. The password is in that method and is omitted here.

`testAction` sends a fixed test body to a fixed address (`andre.ferraz@evolveav.co.za`) with no authentication. Provenance: **Implemented.** It is a public route.

## FR-009 — Confirm or cancel attendance

**Requirement:** `/application/attendance/key/{key}/att/{1|2}` loads `AccreditationApplycation` by `AttendanceKey`.

- `att=1`: email the confirmed-attendance template, clear `AttendanceKey`, set `AttendanceRequest` to 1 and `AttendanceResponseDate` to now. Page message: "Thank you for confirming your attendance".
- `att=2`: send the decline email when `EmailRejected` is set, clear the key, set `AttendanceRequest` to 2, `Status` to 2, `DeclinedDate` to now, `History` to "Declined by Applicant".
- Unknown key: empty `DATA`. The view then shows a warning state (`attendance.phtml` / `testattendance.phtml`).

**Provenance:** **Implemented.** `testattendanceAction` is the same flow and prints exceptions. Who creates `AttendanceKey` is not in this repository. **Unknown / Requires Confirmation.**

On the `att=1` branch of `attendanceAction`, `$message` is used when `att` is not 1. For `att=2`, `$message` is assigned to the view and is never set in that method. Provenance: **Implemented** (undefined variable on the decline path).

## FR-010 — Linked events on one submission

**Requirement:** When `LinkedAccreditations` has rows for this `AID` with `DateTo >= today`, the wizard adds an "Apply" step. Each checkbox is that link's `OID`, labelled with `Acc_Secondary`, `DateFrom`, and `Location`. The note says "Please select one or multiple events that you would like to apply for".

**Provenance:** **Implemented.** Each checked id becomes another insert with the same reference (FR-003).

## FR-011 — Supplier registration notice

**Requirement:** If the selected role option has attribute `reg` equal to `1` (`AccreditationRoles_view.SupplierReg`), the form 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** as a message. This repository does not register the supplier. The layout link to the same URL is commented out.

## FR-012 — Terms

**Requirement:** The finish step states: "I hereby fully accept all the terms and conditions inclusive of SA Rugby’s Tournament and Competitions Accreditations Policy as well as the Safety at Sports and Recreational Events Act of 2009." The applicant must tick acceptance. A modal shows `TermsAndConditions` from the accreditation row.

**Provenance:** **Documented** in the view copy. **Implemented** as a required checkbox in the browser. The checkbox value is stored if the column exists; the server does not reject a missing tick.

The policy document itself is not in this repository. Its text is whatever is stored in `TermsAndConditions`. **Unknown / Requires Confirmation** as a legal text outside the database.

## Integrations

| System | Direction | Trigger | Data | Auth | Failure |
|---|---|---|---|---|---|
| MySQL `teamwizz_SARU` on `db.footprintapp.net` | Read and write | Every page that loads a profile or saves an application | Accreditation views and `AccreditationApplycation` | Username in `application.ini.bak` (and `application.ini` on `prod`). Password not recorded here | Exceptions printed; `display_errors` is on |
| `smtp.gmail.com:587` TLS | Outbound | Apply email, attendance email, `testAction` | HTML body from the accreditation template | Mailbox `accreditation@magus.events`. Password in source, not recorded here | Exception printed; insert is kept |
| `www.footprintapp.net` photo update | Outbound POST | After a successful insert, and on profile photo change | `ac`, `ref`, `pic` | None in this code | Response ignored |
| `www.footprintapp.net` photo read | Outbound HEAD then GET | Profile view | `{Ref}.jpg` | None | Missing file falls back to the local placeholder icon |
| AWS S3 and CodeDeploy | Outbound from Bitbucket Pipelines | Push to `qa` or `prod` | Zip of the working tree | Pipeline variables `*_AWS_ACCESS_KEY_ID`, `*_AWS_SECRET_ACCESS_KEY`, `*_AWS_DEFAULT_REGION`, `*_APPLICATION_NAME`, `*_DEPLOYMENT_GROUP_NAME`, `*_S3_BUCKET` | Script prints the AWS error and returns false. `DEPLOYMENT_CONFIG` is read and is not set in the pipeline file |
| `supplier.sarugby.online` | Link only | Role with `SupplierReg = 1` | None | n/a | n/a |

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

## Important constraints

- Timezone `Africa/Johannesburg` in `public/index.php` and `ApplicationController`.
- Session cookie lifetime `864000` seconds (10 days) in the ini file.
- Locale `af_ZA` is set in the ini file. The screens inspected are English. No locale files were found.
- Reference numbers depend on `mcrypt`.
- Production PHP settings in the ini file turn `display_errors` and `displayExceptions` on.
- `phpinfo.php` exists at the repository root and at `public/phpinfo.php`.
- SQL predicates are concatenated strings (`UserLogin`, `checkEmails`, link query, updates). Provenance: **Implemented.**

## Workflows

### Apply

1. Visitor opens `/`.
2. If the event close date has passed, they see the closed page (GET).
3. Otherwise they complete Personal → Organisation → Photo → optional linked events → Finish.
4. The browser checks required fields, email format, and whether the email already exists for this `AC`.
5. POST stores the row, forwards the photo, optionally emails, and returns the reference.

### Return and edit

1. Visitor opens Sign-In.
2. Email and reference match a row for the hard-coded event.
3. Profile shows status. Confirmed, Declined, and Approved lock the fields.
4. Otherwise Save and photo upload update the row and Footprint.

### Attendance

1. Something outside this repo writes `AttendanceKey`. **Unknown / Requires Confirmation.**
2. The applicant opens the link with `att=1` or `att=2`.
3. The system emails them and, for cancel, sets status Declined.

## Requirements that are assumptions

| Statement | Label |
|---|---|
| This site is only for The Sun Met 2020 horse-racing / SA Rugby event named in the title | **Inferred** from the title, the mail display name, and the SA Rugby terms sentence. The database row can name something else |
| Approvers work in Footprint, not here | **Inferred** from the photo URL and the absence of an approve action |
| One email may apply only once per event | **Inferred** from the browser message. Not enforced on insert |
| `photo = 1` on a role means no headshot is required | **Inferred** from the class toggle. The flag's name suggests the opposite; treat the meaning as **Unknown / Requires Confirmation** |
| Category value `1` is "media" | **Inferred** from `ChangeSelectCat` when `MediaOnly` is 0 |
