# Business rules — psl2020

Evidence date: 2026-10-06. Source: `master` at `007bc5a`.

Rules below are taken from controllers and views. Each rule is marked **Implemented** or **Inferred**. Nothing here is a signed business policy. Where the code and a likely business intent disagree, both are stated.

Passwords are not copied.

## Event identity

**BR-001.** The public home page and the sign-in page always use accreditation code `5F27014B26D5F`.

Provenance: **Implemented** in `IndexController::indexAction` and `ApplicationController::loginAction`.

The screen title is "PSL Operations (2020) - Accreditation Portal". The event name, dates, and venue come from the database row, not from that title. Whether `5F27014B26D5F` is the PSL event is **Unknown / Requires Confirmation**.

**BR-002.** A URL `/application/apply/code/{code}` may load a different accreditation, if `Accreditations_view.aCode` matches.

Provenance: **Implemented** in `applyAction`.

**BR-003.** An unknown code stops the page with "Sorry this page does not exits".

Provenance: **Implemented**. The spelling is the string in the controller.

## Who may apply

**BR-004.** The applicant is anonymous. There is no account password. The reference number plus email is the later credential.

Provenance: **Implemented** (`UserLogin` matches `email`, `Ref`, and `AC`).

**BR-005.** Categories offered on the form are those in `AccreditationCategory_view` for this accreditation where `Teams = 0` and `hidden = 0`, ordered by category name.

Provenance: **Implemented** in `getAccreditationCategoryPublic`. **Inferred:** team categories are hidden from this public form.

**BR-006.** Roles offered are all roles on `AccreditationRoles_view` for this accreditation (`getAccreditationRoleLive`). The method name says "Live". The SQL does not filter on an active or live column.

Provenance: **Implemented**.

**BR-007.** The browser warns when the email already exists on `AccreditationApplycation` for the same `AC`. The server still inserts a second row.

Provenance: **Implemented**. **Inferred** that one email per event was intended. The server does not enforce it.

**BR-008.** Terms must be ticked in the browser (`data[acceptTerms]` has class `required`). The server does not check the flag.

Provenance: **Implemented**. The terms text grants PSL Operations sole discretion on access level and cites the Safety of Sports and Recreational Events Act of 2009. Provenance of the legal text: **Documented** in `index/index.phtml` (commit `76d2dcc`).

**BR-009.** When `MediaOnly` on the accreditation is 1, editor/producer name and email are required in the browser and the media block is shown. The Openfield / Roth Media choice is commented out and is not collected.

Provenance: **Implemented**.

**BR-010.** When the selected role option has attribute `reg` equal to 1, the page shows a note that a services company must register at `http://supplier.sarugby.online`. The form can still be submitted.

Provenance: **Implemented**. Whether a PSL applicant is meant to use the SA Rugby supplier site is **Unknown / Requires Confirmation**.

## Headshot

**BR-011.** The browser rejects a file larger than 2,097,152 bytes. The alert text says 1.5mb.

Provenance: **Implemented**. The two limits disagree.

**BR-012.** If `User_pic` is an empty string on submit, the controller stores a hard-coded placeholder JPEG before insert.

Provenance: **Implemented** in `applyAction`.

**BR-013.** The live submit does not write the photo to local disk. It POSTs `ac`, `ref`, and `pic` to Footprint. The profile later reads `{Ref}.jpg` from `https://www.footprintapp.net/accre/photos/`.

Provenance: **Implemented**.

**BR-014.** Script `getCurrentRole` removes HTML `required` from the photo field when the role's `photo` value is 1, and adds `required` otherwise.

Provenance: **Implemented**. **Inferred** that the flag and the class move in opposite directions from the flag's name. The script also embeds the role list with `<?=$Roles?>`, which is not JSON for a Zend rowset. Runtime effect is **Unknown / Requires Confirmation**.

## Closing date

**BR-015.** Applications are treated as closed when `strtotime(CloseDate + " 24:00:00")` minus now is less than 1. The visitor then sees the closed page and the contact email from the accreditation row.

Provenance: **Implemented**.

**BR-016.** That view switch does not `return`. A POST that arrives after the close date still inserts.

Provenance: **Implemented**. **Inferred** defect, unless late posts are accepted on purpose. **Unknown / Requires Confirmation.**

**BR-017.** An empty `CloseDate` is passed to `strtotime` with no guard. Resulting behaviour is **Unknown / Requires Confirmation**.

## What is stored

**BR-018.** One successful submit inserts a row in `AccreditationApplycation` with `AC`, `TID`, `FID`, `DateApply`, `Ref`, `History` = `None`, and the posted `data` fields. `DaysAttending` is stored as an empty string when missing.

Provenance: **Implemented**.

**BR-019.** The reference is 3 random bytes rendered as uppercase hex (6 characters). There is no uniqueness check.

Provenance: **Implemented**. Collision behaviour is **Unknown**.

**BR-020.** Each checked linked event (`apply[]` = `OID` from `LinkedAccreditations`) causes another insert with the same reference and personal data and a different `AC`. Only links with `DateTo` on or after today are listed on the home page and on `applyAction`.

Provenance: **Implemented**. The email and the Footprint photo POST run once, for the primary accreditation, not once per linked insert.

**BR-021.** `History` is set to `None` on create. Attendance cancel sets `History` to "Declined by Applicant". No other history write exists in this repo.

Provenance: **Implemented**.

## Status and editing

**BR-022.** Profile status text is:

| Condition (first match) | Label | Fields |
|---|---|---|
| `Pending == 1` | Pending | Editable |
| `Status == 0` and `DateApply < LastUpdate` | In Review | Editable (lock is commented out) |
| `Status == 1` | Confirmed | Disabled in the browser |
| `Status == 2` | Declined | Disabled. `DeclinedMessage` is shown |
| `Status == 3` | Approved | Disabled |
| else | New Application | Editable |

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

**BR-023.** Nothing in this repository sets Pending, Confirmed (`Status` 1), or Approved (`Status` 3). The only status write is attendance cancel, which sets `Status` to 2.

Provenance: **Implemented**. **Inferred:** review and approval happen in another Footprint application that shares `teamwizz_SARU`. **Unknown / Requires Confirmation.**

**BR-024.** Disabled fields are a browser constraint. `updateAction` updates `AccreditationApplycation` for the posted `AAID` with no session check and no status check.

Provenance: **Implemented**.

**BR-025.** `LastUpdate` on profile save uses PHP format `Y-m-d H:s:i`. `s` is seconds. The minutes portion is therefore wrong.

Provenance: **Implemented**. **Inferred** defect. The In Review rule compares `DateApply` with `LastUpdate`, so a bad timestamp can change that label.

## Attendance

**BR-026.** An attendance link is valid only while `AttendanceKey` matches the key in the URL.

Provenance: **Implemented** in `getByAttandanceID`.

**BR-027.** `att=1` records attendance (`AttendanceRequest` 1), timestamps the response, clears the key, and emails `EmailAttandanceConfirmed` after replacing the standard placeholders and `[APPLICANTNAME]`.

Provenance: **Implemented**.

**BR-028.** `att=2` declines the application (`Status` 2, `DeclinedDate` now, history "Declined by Applicant"), clears the key, and emails `EmailRejected` when that column is not empty. The reason inserted into `[REASONNOTE]` is "Applicant confirmed NON-ATTENDANCE / CANCELLATION for this event."

Provenance: **Implemented**.

**BR-029.** A missing key shows "this link does not exist or has been used already". A used key cannot be reused because the key is cleared.

Provenance: **Implemented**.

**BR-030.** Who generates the key and the email that contains the link is not in this repository.

Provenance: **Unknown / Requires Confirmation**.

## Email content

**BR-031.** Outgoing mail is sent only when the relevant template column is non-empty (`EmailNotes` on apply, `EmailRejected` on cancel). Attendance-confirmed mail is sent whenever the row is found; an empty template would still call `sendMail`.

Provenance: **Implemented**.

**BR-032.** Placeholders replaced from the accreditation row: `[ACCREDITATIONNAME]`, `[CONTACTNAME]`, `[CONTACTTELEPHONE]`, `[CONTACTEMAIL]`, `[REFNUMBER]`, `[APPLICANTNAME]`, and on decline `[REASONNOTE]`.

Provenance: **Implemented**.

**BR-033.** The visible subject is always "PSL Operations Notification". A local `$subject` variable ("Application Confirmation - …" or "Attendance Confimation") is not passed to `Zend_Mail`.

Provenance: **Implemented**. The second spelling is the string in the controller.

## Access and session

**BR-034.** `applyAction` sends an existing session (`AAID > 0`) to the profile instead of showing the form.

Provenance: **Implemented** via `isloggedIN`.

**BR-035.** `profileAction` without a session redirects to the apply URL. With a session it loads the application by `AAID` and then the accreditation by that application's `aCode`, ignoring the code in the URL.

Provenance: **Implemented**.

**BR-036.** Session cookie lifetime in the tracked ini sample is 864000 seconds.

Provenance: **Documented** in `application.ini.bak`. Live ini is **Unknown / Requires Confirmation** because `application.ini` is gitignored and absent.

**BR-037.** Sign-in comparison is exact on email and reference. The login script strips spaces in a local variable and then posts the original values.

Provenance: **Implemented** in `login.phtml`. **Inferred:** a trailing space can fail a match that the script already treated as non-empty.

## Browser gate

**BR-038.** The portal body renders only when the user-agent matches Firefox, Safari, Chrome, Flock, or Opera in `get_user_browsers()`. An empty result or `MSIE` shows the unsupported-browser panel.

Provenance: **Implemented** in `application/layouts/default.phtml`. Chrome is matched as Safari because that pattern is tested first. The visitor still sees the portal.

## Deploy rules that affect behaviour

**BR-039.** Pipelines deploy `qa` and `prod` only. `qa`'s tracked database host is `qadb.footprintapp.net`. `master` and `prod` use `db.footprintapp.net`. Database name in the bak file is `teamwizz_SARU` on all three.

Provenance: **Implemented** in `bitbucket-pipelines.yml` and `application.ini.bak`.

**BR-040.** Zend is configured to load `application/configs/application.ini`. That file is not in Git. A pipeline zip of the clone therefore does not contain it unless the file is created outside Git.

Provenance: **Implemented** (gitignore plus `public/index.php`). Whether production has a copy placed by hand is **Unknown / Requires Confirmation**.
