# PRD — myPass

Evidence date: 2026-10-09. Source: `git@bitbucket.org:overdrive/mypass.git`, `master` at `803166e` (2022-01-27).

There is no prior PRD in the repository or in Silicon Ark for this directory. Every requirement below was recovered from the code, the page, and the commit history. Provenance is marked on each item.

A later repository, documented in Silicon Ark as `myPass-registration`, continues a similar form and calls `getstyling` before `getevent`. Requirements below describe **this** repository only. Where the two disagree, that is recorded as a related-product difference, not as a requirement of this tree.

## Product purpose

A visitor who opens an event subdomain completes a four-step accreditation application and receives a reference number.

The page does not sign the visitor in. It does not show a pass, a health result, or an application status after submit. Those screens exist only as static design HTML under `exports/`. Provenance: **Implemented** for the form (`index.html`, `scripts/core.js`); **not implemented** for sign-in, health, and pass.

## Users and roles

| Role | What they do | Evidence | Provenance |
|---|---|---|---|
| Applicant | Opens the event host, completes the form, submits | `index.html`, `scripts/core.js` | Implemented |
| Event configuration (outside this repo) | Supplies titles, banner, close date, categories, roles, terms, declaration text, linked events, and whether an organisation reference is required | Response fields read after `getevent` | Inferred from fields consumed. The editor is not in this repository |
| Organisation (outside this repo) | Holds a reference number that `getorganisation` resolves to a trading name | `searchOrg` | Inferred. Organisation admin screens are not in this repository |
| Reviewer | Processes the application after submit | Success copy: "once your application has been processed by the {EVENTNAME} Team" | Documented in `index.html`. No review UI in this repository |

There is no logged-in role, no permission check, and no staff screen.

## Main workflows

### W1 — Open the form for a subdomain

Trigger: browser loads `index.html`.

1. `scripts/custom.js` hides `#preloader` after 150ms and, because `isPWA` is `true`, registers `/_service-worker.js` at scope `/`.
2. `domains.js` defines the label-to-event map (see FR-002).
3. `scripts/core.js` takes the first label of `location.hostname`. If that label is a key in `domains`, `staticEventID` becomes that event id. Otherwise `#page-content` is replaced with "Please check subdomain" and the form is gone.
4. `page` is set to `"registration"` by an inline script in `index.html` that runs before `scripts/core.js`.
5. On `DOMContentLoaded`, if `page == "registration"`, the client calls `GET https://footprintapp.net/api/mypass/getevent/EventID/{staticEventID}`.
6. The JSON is stored in `localStorage` under `regFormDetails`. The page shows `AccreditationTitle`, `DATA.AccreditationName` (twice, `#eventName` and `#eventName2`), and `DATA.AccreBanner`.
7. Category `<select id="roleCategory">` is filled from `json.Cats` (`ACID`, `Category`). `roleSelect()` then fills `#roleFunction` from `json.Roles`.
8. Birth years from 1930 through the current calendar year are written into `#birthdayYear`, with 1990 selected (`5ce83bc`).
9. If `DATA.DateTo` is today or earlier, `#details_content` is hidden and `#notifications` shows "Registration for this event is now closed." The handler returns and does not apply terms, linked events, or the organisation-reference flag.
10. Otherwise terms HTML is written to `#TermsAndConditions`, and `#covidLabel` is replaced with `DATA.AccreditationNote`.
11. If `MediaEvent` is `"0"`, or `Linked` is missing, the Events step is hidden and the Identity Next control's `data-bs-target` is changed to `#tab-4`. The attribute written is `conclick`, not `onclick` (see edge cases).
12. If `MediaEvent` is not `"0"` and `Linked` is an array, `#eventList` is filled with one checkbox per item (`LID`, `Acc_Secondary`).
13. If `RegistrationFields.RequireOrganisationReference` is `"0"`, `#orgCode` is hidden and `#orgName` is enabled for typing.

Failure of `getevent`: `catch` calls `loging` only. The form stays on its HTML defaults. `debug` is `true`, so the error is `console.log`. Provenance: **Implemented**.

### W2 — Personal details

Trigger: applicant edits Personal fields. Next stays hidden (`#nextBtnOne`) until `checkDivInputs('tab-1','nextBtnOne')` decides the step is valid.

Fields on the step: first name, surname, ID or passport number, nationality, date of birth (day, month, year), gender, ethnicity, dial code, mobile number, email, address line 1, address line 2, town or city, state or province, country, postal code.

Changing nationality (`onNationality`):

- Sets `#country` to the first country option whose **visible text** contains the nationality string.
- If nationality is exactly `South Africa`, `#ethnicity` is enabled. Otherwise it is disabled.
- Sets `#mobileNumberPre` to the first dial-code option whose visible text contains the nationality string.

Changing first name copies it into `#passName`. Changing surname appends a space and the surname onto the current pass name. Provenance: **Implemented** (`nameToPass`, `surnameToPass`).

Next calls `setTabActive('2')` and opens `#tab-2`.

### W3 — Identity, organisation, photo

Trigger: Identity step.

- Pass name is required by the empty-field check. It is prefilled only if the name fields fired `onchange`.
- Organisation reference: uppercased and sent to `GET https://footprintapp.net/api/mypass/getorganisation/ORefNumber/{ref}` on change, on the search button, and automatically when the typed length is 6 (`orgCodeSearch`). A hit writes `json[0].TradingName` into `#orgName` and shows the green tick icons.
- Category change refilters roles (`roleSelect`). A category value of `0` lists every role. Any other value lists roles whose `ACID` matches.
- Role description is a textarea.
- Photo: `<input type="file" accept="image/*">`. Choosing a file stores a data URL on `registerUser.profileImage` (`readFile`). EDIT PHOTO builds a Cropme viewport (250 by 280, square, zoom and rotation). CROP replaces the preview and `registerUser.profileImage` with the cropped data URL.
- Moving to step 3 or 4 calls `cropImage()` when `croptimes` is still 0.

Next calls `setTabActive('3')` unless the events step was hidden, in which case the collapse target is `#tab-4` while the original `onclick` is still `setTabActive('3')`.

### W4 — Linked events

Trigger: Events step is visible.

Copy: "Please select the event or events you wish to attend." At least one checkbox must be checked before `#nextBtnThree` is shown (`oncheckEventSubmit`). The checked values are `LID`s from the event payload.

### W5 — Declarations and submit

Trigger: Submit step.

Both `#tandcCheck` and `#covidCheck` must be checked before `#submitRegistration` is shown (`oncheckSubmit`). The terms link opens modal `#menu-call`. The second label is whatever `getevent` wrote into `#covidLabel`. The HTML default, used if the event call has not replaced it, is a Covid-19 close-contact and diagnosis statement covering 14 days.

SUBMIT (`submitRegistration`):

1. Hides Previous, Submit, and the disabled Submit, and shows `#submitBtnLoader`.
2. Builds `registerUser` (FR-010) and `POST`s it as JSON to `https://footprintapp.net/api/mypass/postaccreditation` with `Content-Type: application/json; charset=UTF-8` and `mode: 'cors'`.
3. On a JSON body: hides the form, shows `#response`, sets `#eventTeamName` to `json.EVENTNAME + ' Team'`, `#eventRefNumber` to `"REF: " + json.REFNUMBER`, and `#contactEmail` to the literal `info@mypass.zone`. The response field `EMAIL` is not shown (the assignment is commented out).
4. CLOSE (`closeClear`) clears text inputs and reloads the page.

On network or JSON failure, `catch` only logs. The loader stays. There is no error panel. Provenance: **Implemented**.

## Functional requirements

### FR-001 — Single registration page

The product page is `index.html`. The script treats it as the registration page when `page == "registration"`. That variable is set in the page itself.

Provenance: **Implemented**.

### FR-002 — Subdomain selects the event

`staticEventID` starts at `325`. It is replaced when the first hostname label is in this map (`domains.js`):

| Label | Event id |
|---|---|
| `bil2021media` | 325 |
| `bil2021` | 304 |
| `cts` | 331 |
| `eap` | 329 |
| `fnb` | 332 |
| `loftus` | 330 |
| `local` | 346 |
| `9hour` | 346 |
| `sarugby` | 358 |
| `supersport` | 359 |
| `bulls` | 360 |
| `lions` | 361 |
| `sharks` | 362 |
| `wp` | 363 |
| `pumas` | 364 |
| `cheetahs` | 365 |
| `griquas` | 366 |
| `rc` | 341 |

An unknown label replaces `#page-content` with "Please check subdomain". The default `325` is then unused because the form markup is gone.

Commit subjects name `bil2021.mypass.zone` and `rc.mypass.zone`. The code itself only reads the first label. Provenance: **Implemented** for the map; the parent domain `mypass.zone` is **Inferred** from those subjects.

`local` and `9hour` share event `346`.

### FR-003 — Load event content

Who: applicant's browser. Trigger: registration page load after a known subdomain. Output: titles, banner, categories, roles, and the flags in W1.

`GET https://footprintapp.net/api/mypass/getevent/EventID/{id}`.

Fields read:

| Field | Use |
|---|---|
| `AccreditationTitle` | Line above "PERSONAL DETAILS" |
| `DATA.AccreditationName` | Both headers |
| `DATA.AccreBanner` | `#topBanner` image URL |
| `DATA.DateTo` | Close check |
| `DATA.TermsAndConditions` | Terms modal HTML |
| `DATA.AccreditationNote` | Second declaration label |
| `Cats[]` | `.ACID`, `.Category` |
| `Roles[]` | `.ARID`, `.Role`, `.ACID` |
| `MediaEvent` | `"0"` hides the Events step |
| `Linked[]` | `.LID`, `.Acc_Secondary` |
| `RegistrationFields.RequireOrganisationReference` | `"0"` hides the reference box and unlocks the organisation name |

Provenance: **Implemented**. The producer of this JSON is not in this repository.

### FR-004 — Registration closed

If `new Date(DATA.DateTo)` at local midnight is less than or equal to today at local midnight, the form body is hidden and the closed message is shown. Equal dates count as closed (`dateInPast`). Time of day and timezone are not read. Provenance: **Implemented** (`11e1d9f`).

### FR-005 — Personal step gate

`checkDivInputs` walks every `input`, `textarea`, and `select` in the step. A control with `value.length == 0` fails the step, except `#address2` and `#orgRefNumber`.

The Next control is shown only when that check passes. A failing check also sets `#menu-warning` to `display:block` and constructs `bootstrap.Alert` on it. The sheet's own copy is the theme sentence "You can continue with your previous actions." Provenance: **Implemented**.

Placeholder selects whose value is non-empty still pass. `#nationality` and `#country` use value `default`. Day and month use value `00`. Those placeholders are not empty, so they do not block Next. Provenance: **Implemented** behaviour. Whether that was intended is **Unknown / Requires Confirmation**.

Email format is not checked. The mobile field shows a green tick when its length is greater than 9 (`validateField`, `803166e`) and a red cross otherwise. That icon does not block Next. A non-empty mobile number of any length can pass the step gate.

### FR-006 — Ethnicity only for South Africa

`#ethnicity` starts disabled. It is enabled only when nationality is exactly `South Africa`. Options: `African_Black` ("African Black"), `White`, `Indian`, `Coloured`, plus a disabled placeholder value `default`. Provenance: **Implemented**.

The submitted value is the select's value even when the control is disabled. A visitor who never enables it still sends `default` if the browser includes the disabled control's value in the script read (`registerUser.ethnicity = ...value`). The script reads `.value` directly, so the placeholder value is what is posted unless the visitor changed it. Provenance: **Implemented**.

### FR-007 — Gender

The form offers Male (`value` `1`, checked by default), Female (`0`), and Prefer not to say (`2`).

Submit sets `gender` to `F` only when `#female` is checked. Every other state, including Prefer not to say, sends `M`. The radio values are not posted. Provenance: **Implemented** (`submitRegistration`). The mismatch between the third option and the payload is visible in the code; no comment says it is intentional.

### FR-008 — Organisation reference

Who: applicant. Trigger: six characters typed, change on the reference field, or the search control.

The value is uppercased. `GET https://footprintapp.net/api/mypass/getorganisation/ORefNumber/{ref}`. On success the first row's `TradingName` fills `#orgName`.

`#orgRefNumber` is excluded from the empty check, so a blank reference does not by itself block Identity Next. `#orgName` is included. While it is empty, Next stays off. After a successful lookup, or when the event says a reference is not required and the visitor types a name, Next can proceed.

A failed lookup, a non-array body, or an empty array: `catch` logs, and the search button stays disabled with the loader visible, because those are only reset inside the success path. Provenance: **Implemented**.

A comment on the `fetch` line shows the example `07D40F4D5C91` (12 characters). Auto-search runs at length 6. Provenance: **Implemented**. Whether organisation references are 6 or 12 characters is **Unknown / Requires Confirmation**. `myPass-registration` later reads a `LimitReached` flag. This client does not.

### FR-009 — Photo

The Identity step tells the visitor to centre the face in the white square and to click CROP before Next. Rules printed on the page: clear and sharp, both sides of the face, face and shoulders centred, no hair across the eyes, no hats, caps, sunglasses, or head coverings, uniform lighting.

The file input's empty value fails `checkDivInputs`, so a file must be chosen before Identity Next appears.

`setTabActive(3)` and `setTabActive(4)` call `cropImage()` if `croptimes` is 0. `cropImage` calls `profileImage.crop()`. `profileImage` is created only in `editImage`. If the visitor never pressed EDIT PHOTO, that call throws. Provenance: **Implemented**. There is no guard.

Crop output is a data URL stored on `registerUser.profileImage`. The payload also sends `fileUpload` as the file input's filename string (`#file-upload` `.value`), not the file bytes.

### FR-010 — Submit payload

`POST` body, JSON:

| Property | Source |
|---|---|
| `firstName`, `familyName`, `passportNumber`, `nationality` | Personal fields |
| `ethnicity` | Ethnicity select value |
| `birthday` | `{day}-{month}-{year}` |
| `gender` | `F` or `M` as in FR-007 |
| `mobileNumber` | Mobile field with leading zeros removed (`replace(/^0+/, '')`) |
| `mobileNumberPre` | Dial-code select value (digits, no plus) |
| `email`, `address1`, `address2`, `townCity`, `stateProvince`, `country`, `postalCode` | Personal fields. `address1` is assigned twice with the same value |
| `passName`, `orgRefNumber` (uppercased), `orgName` | Identity fields |
| `roleCategory`, `roleFunction` | Select values (`ACID`, `ARID` when filled from the API) |
| `roleDescription` | Textarea |
| `fileUpload` | File input filename |
| `profileImage` | Data URL from file read and/or crop, if that ran |
| `tandcCheck`, `covidCheck` | `1` when checked, else `0`. Submit is only offered when both are checked, so a normal submit sends `1` and `1` |
| `eventID` | `staticEventID` |
| `eventsLinked` | Array of checked `LID` values. Empty when the events step was hidden and nothing was checked |

`phone` is commented out in the markup and is not posted. Provenance: **Implemented**.

### FR-011 — Success panel

The panel states that a confirmation email was sent with the reference number and that more information will follow once the `{EVENTNAME} Team` has processed the application. Contact shown is `info@mypass.zone` (`097486a`).

This repository does not send mail. Provenance: **Documented** in the page; delivery is **Unknown**.

### FR-012 — Offline shell

`_service-worker.js` cache name is `Cache-Version-v1`. The only precached file is `index.html`. Fetch uses cache-first, then network. `scripts/custom.js` registers it when `isPWA` is true.

The cached page cannot complete `getevent` or submit while offline. Template helpers `offlinePage` / `onlinePage` exist in `scripts/custom.js`. The registration page does not include the offline markup those helpers expect. Provenance: **Implemented** for the worker; offline completion of an application is **not implemented**.

## Integrations

| Call | Direction | Trigger | Data | Auth | Failure |
|---|---|---|---|---|---|
| `GET /api/mypass/getevent/EventID/{id}` | Browser → Footprint | Known subdomain, page load | FR-003 | None in this client | `loging` only |
| `GET /api/mypass/getorganisation/ORefNumber/{ref}` | Browser → Footprint | Length 6, change, or search | `TradingName` of row 0 | None | Loader can stick; `loging` only |
| `POST /api/mypass/postaccreditation` | Browser → Footprint | SUBMIT | FR-010 | None | Loader remains; no error panel |
| Google Fonts Inter | Browser → Google | Page load | Stylesheet | None | Browser default if blocked |
| Service worker | Browser | Page load, `isPWA` | Caches `index.html` | None | Registration errors are unhandled |

Host: `https://footprintapp.net`. No API key header.

`scripts/custom.js` can call `https://api.qrserver.com/v1/create-qr-code/` when an element with class `generate-qr-auto` or `generate-qr-button` exists. `index.html` has neither. Provenance: **Implemented** in the theme script; **not used** by the registration page.

`php/contact.php` would `mail()` to `mail@domain.com`. Nothing posts to it. Provenance: **not part of the product flow**.

## Constraints

- Public page. No session, no CSRF token, no captcha.
- `debug` is hard-coded `true`. API responses are written to the browser console.
- The whole `getevent` JSON, which includes terms and role lists, is stored in `localStorage` (`regFormDetails`).
- The photo data URL is held on `registerUser` in memory and posted in the JSON body. Size limits are **Unknown**.
- Pipeline uploads `BITBUCKET_CLONE_DIR`, so a successful pipeline also publishes `template/`, `exports/`, and `.scannerwork/`.
- Nationality options and country options are separate hardcoded lists. Country includes value `Afganistan` with label `Afghanistan`. Dial codes are a third hardcoded list, including United Kingdom (`44`, `65f74d7`) and United States (`1`, `9c25c70`). `country.json` is unused.
- `onNationality` matches with `String.includes`. A short nationality name can match the wrong country or dial code when one label contains another. Provenance: **Implemented**. Which pairs collide was not exhaustively tested in this review.

## Edge cases

| Situation | Behaviour | Provenance |
|---|---|---|
| Unknown subdomain | Form replaced with "Please check subdomain" | Implemented |
| `getevent` fails | Form shell remains; no closed check, no categories | Implemented |
| `DateTo` is today | Registration closed | Implemented |
| `DateTo` missing or unparseable | `dateInPast` result is not a reliable boolean; the closed branch may or may not run | Inferred from `new Date(invalid)` producing `Invalid Date`. **Unknown** what the API sends when the date is absent |
| Placeholder day `00` or nationality `default` | Step gate can still pass | Implemented |
| Surname edited twice | Pass name appends the surname again | Implemented |
| Prefer not to say | Payload `gender` is `M` | Implemented |
| Organisation lookup fails | Search control can stay disabled | Implemented |
| Reference length 6 vs 12-character example | Auto-search fires at 6 | Implemented |
| `RequireOrganisationReference == "0"` | Reference row hidden; name is typed | Implemented |
| `MediaEvent == "0"` or no `Linked` | Events step hidden | Implemented |
| Identity Next after events hidden | Collapse target `#tab-4`, `onclick` still `setTabActive('3')`, extra attribute name `conclick` | Implemented |
| `setTabActive(4)` | Looks up `#tab5`, which is not in the page, and calls `setAttribute` on null | Implemented. Throws if that function runs |
| `setTabActive(n)` | Also marks tab `n+1` active on the first tab bar | Implemented |
| Crop without EDIT PHOTO | `profileImage.crop` throws | Implemented |
| Submit failure | Loader remains, buttons stay hidden | Implemented |
| Submit success | Reference and `info@mypass.zone`; CLOSE reloads | Implemented |
| Opened as `file://` | Theme script attaches a mouseover note that PWA and AJAX transitions need a server. The `console.log` in that handler is commented out | Implemented |
| Service worker serves stale `index.html` | Cache name is still `v1`. No version bump was found after the last form edits | Inferred from `CACHE_NAME` |

## Out of scope for this repository

- Creating or approving organisations and events.
- Sending the confirmation email.
- Showing application status, a pass, or a QR code.
- Sign-in, sign-up, forgot password, and health-pass screens drawn under `exports/`.
- The later `getstyling` flow in `myPass-registration`.

## Requires confirmation

1. Whether each `domains.js` event id is still a live accreditation, and whether the hosts are `{label}.mypass.zone`.
2. Whether `gender: "M"` for Prefer not to say is accepted by the API and by the business.
3. Expected organisation reference length, and what the client should do when `getorganisation` returns no row.
4. Whether `AccreditationNote` is still a Covid declaration or a general declaration. The HTML default is Covid wording; the live label is the API field.
5. Whether a confirmation email is sent, and whether `info@mypass.zone` is the right contact. `json.EMAIL` is ignored.
6. Whether publishing the theme catalogue and `.scannerwork` to the S3 bucket is acceptable.
7. Whether this client is retired in favour of `mypass-registration`.
