# PRD — SaruEfiling.com (SARU E-Filing)

Evidence date: 2026-10-04. Source: `git@bitbucket.org:overdrive/saruefiling.com.git`, `master` at `379437f8` (2023-03-07).

There is no prior PRD in the repository or in Silicon Ark. Requirements below were recovered from controllers, views, models, configuration, and commit messages. Provenance is marked on each item.

Secrets that are hardcoded (database password, a login bypass password, SMTP material, a GraphQL bearer token) are not copied into this document.

## Product purpose

SARU E-Filing lets rugby organisations complete structured annual returns, submit them so they lock, and record approval by a president and by a CEO (with a special case for one form). National users (lower `UnionType`) can list every organisation's forms, reopen a submitted form, and build form templates. Union users work on their own organisation.

The same signed-in application also manages personnel and organisations, shows a GIR gender overview, and administers plans, budgets, and grants. Older Footprint modules for squads, matches, scouting, and courses remain in the code.

Provenance: **Implemented**. A written product goal beyond the screen titles and mail signature was not found.

## Users and roles

| Role | What they do | Evidence | Provenance |
|---|---|---|---|
| E-filing user | Signs in with email and password when `EFLogin` is 1 | `LoginController::indexAction` | Implemented |
| New account | Lands on `/first` when profile `Status` is 0 and completes a player or personnel profile | `IndexController::init`, `FirstController` | Implemented |
| National / higher union user | `UnionType` less than 3. Dashboard lists projects. Can list forms across unions. Can reopen a submitted or completed form | `IndexController::indexAction`, `epg/form.phtml` | Implemented. The business name for each `UnionType` number is **Unknown / Requires Confirmation** |
| Union user | `UnionType` 3 or 11. Dashboard renders `index/union.phtml`. Form lists are forced to their own `UnionID` | `IndexController`, `EpgController::indexAction` | Implemented |
| Other union type | `UnionType` greater than 3 and not 11. Dashboard redirects to that person's own member profile | `IndexController::indexAction` | Implemented |
| President | `JobTitle` 35. Approve button writes president fields | `epg/form.phtml`, `approveformAction` | Implemented |
| CEO | `JobTitle` 23. Approve button writes CEO fields, except form 79 which also allows job title 22 | `epg/form.phtml`, commit `379437f8` | Implemented |
| Job title 22 | Treated as a CEO approver on form 79 and in `approveformAction` for every form | Commit message calls this "Administrator". The label is not looked up in code | Documented in commit `24aea552` / `379437f8`. Display name **Requires Confirmation** |
| Form owner group | User ids stored on the form as Owner, Manager, Coordinator, Administrator, plus hardcoded ids 582, 1007, and 74664, see an "Open Form" control | `epg/form.phtml` | Implemented |
| Menu-gated user | JSON `Role` keys `members`, `organisations`, `forms`, `grants` show those menus. Action names under each key show child links | `menu/default.phtml` | Implemented |
| Settings user | Access control, sent mail, and mail templates. The settings menu is included when `UnionType` is less than 3 | `default.phtml`, `menu/settings.phtml` | Implemented |
| Inactive profile | `Active` is 0. Header shows "Inactive". The main menu is not rendered. Settings can still render for `UnionType` < 3 | `default.phtml` | Implemented |
| Scout mobile client | `POST /app/login` with user and password. Succeeds only when the role JSON has `scout.Actions.app` | `AppController::loginAction` | Implemented. Whether a mobile app still calls it is **Unknown** |
| Anonymous visitor | `/login` only. Other traced controllers redirect to `/login/` | Controllers' `init` | Implemented |

`Role` is a JSON document of modules and action names, not a single role enum. `JobTitle` is a separate numeric field used for approval. Provenance: **Implemented**.

## Main workflows

### W1 — Sign in

Trigger: GET `/login`. Layout is disabled.

1. If Zend_Auth already has an identity, redirect to `/`.
2. POST `data[Email]` and `data[Password]`.
3. Load `co_credetials` where `Username` equals the email and `EFLogin` is 1. If that row is missing, the email is replaced with a random id so the later authentication cannot succeed.
4. Password is compared as `md5` of the typed password, unless the typed password equals a hardcoded bypass string, in which case the stored hash is submitted instead.
5. `Zend_Auth_Adapter_DbTable` authenticates against `co_credetials.Username` / `Password`.
6. On success, the profile row is written into the auth storage and `LastLogin` is updated. If profile `Status` is greater than 0, redirect to `/`. Otherwise redirect to `/first`.
7. On failure, if the username exists and the password column is empty, or if the email exists on `profile` but not on credentials, a reset key is stored and a reset email is sent. Otherwise the page shows "Email and/or password is invalid!".

Logout (`/login/logout`) clears the identity, destroys the session, and redirects to `/login/`.

Password reset (`/login/resetpassword` and `/login/reset/user/{key}`) stores an `md5` reset key and emails a link. The message text still says the Footprint password expired.

Provenance: **Implemented** (`LoginController`). The bypass string is a security defect; it is not repeated here.

### W2 — First profile

Trigger: profile `Status` is 0 after login, or a later check redirects to `/first`.

`JobType` 2 renders the player first-login form. Any other job type renders the personnel form. A successful post sets `Status` to 1, stores the full name, and redirects to `/`. An ID-copy upload accepts `jpg`, `png`, `jpeg`, `gif`, or `pdf`.

Provenance: **Implemented** (`FirstController`).

### W3 — Open the shell

Trigger: any request to `IndexController` without an identity redirects to `/login/`. With an identity and `Status` 0, redirect to `/first`.

A cookie `efilingApp` is set for one hour. The layout title is `SARU:: Efiling`. Header links: own profile when `Status` is not 0 (`/members/profile/id/{UserID}`), logout, and calendar (`/events/`). The main menu renders only when `Active` is 1. The settings menu renders when `UnionType` is less than 3.

Dashboard:

- `UnionType` < 3: current-year projects plus squad, school, and GIR ethnicity counts for the calendar year, month, and week.
- `UnionType` 3 or 11: union dashboard view.
- Otherwise: redirect to the member profile.

Provenance: **Implemented** (`IndexController`, `default.phtml`).

### W4 — List e-filing forms

Trigger: menu "Efiling Forms" goes to `/epg/index/year/{current year}` when `Role.forms.Actions.index` is set.

Query parameters: `id` (organisation), `year`, `type`, `form`, `status`. If `UnionType` is greater than 2, `id` is replaced with the user's own `UnionID`.

The list is `epg` joined to the union, filtered by those arguments, plus a dropdown of forms from `epg_forms`.

Provenance: **Implemented** (`EpgController::indexAction`). The action assigns the list only when `year` is set. A request without a year therefore has no list. Provenance: **Implemented**.

### W5 — Open and fill a form

Trigger: `/epg/form/epg/{form id}/unionid/{union id}`.

The action loads the `epg` row for that union and form, the union, the `epg_forms` definition, uploaded files, and approver profiles from `ApproveArray`.

Confidential forms (`epg_forms.Confidential` = 1): a user with `UnionType` greater than 2 is redirected back to `/epg/` unless their user id is in `MembersArray` or `ApproveArray`. Users with `UnionType` 2 or below are not blocked. When blocked is 1, the form script disables inputs after load.

The page then loads `/epg/loadform/unionid/{id}/id/{form id}` into the form body. That fragment renders the headers, sections, and HTML elements stored for the form, including yes/no/N/A selects, frequency selects, currency fields, and file upload controls.

Saving a page posts to `/epg/savedata` with the `epg_id`, a page key, and `status` omitted or not 1. The page's fields are merged into the JSON `formdata` column. Union types below 3 update `AdminDateModified` only. Other union types also set `DateCompleted` and `AdminUserID` on that save. The status column is left unchanged on a plain save.

Provenance: **Implemented** (`formAction`, `savedataAction`, `loadform.phtml`).

### W6 — Submit a form

Trigger: the submit control opens a modal: "If you click Confirm Button, this form will be locked and you will not have access to this form or make any changes to it." Confirm posts `/epg/savedata/epg/{epg_id}/status/1/page/{page}`.

`savedataAction` sets `status` to 1. For `UnionType` 3 and above it also sets `DateSubmitted` and `SubmittedBy`. Email notification calls in that action are commented out.

The header label is DRAFT when `status` is 0, SUBMITTED when `status` is 1, COMPLETED when `status` is 2.

Provenance: **Implemented** for status 0 and 1. No assignment of status 2 was found in `EpgController`. Where COMPLETED is written is **Unknown / Requires Confirmation**.

### W7 — Reopen a form

Two controls set the row back to draft:

- "Open Form" is shown when the user id is the form's Owner, Manager, Coordinator, or Administrator, or is 582, 1007, or 74664. It posts `/epg/draftform`, which sets `status` 0 and clears `PresidentID`, `ApprovedPresident`, and `ApprovedCEO`.
- A tick control (`activeForm`) is shown when `UnionType` is less than 3 and status is 1 or 2. It posts `/epg/activateform`, which sets `status` 0 and, when `AdminUserID` is set, sends email template `(2, 6)`. The alert text says "Form is now active" while the status value is the DRAFT value.

Provenance: **Implemented**. The two reopen paths do not do the same thing. Whether both are still intended is **Requires Confirmation**.

### W8 — Approve a form

The legend differs for form id 79:

- Form 79 shows Submitted and a single Approved tick (CEO flag).
- Every other form shows Submitted, President Approved, and CEO Approved.

Approve button:

- `JobTitle` 35, and this user is not already `ApprovedPresident`: button calls `approveForm()`.
- Form 79 and `JobTitle` 23 or 22, and this user is not already `ApprovedCEO`: same button.
- Any other form and `JobTitle` 23 only, and this user is not already `ApprovedCEO`: same button.

`approveForm()` alerts "Your form data is now approved" immediately, then posts `/epg/approveform` and reloads.

`approveformAction`:

- Job title 35 writes `PresidentID`, `ApprovedPresident`, and `PresidentApprovDate`.
- Job title 23 or 22 writes `CEO_ID`, `ApprovedPresident`, `ApprovedCEO`, and `CEOApprovDate`.
- Any other job title leaves the data array unset. The update then runs with an undefined array. Provenance: **Implemented**. Outcome of that request is **Unknown** (likely a PHP notice and no useful update).

The CEO path also writes `ApprovedPresident` to the same user id. Provenance: **Implemented**. Whether overwriting the president flag is intended is **Requires Confirmation**.

Approval does not change `status` in this action. The email block under approval is commented out (commit `822a86fe`, "Removed email notification").

### W9 — Download and upload

- Excel: `/epg/downloadexcel/unionid/{id}/epg/{form id}` from the form header.
- Report view and report Excel exist (`reportAction`, `reportexcelAction`) but the header links are commented out.
- Document upload on the form header is rendered only when the form's `FileUpload` is 1 and the user id is 1. Per-element upload was added in February 2023 (`ee73a84c`). Files post to `/epg/fileupload`.

Provenance: **Implemented**.

### W10 — Form templates

Menu "Form Templates" (`/builder/`) is shown only when `UnionType` is 1, or the user id is 582 or 1, and `Role.forms.Actions.financial` is set. "Create Form" (`/epg/add`) is additionally inside that block and is rendered with an empty label (the visible text is commented out).

`BuilderController` creates headers, subheaders, sections, and rows and saves targets.

Provenance: **Implemented**.

### W11 — GIR

The GIR menu is always rendered. Overview (`/members/giroverview`) loads male and female totals by province from `Gir_Report` and a manager list. Pivot is `/members/girpivot`.

`public/cron/getData.php?a=file` downloads programmes, regions, projects, institutions, and participants from the Global Performance GraphQL API into `temp.json`. `?a=db` reads that file into a MySQL database named `saruefiling` on `127.0.0.1`. `getManagers.php` does the same for organisation and project managers into `managers.json`.

Provenance: **Implemented** as scripts. Scheduling, and whether those rows are what the overview reads, is **Unknown / Requires Confirmation**. The bearer token in those files is hardcoded and its `exp` claim is 1609372800 (2020-12-31). The token is not copied here.

### W12 — Personnel, organisations, plan and budget

- Members list and add, gated by `Role.members`.
- Organisations list and create, gated by `Role.organisations`.
- Plan and budget (`/plan`, `/plan/manage`, grant categories and classifications) gated by `Role.grants` and, for the manage links, `UnionType` less than 3.

Creating a person or organisation inserts credentials with `EFLogin` 1 and an `md5` password (`MembersController`, `OrganisationsController`).

Provenance: **Implemented**.

## Functional requirements

### FR-001 — E-filing login gate

The system authenticates only credentials with `EFLogin` = 1, using an MD5 password, then stores the profile as the session identity.

Who: visitor. Trigger: POST `/login`. Output: redirect to `/` or `/first`, or an error or a reset email.

Evidence: `LoginController::indexAction`. Implementation: Implemented. Confidence: High.

### FR-002 — First-login profile

When `Status` is 0 the system collects a player or personnel profile and then sets `Status` to 1.

Evidence: `FirstController`, `IndexController::init`. Implementation: Implemented. Confidence: High.

### FR-003 — Union-scoped form list

Users with `UnionType` greater than 2 see only their union's forms. Users with a lower type may filter by organisation, year, form, and status.

Evidence: `EpgController::indexAction`. Implementation: Implemented. Confidence: High.

### FR-004 — Confidential form access

A confidential form is refused to union-level users who are not listed on the form's member or approver arrays. National users are not refused.

Evidence: `EpgController::formAction`. Implementation: Implemented. Confidence: High.

### FR-005 — Save and submit

A page save merges JSON into `epg.formdata`. Submit sets `status` to 1 and, for union-level users, records who submitted and when. The confirm copy says the form will be locked.

Evidence: `savedataAction`, `loadform.phtml`. Implementation: Implemented. Confidence: High.

The confirm copy is stronger than the code: national users can still reopen the form (FR-007), and a plain save does not check status before writing `formdata`. Provenance: **Implemented**. Whether a submitted form is actually read-only in `loadform.phtml` for the submitting user was not established field by field. **Requires Confirmation** if a tester needs a lock contract.

### FR-006 — Approval by job title

President (`JobTitle` 35) records president approval. CEO (`JobTitle` 23) records CEO approval. On form 79 the approve button is also shown for job title 22. The server action records a CEO approval for job title 22 on every form, not only form 79.

Evidence: `epg/form.phtml`, `approveformAction`, `379437f8`. Implementation: Implemented. Confidence: High.

Discrepancy: the button and the server do not use the same form-79 limit. **Requires Confirmation** which behaviour is the rule.

### FR-007 — Reopen to draft

Named form officers and three hardcoded user ids can clear approvals and set status 0. National users can set status 0 on a submitted or completed form without clearing approval columns.

Evidence: `draftformAction`, `activateformAction`, `form.phtml`. Implementation: Implemented. Confidence: High.

### FR-008 — Form 79 legend

Form 79 shows one Approved indicator (the CEO flag). Other forms show president and CEO indicators separately.

Evidence: `epg/form.phtml`. Implementation: Implemented. Confidence: High. The product reason for form 79 is not documented. **Unknown / Requires Confirmation**.

### FR-009 — GIR overview

A signed-in user can open a province gender overview and a pivot without a role-menu check.

Evidence: `menu/default.phtml`, `MembersController::giroverviewAction`. Implementation: Implemented. Confidence: Medium (the overview action checks identity; the menu does not hide the link).

### FR-010 — Role menu

Members, organisations, forms, and plan/budget appear only when the matching key exists on the user's `Role` JSON.

Evidence: `menu/default.phtml`. Implementation: Implemented. Confidence: High.

The controllers were not all checked for a matching server-side refusal. A person who knows the URL may still open a screen the menu hides. **Requires Confirmation** per controller before treating the menu as an access-control boundary.

## Integrations

| System | Purpose | Direction | Auth | Failure |
|---|---|---|---|---|
| MySQL `teamwizz_SARU` on `db.footprintapp.net` | All application data | App reads and writes | Username and password in `application.ini` | Zend DB exceptions. Several model methods print or return false |
| AWS CodeDeploy via Bitbucket Pipelines | Deploy QA from `development`, production from `master` | Push from CI | Pipeline variables, not in git | Script prints the boto3 error and returns false |
| Global Performance GraphQL `admin-api.globalperformance.co.za` | Import programmes, participants, managers | Outbound POST | Hardcoded bearer token in the cron scripts | curl error string echoed. Token expiry claim is in 2020 |
| SMTP | Reset mail and form mail | Outbound | Inside mail classes; not copied here | Login reset still attempts send. Several notification calls are commented out |
| Browser calendar | `/events/` | In-app | Session | Not traced beyond the header link |

## Constraints

- Timezone is `Africa/Johannesburg` in `public/index.php` and in `EpgController`.
- Locale default in configuration is `af_ZA`. Visible form labels sampled in the form view are English. Provenance: **Implemented**. Effect of the locale on formatting is **Unknown**.
- Passwords are stored as MD5. There is no lockout, captcha, or password-complexity check on the login action.
- `application.ini` is listed in `.gitignore` but the file is present in the clone, including production credentials. Treat the ignore rule as not effective for the copy that is already tracked. Provenance: **Implemented**.
- No automated test suite exists. Files named `*test*` are alternate screens or debug actions.

## Conflicts to confirm

1. Form 79 hides the president legend and shows the CEO approve button for job titles 23 and 22. `approveformAction` grants the CEO write path to job title 22 for every form, and that path also writes `ApprovedPresident`.
2. Status label COMPLETED (`status` 2) has no writer in `EpgController`. Submit writes 1. Reopen writes 0.
3. `activateform` tells the user the form is "active" and stores the draft status.
4. Reset email copy says Footprint. The product title says SARU E-Filing.
5. Menu role checks are not, by themselves, proof that the controller refuses the same user.
