# PRD — bluecard.footprintapp.net

Evidence date: 2026-10-19. Source: `git@bitbucket.org:overdrive/bluecard.footprintapp.net.git`. Behaviour below is `origin/prod` at `aa2cc42` (2022-10-19) unless a line says `master`.

Provenance labels: **Documented**, **Implemented**, **Inferred**, **Planned**, **Unknown**.

No formal PRD existed in the repository or in `silicon_ark/bluecard.footprintapp.net/` before this file. Requirements below are behaviour found in code. They are not a new product decision.

`origin/qa` (`a5caf1a`) matches this tree except the database host. `origin/master` (`3e123e6`) is the initial import, where Blue Card is a module inside a larger Footprint application. The pipeline does not deploy `master`.

Credentials in tracked source are not copied here.

## Product purpose

**Inferred.** A public reporter logs a rugby concussive injury (a Blue Card) for SA Rugby / BokSmart. The system emails concussion guidance to the reporter and to union BokSmart representatives, and stores the submission.

**Documented** purpose statement: none. The form and the emails describe the SA Rugby Injury and Illness Surveillance and Prevention Project (SARIISPP) and BokSmart concussion management. That text is the closest statement of purpose.

## Users and roles

No login and no permission matrix exist on prod. `Zend_Auth` is not used by the default module.

| Role | What the code shows | Provenance |
|---|---|---|
| Reporter | Anyone who can open `/` and submit the form. No account | **Implemented** |
| Player reporting their own injury | Role "Player". The "Who was involved" step is removed. A different email body is used | **Implemented** |
| Non-player reporter | Referee, Coach, Team Manager, Physiotherapist, Medical Doctor, Parent, Guardian | **Implemented** as the role list |
| Referee or Coach | BokSmart number field is shown. It is not marked required | **Implemented** |
| Extra contact | Up to four people, each with role, name, and email, copied on the mail | **Implemented** |
| BokSmart representative | Rows in `profile` with `BoksmartRep = 1` whose `UnionID` matches the selected union. Their email is copied. They do not use this application to opt in | **Implemented** lookup. How those rows are maintained is **Unknown** |
| Operator | No admin screen on prod. `testAction` is an unauthenticated mail probe | **Implemented** |

Git authors are not application roles.

## Major workflows

### WF-1 — Open the form

Trigger: `GET /` (default module, `IndexController::indexAction`).

Inputs: none.

Behaviour (**Implemented**):

1. Build the cause list: Tackle, Ball Carry, Ruck, Scrum, Line-out, Open play, Collision, Unknown.
2. Load unions from `ef_unions` where `un_type = 3` and `main = 1` (`Model_Unions::getUnions`). The query does not set an ORDER BY.
3. Build the role list: Referee, Coach, Team Manager, Physiotherapist, Medical Doctor, Parent, Guardian, Player.
4. Build a fixed list of 15 playing positions (1 loose-head prop through 15 full back). The `Model_Positions` call is commented out.
5. Generate an 8-character uppercase hex reference with `mcrypt_create_iv(4, MCRYPT_DEV_URANDOM)` and put it in a hidden field `data[RefNumber]`.
6. Load ethnicity names from `Ethnicity` where `EthCountry = 'South Africa'`, ordered by `EthnicityName`.
7. Render the wizard in `index/index.phtml` inside `layouts/default.phtml`.

Output: the form. A database error in the union or ethnicity query is not given a dedicated message. `public/index.php` turns `display_errors` on, so a PHP error is printed.

Error behaviour: if `mcrypt` is not loaded, `random_id` fatals and the form does not render. PHP version is **Unknown**.

### WF-2 — Complete the wizard

Trigger: the reporter moves through the steps and presses "Agree & Submit".

Steps (**Implemented** in `index.phtml`):

1. What happened — injury date, hour, minute, venue, cause, injured team, opposing team, provincial union.
2. Tell us who you are — role, name, surname, email, mobile. BokSmart number when the role is Referee or Coach. When the role is Player, also ID/passport/date of birth, age, position, gender, ethnicity.
3. Who was involved — the injured player's name, surname, email, mobile, ID, age, school/club, town/city, province, position, gender, ethnicity. This step is removed when the role is Player.
4. Additional contacts — one row is always present. "Add" appends rows up to four. Each row is role, name and surname, email.
5. Consent — SARIISPP / Blue Card consent paragraphs. There is no checkbox. The finish button label is "Agree & Submit". A Sports Concussion South Africa image links to `http://www.sportsconcussion.co.za/`.

Client checks (**Implemented**, jQuery Validate):

- Moving forward validates the current step. Hidden and disabled fields are ignored on that check.
- Moving backward is always allowed.
- Required fields use class `required`. Emails use `type="email"`. Hour, minute, and both age fields accept digits only and `maxlength="2"`. Minutes default to `00`.
- The union `<select>` label includes an asterisk. The control does not have class `required`.
- BokSmart number and contact fields are not required.
- `onFinishing` does not return `form.valid()` (that return is commented out). The first click sets a flag and posts. A second click does not post again. A failed post is not retried until the page is reloaded.
- `onStepChanged` skips a step when `$("#age").val()` is 18 or more. No element with id `age` exists (`Age` and `YourAge` do). **Inferred** leftover wizard code that does not run.

Output of a successful browser post: the step list is replaced with a single "Finish" step and the hidden thank-you HTML is shown. The thank-you reference number is the number rendered with the page, not a value returned by the server.

### WF-3 — Submit, notify, and store

Trigger: `POST /index/submit` from `Finished()` with `$("#BlueCardForm").serialize()`.

The `isPost()` guard in `submitAction` is commented out. A request that is not a POST still enters the action if `data` is present.

Behaviour (**Implemented**, `IndexController::submitAction`):

1. Read `data` from the request.
2. If `InjuryDate` is missing or empty, set it to today as `d/m/Y` (timezone `Africa/Johannesburg` from `public/index.php`). A date the user did enter is stored as posted. The datepicker format is `dd/mm/yyyy`.
3. Load seven files from `UPDATE_PATH/bluecard/` (`UPDATE_PATH` is `public/archives`) and attach them. See FR-012.
4. If `ApplicantRoll` is `Player`, build the player email body (`player_contant`). Otherwise build the other-person body (`contant`) and add `ApplicantEmail` to the CC list.
5. Look up BokSmart representative emails for `UnionName` and append addresses that are non-empty.
6. Append each `Contact[*][ContactEmail]` that is non-empty.
7. If `ApplicantEmail` is non-empty, send one message to that address with the CC list. If it is empty and the CC list is not, send one message per CC address with no CC list.
8. JSON-encode `data[Contact]` and insert the `data` array into `BlueCard_Consents`.
9. `exit`. There is no JSON body and no redirect.

The browser treats any HTTP success as success and shows the thank-you panel. It has no error callback. Mail and insert failures are printed into the response (`print_r` of the exception) and do not stop the other step: a mail exception is caught inside `sendMail`, then `insertData` still runs. An insert exception is printed and `insertData` returns false. The thank-you panel still appears when the response status is success.

`PlayerEmail` is posted and would be part of the insert. It is not added to the recipient list. **Inferred:** the injured player is emailed only when they are the applicant, or when someone types their address into a contact row.

### WF-4 — Test mail

Trigger: request `/index/test` (`testAction`). No authentication check.

Behaviour (**Implemented**): build a sample player email with fixed names and send it, with six attachments (no research-consent PDF), to a mailbox hardcoded in the action, then `exit`. The action does not insert a row.

This is a live send, not a dry run. Whether it should remain routable is **Requires Confirmation**.

## Functional requirements

### FR-001 — Public form

The system shows the Blue Card incident wizard at the default route without a login.

Evidence: `IndexController::indexAction`, `index.phtml`, no auth call.

Implementation: **Implemented** on prod. Confidence: High.

### FR-002 — Incident facts

The reporter enters the injury date, estimated time as hour and minute, venue, cause, injured team, opposing team, and provincial union.

Evidence: step 1 of `index.phtml`. Cause options are assigned in `indexAction`.

Validation: date, time, venue, cause, and both team names are client-required. Union is not client-required. The server checks only that an empty injury date becomes today. Hour and minute are not range-checked.

Implementation: **Implemented**. Confidence: High.

### FR-003 — Reporter identity

The reporter selects a designation and enters name, surname, email, and mobile.

Evidence: step 2. Role list in `indexAction`.

Validation: name, surname, email, and mobile are client-required. Email uses the email input type. The server uses the fields without checking format. A missing `data` array is not guarded.

Implementation: **Implemented**. Confidence: High.

### FR-004 — Player-specific questions

When the designation is Player, the form asks for the reporter's ID or passport or date of birth, age, playing position, gender (Male or Female), and ethnicity, and it removes the separate player step.

Evidence: `Player_Selected` in `index.phtml`. Position list and ethnicity query in `indexAction` and `Model_Ethnicity`.

Implementation: **Implemented**. Confidence: High.

### FR-005 — Reporting on behalf of a player

When the designation is not Player, the form asks for the player's name, surname, email, mobile, ID, age, school or club, town or city, province, position, gender, and ethnicity.

Evidence: section `playerdetails`. School, town, and province were added in `f990a10` (2022-10-18).

The legend text on that step is "Who was involved1". **Inferred** accidental edit in the same commit. The step title remains "Who was involved".

Implementation: **Implemented**. Confidence: High.

### FR-006 — BokSmart number

When the designation is Referee or Coach, the form shows a BokSmart number. Other roles hide it and clear it.

Evidence: `Player_Selected`. The input is not class `required`.

Implementation: **Implemented**. Confidence: High.

### FR-007 — Additional contacts

The reporter can include up to four extra contacts. The first row is always on the form. Each contact has designation, name and surname, and email. None of those inputs is required. Non-empty contact emails are copied on the notification.

Evidence: `AddNewContact`, `submitAction`.

The cap is enforced in the browser by counting `.row` elements. The server does not enforce a maximum. **Inferred** from the absence of a server check.

Implementation: **Implemented**. Confidence: High.

### FR-008 — Consent

The last step shows the SARIISPP consent text. Continuing by pressing "Agree & Submit" is the only consent action. No consent checkbox is posted. The consent paragraphs are not written to a separate column by the controller. They are not re-read from the database on submit.

Evidence: consent `<section>` in `index.phtml`. Button label in the steps configuration.

Whether a human considers the button press informed consent is a product question. **Requires Confirmation** if a stored consent flag is required. The code does not store one of its own.

Implementation: **Implemented** as page copy and button label. Confidence: High for the UI. The legal interpretation is **Unknown**.

### FR-009 — Reference number

Each page view creates a new reference number, shows it in the thank-you copy, posts it back, prints it in the email ("Ref:"), and includes it in the insert payload.

Evidence: `random_id`, hidden input, thank-you HTML, both email templates.

The number is not checked for uniqueness. The thank-you text uses the number from the original page render. If the posted value differs, the email and the insert use the posted value. **Implemented**.

Implementation: **Implemented**. Confidence: High.

### FR-010 — Union list and BokSmart copy

The union dropdown lists `ef_unions.un_name` for rows with `un_type = 3` and `main = 1`.

On submit, `Model_Bluecard::getUnionsEmails` selects `profile.Email` where `BoksmartRep = 1` and `UnionID` is the `un_id` of a union with that name, `un_type = 3`, and `main = 1`. The union name is concatenated into the SQL string.

Evidence: `Unions.php`, `Bluecard.php`.

A commented query in the same method used a parent-union expansion. The active query does not. **Implemented**.

Implementation: **Implemented**. Confidence: High.

### FR-011 — Two email bodies

If the applicant role is Player, the message thanks the reader for reporting their own event and uses `player_contant`. Otherwise the message thanks the reader for taking part in the named player's management and uses `contant`.

Both bodies include: BokSmart logo, a light-blue SARU privacy sentence linking to `https://www.springboks.rugby/general/privacy-policy-updated-2021/`, "PLEASE DON'T REPLY TO THIS EMAIL", the reference and the generated date, Sports Concussion SA contact details (011-3047724 office, 0825746918 after hours, `sportsconcussion@mweb.co.za`, `www.sportsconcussion.co.za`), a document list, a Heads Up logo, `www.BokSmart.com/Concussion`, and a Vision Zero link. The sign-off is "The BokSmart Team". Subject is "Bluecard Notification". From is `bluecard@sarugby.co.za`, display name "Bluecard at sarugby.co.za".

`contant` computes "he" or "she" from `PlayerGender` and replaces `[he/she]`. The current HTML does not contain that placeholder. **Implemented** substitution with no visible effect.

`player_contant` is called with the player's full name in the slot that the function treats as the greeting name, and with an empty string in the slot that replaces "[the logged Blue Card player]". **Implemented.** The player-role email can greet the player and then refer to a blank player name. **Requires Confirmation** whether that call order is intentional.

Implementation: **Implemented**. Confidence: High.

### FR-012 — Attachments

Submit attaches these files from `public/archives/bluecard/`:

| Attachment name | File read |
|---|---|
| `2 BlueCard_Research_Consents.pdf` | `2 BlueCard_Research_Consents.pdf` |
| `3 Blue Card infographic.pdf` | `3 Concussion Blue Card infographic.pdf` |
| `4 Concussion Advice Sheet 2018.pdf` | `4 Concussion Advice Sheet 2018.pdf` |
| `5 Concussion Register Template.xlsx` | `5 Concussion Register Template.xlsx` |
| `6 When can a player safely return to play WR 22 05 2017.pdf` | `6 When can a player safely return to play WR 22 05 2017.pdf` |
| `7 Concussion Management infographic.pdf` | `7 Concussion Management infographic.pdf` |
| `8 ConcussionPreventionFINAL.pdf` | `8 Concussion Prevention update.pdf` |

The thank-you panel links to a second set under `public/archives/bluecarddownload/` with spaces removed from the file names, and it links the register as the `.xlsx.zip`. The email attaches the unzipped `.xlsx`, not the zip. Both sets, plus a `backup.16-05-2022` copy of the research-consent PDF, are in the prod tree.

`file_get_contents` has no existence check. A missing file warns and attaches empty or false content. **Implemented**.

Implementation: **Implemented**. Confidence: High.

### FR-013 — Persistence

After the mail attempt, the action JSON-encodes the contact list and inserts the posted array with `Model_Bluecard`, table `BlueCard_Consents`.

Evidence: `submitAction`, `Bluecard.php`.

There is no schema file. Zend insert fails if a posted key is not a column. The October 2022 fields are in the form only. Whether the table has `PlayerSchoolClub`, `PlayerTownCity`, and `PlayerProvince` is **Unknown / Requires Confirmation**. An insert failure is printed and does not change the browser's success handling (FR workflow WF-3).

`updateData` exists on the model and updates `AC = '{id}'`. No prod controller calls it.

Implementation: **Implemented** insert call. Column contract **Unknown**. Confidence: High for the call, Low for a successful write of every field.

### FR-014 — Thank-you content

The thank-you panel states that the reference was logged, points the reader at the email, gives Sports Concussion SA contacts, links BokSmart concussion information, and lists download links for the research consent, the Blue Card process, concussion advice, the zipped register, return-to-play, the management infographic, and prevention. The sign-off is "The BokSmart Team".

Evidence: `thankyou` string in `indexAction`.

The panel is shown on HTTP success even when the email or the insert failed. **Implemented**.

Implementation: **Implemented**. Confidence: High.

### FR-015 — Page chrome

The layout shows the SA Rugby banner, the title "SA Rugby - Official Home of the Springboks", and a footer "Copyright {year} - SARU. All Rights Reserved."

Evidence: `application/layouts/default.phtml`.

Implementation: **Implemented**. Confidence: High.

## Integrations

| System | Direction | Trigger | Data | Failure |
|---|---|---|---|---|
| MySQL `teamwizz_SARU` | Read and write | Page load and submit | Unions, ethnicities, BokSmart emails, consent row | Printed exception. Page or insert stops at the PHP error |
| `mail.sarugby.co.za:25` | Out | Submit and `/index/test` | HTML body, CC list, attachments. From `bluecard@sarugby.co.za` | Caught and printed. Insert still runs on submit |
| SARU privacy policy, BokSmart, Vision Zero, Sports Concussion SA | Browser links only | Email and thank-you markup | None from this app | Not handled. They are ordinary links |
| `cdn.ywxi.net` | Browser loads a script | Every layout render | Not documented | Not handled |
| AWS CodeDeploy | Deploy only | Push to `qa` or `prod` | The zipped repository | Pipeline step exits non-zero if upload or deploy returns false |

Authentication to MySQL and SMTP is a username and password in source. Values are not recorded here. Earlier Office 365 / TLS settings are commented out in `sendMail`. The active transport is `mail.sarugby.co.za` port 25 with login auth and no `ssl` entry.

## Constraints

- **Implemented.** No authentication on the form, the submit action, or the test action.
- **Implemented.** `public/index.php` sets `display_errors` to 1 and `E_ALL|E_STRICT`. `application.ini` sets `display_errors` to 0. The front controller runs after config is loaded and turns display back on. `IndexController` then sets `E_ALL` without warnings and `memory_limit` 180M.
- **Implemented.** Locale in ini is `af_ZA`. The form copy is English.
- **Implemented.** Session cookie lifetime is 864000 seconds. Prod has no login that uses the session.
- **Inferred.** The pipeline `zip -r /tmp/artifact.zip *` does not include dotfiles unless the shell is configured to. Root `.htaccess` is a dotfile and is required for the rewrite into `public/`. Whether the deployed artifact contains it is **Requires Confirmation**.
- **Unknown.** Database column lengths, required columns, and retention. The consent text says SA Rugby stores the data at its offices. The code stores a row in `teamwizz_SARU`. Where that server sits is not described beyond the host name.

## Behaviour that is not a current prod feature

These exist on `master` or as unused files on prod. They are not requirements of the prod Blue Card flow.

- Match centre, cron, login, news, and tournament controllers (removed from the module tree on the way to prod).
- Menu, company, permit, and organisation modules (same).
- `website/` second tree (`master` only).
- Models for fixtures, accreditation, allergens, and similar, still present under `application/models` on prod and not referenced by the default module.
- `Model_Bluecard::updateData` and `Model_Ethnicity::insertData` / `updateData`.

Nothing in the repository describes a replacement or a plan to restore them. Status: removed from the deployed module tree, not formally deprecated in a document.

## Open points

See the confirmation list in `project-plan.md`. The ones that change this PRD are: prod versus master as the product tree, PHP/`mcrypt` availability, the `BlueCard_Consents` columns for the October 2022 fields, whether the player-role email blank name is intentional, and whether `/index/test` may stay public.
