# Business rules — permits.mypass.zone

Evidence date: 2026-10-07. Source: `master` at `16d1793`.

Rules are labelled:

- **Documented** — written in email copy, FAQ text, or a view label.
- **Implemented** — enforced or performed by code.
- **Inferred** — a reasonable reading of the code, not stated as a rule.

Do not treat an inferred rule as an approved business rule. Conflicts are listed at the end.

Secrets (PIN, database password, SMTP password, a sample identity number, a test mailbox) are not copied here.

## Access

### BR-001 — Shared PIN opens one event

**Implemented.** `LoginController::indexAction` accepts a single hardcoded PIN. A match stores event id `45` in session namespace `EVENT` and redirects to `/`. Any other posted value shows "Bad entered credentials".

The person who holds the PIN is not named. **Unknown / Requires Confirmation.**

### BR-002 — Event session is required for the desk and for permits

**Implemented.** `WalkinsController`, `PermitsController`, and `IndexController` redirect to `/login/` when `EVENT.EID` is missing.

`IndexController` then redirects to `/walkins` when `EID` is present, so the home URL is the company picker, not the dashboard.

### BR-003 — A second entry sets the event without the PIN

**Implemented.** `/login/external` copies query parameter `eid` into `EVENT.EID` and redirects to `/walkins`.

Whether another system is allowed to call this is **Unknown / Requires Confirmation**.

### BR-004 — Company passkey gates the permit form

**Implemented.** The company list embeds `Company_Events.passkey` on each button. The modal accepts the form only when the typed value equals that attribute. `/walkins/new` loads the company only when `CEID` and `passkey` match.

**Inferred.** An empty passkey matches an empty modal field in the browser, and `getRow` would also match an empty passkey. Registration does not set `passkey`. Who issues passkeys is **Unknown / Requires Confirmation**.

### BR-005 — Company admin screens need a different login

**Implemented.** `UsersController` and `CompanyController` require `Zend_Auth` identity and redirect to `/login/` without it. The PIN login does not write that identity.

`LoginNow` checks email and password against `CompaniesLogin` using the stored password column as the credential. The adapter does not apply an extra hash at login time. Registration stores `md5`. **Inferred** that a matching login would require the stored value to equal the submitted credential as `Zend_Auth_Adapter_DbTable` compares them. The method is never called.

### BR-006 — Logout clears the session

**Implemented.** `/login/logout` clears `Zend_Auth` and destroys the PHP session.

### BR-007 — Session lifetime

**Implemented** in config. `resources.session.cookie_lifetime = 864000` (10 days). No idle timeout was found in the controllers.

## Registration and people

### BR-008 — One registration per company number, ID number, or email

**Implemented.** `RegisterController::newcompanyAction` rejects the post when `Companies_Profile.RegistrationNumber` already exists, or when `Company_Members` already has that ID number or email. The message tells the person to contact SARU. It does not say which of the three fields collided.

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

**Implemented.** The inserted member gets `Rol = Administrator`.

**Documented.** The FAQ says "No, only one administrator is allowed."

**Conflict.** `UsersController::addAction` can insert more members for the same `CID`, with a role chosen from the fixed list. That screen is not reachable from the PIN login. See the conflict list.

### BR-010 — South African ID type

**Implemented.** If the company country equals `SOUTH AFRICA`, the member `IDType` is `1`. Any other country sets `IDType` to `2`. No checksum is applied to the ID number.

### BR-011 — New companies are active and linked to event 44

**Implemented.** The company row is inserted with `Active = 1`. `Company_Events` is inserted with `EID = 44` and `Status = 2`. No passkey is written.

**Conflict.** The desk lists companies for event `45`. A company created only by this form does not appear on the desk.

### BR-012 — Email confirmation

**Documented** in the registration email: the person must click a link, and an administrator reviews the company before the profile can be activated. The email says the review can take up to 48 hours, and that extra documents are required before the company can connect to an event.

**Implemented.** The link points at `http://supplier.sarugby.online/register/validateemail/id/{Validate_Key}`. This application's own `validateemailAction` sets `Email_Confirmed = 1`, `AllowLogin = 1`, and today's `Email_Confirmed_Date`, and clears the key. There is no 48-hour timer and no administrator approval action in this repository.

The success JSON says only that a verification email was sent. It does not mention the 48-hour review.

### BR-013 — Password storage and reset

**Implemented.**

- Registration stores `md5` of the submitted password.
- Forgot-password stores `ResetKey` as `md5` of 4 random bytes and emails a link on the current host.
- Reset stores `md5` of the new password, clears the key, sets `AllowLogin = 1` and `Email_Confirmed = 1`, and emails the new password in clear text.
- Adding a staff member with `AllowLogin` set stores `md5` of a 3-byte random id. The new password is not emailed by `UsersController`.

No length or complexity rule is enforced on the server. The reset view asks the browser to confirm the two fields match.

### BR-014 — Staff roles

**Implemented** as labels only: Administrator, Director, Management, Operations, Driver, Technician, General Staff. No action checks `Rol` before allowing an operation.

### BR-015 — Occupant type 1 is a known member

**Implemented.** On permit create and save, an occupant or driver with `Type == 1` is looked up in `Company_Members` by ID number. A hit replaces the submitted name, surname, and ID with the member row and stores `MID`. A miss keeps the submitted values and `MID` `0`.

Type values other than `1` are not looked up. The meaning of the other type codes is **Unknown / Requires Confirmation** (the form emits them; this repository does not define them in PHP).

## Permits

### BR-016 — A new request starts at status 2

**Implemented.** `addpermitAction` and `duplicatepermitAction` set `Status = 2`.

The dashboard badge treats any status below `4` as Pending. Status `2` is therefore shown as Pending on that screen. The dashboard is not reachable after the PIN login.

### BR-017 — Permit number

**Implemented.** After insert, `PermitNr` is `PM` plus the new `PID` padded with zeros to 6 digits.

`getRef` can build a different number from the accreditation prefix and a count of existing rows. `addpermitAction` replaces that value. `duplicatepermitAction` keeps the `getRef` value and does not apply the `PM` format.

**Inferred.** Live walk-in permits use the `PM` number. Duplicated permits would use the prefix number if duplicate were reachable.

### BR-018 — One serial alongside the number

**Implemented.** `PermitRefSerial` is 13 random digits, stored separately from `PermitNr`. The email shows `PermitNr` and the year of `RequestDate`, not this serial.

### BR-019 — Load-in and load-out

**Documented** in the FAQ. A load-in permit is for the period before and during the event. A load-out permit is for post-event collection, removal, or work.

**Implemented.** The form offers `LoadType` `1` (Load-In) and `2` (Load-Out). The date list is the matching JSON field on the event (`LoadinActiveDate` or `LoadOutActiveDate`). Walk-in dates before today are omitted.

The confirmation email chooses the words "Load-Out" when `$permit["Surname"] == 2`, otherwise "Load-In". That is not a read of `LoadType`. **Inferred** defect in the email wording.

### BR-020 — One permit per vehicle per day

**Documented** in the FAQ. "You are required to create a permit for each day for each vehicle." A duplicate action exists so a previous request can be copied to another day.

**Implemented.** Nothing in `addpermitAction` rejects a second permit for the same vehicle, date, or company. Duplicate is a separate action and depends on `Zend_Auth`.

### BR-021 — Delivery permits are at most two hours

**Documented** in the FAQ. "A Delivery Permit is for deliveries only and cannot be scheduled for a period longer than 2 hours. Delivery permits are also limited in their access areas. A Work Permit is for suppliers or service providers that need to complete a job... A work permit requires the organisation to have their Health and Safety File."

**Not implemented** in `addpermitAction`. Time in and time out are stored as submitted. No action compares them to a two-hour limit or branches on a delivery service type.

### BR-022 — Changes close at 1pm the day before

**Documented** in the confirmation email. "CANCELLATIONS/CHANGES may be submitted up to 1pm daily. Any changes to permit requests for the next day after this time will not be accepted."

**Implemented** as a flag only. `profileAction` and `profiletestAction` set `LOCKED` to `1` when `RequestDate` at 13:00 minus one day is in the past. Timezone is `Africa/Johannesburg`.

The unreachable dashboard uses 10:30 the previous day when status is below `7`, and only to choose a row style. **Conflict** with the 13:00 email rule.

`addpermitAction` does not read `LOCKED` and does not reject a late create. The walk-in form does not receive `LOCKED`.

### BR-023 — Cancel sets status 5

**Implemented.** `cancelAction` sets `Status = 5`, `CancelledDate`, `CancelledBy` (member id), and `CancelMessage`. The dashboard badge for status `5` is "Cancelled".

The action depends on `Zend_Auth`, which the desk does not have.

### BR-024 — Other statuses

**Implemented** as labels on the unreachable dashboard only:

| Code | Badge text |
|---|---|
| `< 4` | Pending |
| `4` | Approved |
| `5` | Cancelled |
| `6` | Declined |
| `7` | Checked-In |
| `8` | Delayed |
| `9` | Checked-Out |

No action in this repository sets `4`, `6`, `7`, `8`, or `9`. The same template counts status `4` as both approved and declined. **Requires Confirmation.**

**Documented** in the email: an administrator reviews the request and may change the date and time to balance load. That review is not in this repository. **Inferred** to live in another application that shares `Accreditation_Permits`.

### BR-025 — Printed permit and identification

**Documented** in the FAQ. Entry requires a valid permit and a Vehicle Access Pass. A printed permit is required. Every person needs identification. The gate is printed on the issued permit. Courier deliveries need a delivery permit sent with the waybill. A subcontractor is applied for under the contracting company's name and included in that company's health and safety file.

None of these checks exist in this application.

### BR-026 — Health and safety file

**Documented** in the email and the FAQ. Work at the venue requires a file. The checklist has 14 items (37(2) agreement, public liability insurance, site induction, letter of good standing, health and safety plan, legal appointments, tools and equipment, PPE, risk assessment, toolbox talks, MSDS where chemicals are used, medical records, training records, professional registration, safe work procedures, emergency procedures). The FAQ says a properly labelled file can be approved in up to an hour.

**Not implemented.** There is no file upload and no approval state for a safety file.

The FAQ names `HealthandSafety7s@sarugby.co.za` for health and safety and `security@sarugby.co.za` for security. One answer says "Please contact: Danie xxxxxx" with the surname redacted in the source.

### BR-027 — Vehicle weight

**Documented** in the FAQ. Weight is the vehicle plus its contents, in kilograms, taken from the vehicle or the owner's manual.

**Implemented.** The form has vehicle weight, vehicle height, and optional trailer weight and height. No range check was found.

### BR-028 — First come, first served for load-out

**Documented** in the email. "Load-out time is restricted and approved on a first come first serve basis." No capacity check was found in `addpermitAction`. The dashboard estimates zone weight against parking-bay capacity, and that screen does not run after login.

## Contacts named in copy

| Use | Value | Where |
|---|---|---|
| Accreditation enquiries | `accreditation@sarugby.co.za` | Registration email, permit email, password emails, FAQ |
| Registration contact centre | `+27219287000` | Registration email, permit email |
| Health and safety | `HealthandSafety7s@sarugby.co.za` | FAQ |
| Security | `security@sarugby.co.za` | FAQ |
| Mail from address | `accreditation@sarugby.co.za`, display name "Registration at sarugby.co.za" | `sendMail` in login, register, permits, and index |
| Email subject | "SARugby Notification" | Those `sendMail` methods |
| Sign-off | "SA Rugby Event Administration" | Email bodies |

## Conflicts that need a human decision

1. **Event id.** The desk uses `45`. Registration writes `44`. A new company is not on the desk list. Which id is correct?
2. **Home screen.** The dashboard code is the operations view. `init` sends every logged-in session to the walk-in company picker. Which screen should `/` show?
3. **Status 4.** The badge says Approved. A counter in the same file treats `4` as declined, and status `6` is the Declined badge. Which code is declined?
4. **Cutoff time.** The email and the profile flag use 13:00 the day before. The dashboard uses 10:30 the day before. Which cutoff applies, and should create enforce it?
5. **One administrator.** The FAQ forbids a second administrator. The staff screen can add one. The staff screen is unwired. Which statement stands?
6. **Email load type.** The email treats surname `2` as load-out. The form stores load type on `LoadType`. The email line should be confirmed before anyone relies on it.
7. **Approval.** Mail says an administrator will approve the permit and the company. This repository has no approval action. Confirm which system performs it.
8. **Passkey owner.** Confirm who creates `Company_Events.passkey`, and whether an empty passkey is allowed.
