# Business rules — permits.magus.events

Evidence date: 2026-10-07. Source revision: `master` at `4617ab1`.

Rules below are taken from code, views, or email and FAQ copy. Each rule is marked **Implemented**, **Documented**, or **Inferred**. A rule that the code does not enforce is not written as if it were enforced.

Secrets are not copied here.

## Access

### BR-001 — One shared secret opens the desk

**Implemented.** `LoginController::indexAction` compares the posted `password` to one hardcoded value. A match stores event id `45` in session namespace `EVENT` and redirects to `/`. Any other value shows "Bad entered credentials". There is no user account on this form, no attempt counter, and no captcha.

The view heading calls the field a PIN. The input placeholder says "Password".

### BR-002 — Home is the walk-in list

**Implemented.** `IndexController::init` redirects to `/login/` when `EVENT.EID` is missing, and to `/walkins` when it is set. The dashboard action is not reached.

### BR-003 — Walk-in and permit actions require the event session

**Implemented.** `WalkinsController::init` and `PermitsController::init` redirect to `/login/` when `EVENT.EID` is missing. They do not check `Zend_Auth`.

### BR-004 — Company admin requires a different login that this form does not perform

**Implemented.** `UsersController` and `CompanyController` require `Zend_Auth`. `LoginNow` would authenticate `CompaniesLogin.Email` and `CompaniesLogin.password` and is not called. After the shared secret is accepted, a visit to `/users` or `/company` redirects to `/login/`, which sees `EID > 0` and returns the browser to the desk.

**Inferred:** the email-and-password login is leftover from the supplier portal and is not the live gate of this tree.

### BR-005 — The event id can be set from the query string

**Implemented.** `LoginController::externalAction` assigns `eid` to `EVENT.EID` and redirects to `/walkins`. It does not check the shared secret.

### BR-006 — Session lifetime

**Implemented.** `application.ini` sets `resources.session.cookie_lifetime = 864000` (10 days). Logout destroys the whole session.

### BR-007 — Company passkey

**Implemented.** The company card puts `Company_Events.passkey` in the button attribute `ref`. The browser allows `/walkins/new` only when the typed value equals that attribute. `CompanyEvents::getRow` loads the row only when `CEID` and `passkey` both match.

**Inferred:** the passkey is a desk PIN for that company, not a password stored as a hash. Registration does not write `passkey`. Who creates it is **Unknown / Requires Confirmation**.

The company list does not filter on `Company_Events.Status`. Any company row joined to the session event is shown.

## Registration and people

### BR-008 — A company is unique by registration number

**Implemented.** `RegisterController::check_Company` rejects a POST when `Companies_Profile.RegistrationNumber` already exists. The JSON message says the account already exists and to contact SARU. The same message is used when the person already exists.

### BR-009 — A person is unique by ID number or email

**Implemented.** `check_IDNumber` matches `Company_Members` on `IDNumber` or `Email`.

### BR-010 — The first user is an administrator

**Implemented.** The inserted member gets `Rol = Administrator`, an MD5 password, `DateCreated` of today, and `Ref` from 3 random bytes shown as uppercase hex.

**Documented.** The FAQ says "only one administrator is allowed." `UsersController` can insert more members, but only behind `Zend_Auth`, which this login does not create.

### BR-011 — South African IDs

**Implemented.** If the company country string is exactly `SOUTH AFRICA`, `IDType` is `1`. Otherwise it is `2`. No checksum of the ID number was found.

### BR-012 — The new company is active and tied to event 44

**Implemented.** `Create_Company` sets `Active = 1`, `CreatedBy` and `AdminID` to the new member, `CreatedDate` to today, and `RefNumber` from 6 random bytes as hex. `Company_Events` is inserted with `EID = 44` and `Status = 2`. No passkey is stored.

The desk lists companies for event `45`. A company created by this registration form is not on that list.

**Inferred:** event `44` and event `45` were not updated together. Whether that split is intentional is **Unknown / Requires Confirmation**.

### BR-013 — Email confirmation versus the 48-hour review

**Documented.** The registration email says an administrator will review the company before it is activated, and that activation can take up to 48 hours. It also asks the person to click a link.

**Implemented.** There is no review queue in this repository. `validateemailAction` sets `Email_Confirmed = 1`, `Email_Confirmed_Date` to today, and `AllowLogin = 1` as soon as `Validate_Key` matches, and it clears the key.

The link in the email is `http://supplier.sarugby.online/register/validateemail/id/{key}`, not this host.

### BR-014 — Password reset confirms the email as well

**Implemented.** A successful reset clears `ResetKey`, stores `md5` of the new password, and sets `AllowLogin = 1` and `Email_Confirmed = 1`. The email contains the new password in clear text. The reset link is `http://{HTTP host}/login/reset/user/{key}`.

An unknown email on the forgot form does not reveal that the address is missing.

### BR-015 — Staff passwords

**Implemented**, on the unreachable staff screens. If `AllowLogin` is posted for a new member, the password is `md5` of 3 random bytes. On edit, a new password is generated only when the stored `AllowLogin` is `0` and the form posts `AllowLogin`. Otherwise `AllowLogin` is forced to `0` when the field is absent.

Staff role labels are Administrator, Director, Management, Operations, Driver, Technician, General Staff. No action checks `Rol` before allowing a request.

### BR-016 — Permanent staff on a permit are rewritten from the member table

**Implemented.** On add and save, an occupant or driver whose type is `1` and whose ID number matches `Company_Members.IDNumber` is stored with that member's first name, surname, and ID number. Casual staff (type `2`) are stored as typed. A driver is stored with `Driver = 1`. Other occupants are stored with `Driver = 0`.

## Permits

### BR-017 — A new request is status 2

**Implemented.** `addpermitAction` sets `Status = 2`, `EID` from the session, and `CID` from the URL. `PermitRefSerial` is 13 random digits. `DateCreated` is the current timestamp.

### BR-018 — The stored permit number is `PM` plus the id

**Implemented.** `getRef` builds an accreditation prefix plus a zero-padded count. `addpermitAction` then replaces `PermitNr` with `PM` plus the new `PID` zero-padded to 6 digits. The value that remains is the `PM` form.

### BR-019 — One request is for one date

**Documented.** The FAQ says a permit cannot cover more than a day or a week, and that each vehicle needs its own permit per day. Duplicate-from-a-previous-day is described there.

**Implemented.** The form has one `RequestDate`. Nothing in `addpermitAction` rejects a multi-day range, because the form does not submit one. `duplicatepermitAction` exists and is not posted by the walk-in views.

### BR-020 — Load-in and load-out dates come from the event

**Implemented.** `Accreditation_List.LoadinActiveDate` and `LoadOutActiveDate` are JSON arrays of `d/m/Y` strings. The walk-in form keeps a date only when it is today or later (`Africa/Johannesburg`). Load type `1` shows load-in dates. Load type `2` shows load-out dates.

`PermitsController::createAction` builds the same lists and does not drop past dates. That action is not the walk-in form.

### BR-021 — Delivery length and work permits

**Documented.** The FAQ says a delivery permit cannot be longer than 2 hours and is limited in access areas. A work permit is for contracted work and requires a health and safety file. The confirmation email repeats a 14-point file list for work permits.

**Implemented.** The form stores `ServiceType` as the selected `Accreditation_Load_Services_Types` id. No server check limits duration to two hours or requires a file upload. No health-and-safety approval action exists in this tree.

### BR-022 — Status meanings on screen

**Implemented** as labels in `index/index.phtml` and `permits/profile.phtml`:

| Value | Label on screen |
|---|---|
| below 4 | Pending |
| 4 | Approved |
| 5 | Cancelled |
| 6 | Declined |
| 7 | Checked-In |
| 8 | Delayed |
| 9 | Checked-Out |

This repository writes `2` (create, duplicate) and `5` (cancel). It does not write 4, 6, 7, 8, or 9.

**Inferred:** approved, declined, checked-in, delayed, and checked-out are written by another system that shares `Accreditation_Permits`. That system was not found in this repository.

### BR-023 — Cancel

**Implemented.** `cancelAction` sets `Status = 5`, `CancelledDate` to now, `CancelledBy` to `$this->identity->MID`, and `CancelMessage` from the request. It prints the payload. It does not send the email the comment mentions.

The identity is unset in this controller, so the assignment does not complete as written.

### BR-024 — Cutoff for changes

Three different cutoffs are in the tree:

| Place | Rule | Provenance |
|---|---|---|
| Confirmation email | Cancellations and changes may be submitted until 1pm. Changes for the next day after that time will not be accepted | **Documented** |
| `PermitsController::profileAction` | Lock flag when `RequestDate` at 13:00 the previous day is already past | **Implemented** as a view flag. Save does not read the flag |
| Dashboard rows with status below 7 | Lock when `RequestDate` at 10:30 the previous day is already past | **Implemented** in `index/index.phtml`. That view is not reached |

Which cutoff is the business rule is **Unknown / Requires Confirmation**.

### BR-025 — Administrators may change the requested slot

**Documented.** The confirmation email says administrators may modify the request date and time to balance load, and that load-out is approved first come, first served.

No action in this repository changes another company's date, balances zone weight, or approves a permit. Zone weight colouring exists in the unreachable dashboard view.

### BR-026 — A printed permit and identification are required at the gate

**Documented** in the FAQ. Not enforced by this application. The FAQ also says a permit can be printed at the operations centre, and that the gate is printed on the issued permit.

### BR-027 — Download only after approval

**Documented.** The FAQ says the permit document can be downloaded from a menu next to the approved field only after approval.

**Implemented** as an unreachable action: `getpermitpdfAction` would fetch HTML from footprintapp.net. The dashboard that would host the menu does not run. No check for status `4` was found inside `getpermitpdfAction`.

### BR-028 — Draft versus submit

**Documented.** The FAQ says a request can be saved incomplete and is reviewed only once submitted.

**Implemented.** `addpermitAction` always sets status `2` and attempts the review email. There is no separate draft status in that action. `savepermitAction` updates a row without changing status, and it depends on an identity this controller does not have.

### BR-029 — Courier and subcontractor permits

**Documented.** A courier is a delivery permit under the company name, sent to the courier after approval. A contracted company is included in the scope of work and in the health and safety file. No separate courier entity exists in the form.

### BR-030 — Vehicle weight

**Documented.** Weight is the vehicle plus its contents, in kilograms, taken from the vehicle plate or the owner's manual. The form field is "Estimated Load Weight (Kg)". The server does not check that it is numeric.

## Contacts named in copy

| Copy | Address or name | Provenance |
|---|---|---|
| Registration, reset, and permit mail | `accreditation@sarugby.co.za`, phone `+27219287000`, sign-off "SA Rugby Event Administration" | **Documented** and used as the SMTP from-address |
| FAQ health and safety | `HealthandSafety7s@sarugby.co.za` | **Documented** |
| FAQ security | `security@sarugby.co.za` | **Documented** |
| FAQ named person | "Danie xxxxxx" (the source redacts the rest) | **Documented** |
| Layout footer | "© Sun International" | **Implemented** in `default.phtml` |
| Login title | "SA Rugby - Load-in and Load-out" | **Implemented** |
| FAQ event name | "HSBC World Rugby Sevens Series" | **Documented** |

## Conflicts that need a human decision

1. The desk uses event `45`. Registration attaches the company to event `44`.
2. The email promises an administrator review of up to 48 hours. Confirmation sets `AllowLogin` immediately, and it does so on a different host than this site.
3. The FAQ allows only one administrator. The staff controller can add more people, and the PIN login cannot reach that controller.
4. The FAQ limits a delivery to two hours. The server does not.
5. Change cutoffs are 10:30 the day before, 13:00 the day before, and "1pm" in the email.
6. The login title is SA Rugby. The footer is Sun International. The FAQ names the Sevens Series. The database is `teamwizz_SARU`.
7. Permit mail is attempted with `$this->identity`, which the walk-in controller never sets. The insert is ordered before that read.
8. `permits.mypass.zone` documents the same desk with CodeDeploy and a localhost database. This tree has neither. Whether both hostnames were meant to stay in service is **Unknown / Requires Confirmation**.
