# Business rules — magus-core

Evidence date: 2026-10-09. Source: `git@bitbucket.org:overdrive/magus-core.git`, `master` at `f806a7e` (2019-08-19), and `admin_magus.sql` (generated 2019-03-15, database `admin_magus`).

No business-rules document existed in the repository. Rules below are taken from code or from seed rows. Each rule is marked **Implemented** or **Inferred**. Where two sources disagree, the disagreement is the finding.

Secret values (SMTP password, Google API key, login literal, identity numbers, user rows) are not repeated here.

## Identity and access

| ID | Rule | Label |
|---|---|---|
| BR-001 | A person signs in with `users.Email` and `users.password`. The stored password is an MD5 digest, except when the submitted password equals the literal in `LoginController::indexAction`, in which case the digest step is skipped. | **Implemented** |
| BR-002 | Login does not check `AllowLogin` or `Email_Confirmed`. | **Implemented** |
| BR-003 | Any authenticated identity may open companies, users, venues, events, accreditation, roles, types, permits, and the dashboard. Role id is not consulted. | **Implemented** |
| BR-004 | Logged-in visitors who open `/login` are sent to `/`. | **Implemented** |
| BR-005 | Session cookie lifetime is 864000 seconds (10 days) in production, staging, testing, and development. | **Implemented** |
| BR-006 | Logout clears Zend_Auth and destroys the session. | **Implemented** |
| BR-007 | `/geo/*` address lookups require no login. | **Implemented** |
| BR-008 | Company image fetch and remote company image upload require no login. | **Implemented** |
| BR-009 | Load-in without an identity redirects to `/companies/login/`. That action does not exist on `CompaniesController`. | **Implemented** redirect; missing target |
| BR-010 | Seeded user roles are Administrator, Director, Management, Operations, Driver, Technician, and General Staff. RID 4 is not in the dump. `UserRoles.Active` is 0 for all of them and is not checked before use. | **Implemented** seed; unused flag **Inferred** as not enforced |

## Registration

| ID | Rule | Label |
|---|---|---|
| BR-011 | A company cannot be registered when `RegistrationNumber` already exists. | **Implemented** (lookup class `Model_CompanyProfile` is missing from the repo) |
| BR-012 | A user cannot be registered when `IDNumber` or `Email` already exists. The same message is returned as for a duplicate company: contact SARU. | **Implemented** |
| BR-013 | The registering user is stored with role string `Administrator`, not `UserRoles.RID` 1. The user list joins `UserRoles.RID = users.Rol`. | **Implemented**. Match to the roles table is **Inferred** to fail for that string |
| BR-014 | Country value `SOUTH AFRICA` sets `IDType` 1. Any other country string sets `IDType` 2. | **Implemented** |
| BR-015 | New users and companies receive random refs from `mcrypt` (`Ref` 3 bytes hex, company `RefNumber` 6 bytes hex, validation key 8 bytes hex), uppercased. | **Implemented** |
| BR-016 | The new company is linked to event id 44 with status 2 via `Model_CompanyEvents`. | **Implemented** call. Class and table `Company_Events` are not in this repo or dump. Which event 44 is: **Unknown / Requires Confirmation** |
| BR-017 | Registration email tells the supplier that an administrator reviews the company and that activation can take up to 48 hours. | **Documented** in the email body. Not enforced by a timer or an approval flag in this code |
| BR-018 | Registration email says the company cannot sign up to an event without additional documents. | **Documented** in the email body. No document gate was found in the registration action |
| BR-019 | Confirming `Validate_Key` clears the key, sets `Email_Confirmed` and `Email_Confirmed_Date`, and sets `AllowLogin` to 1. The page then says the account is active. | **Implemented**. Conflicts with BR-017 |
| BR-020 | Company add inside the signed-in area repeats BR-012, BR-014, and the MD5 password step. The event-44 insert is commented out there. | **Implemented** |

## Passwords

| ID | Rule | Label |
|---|---|---|
| BR-021 | Forgot-password finds the user by email and stores an MD5 reset key. The link uses the current request host, not the hardcoded `supplier.sarugby.online` host used at registration. | **Implemented** |
| BR-022 | Reset with a known key replaces the password (MD5), clears the key, and sets `AllowLogin` and `Email_Confirmed` to 1. | **Implemented** |
| BR-023 | The email after reset includes the new password in clear text. | **Implemented** |
| BR-024 | An unknown reset key does not change a user. A request with no `user` parameter redirects to `/login`. | **Implemented** |

## Companies, types, venues

| ID | Rule | Label |
|---|---|---|
| BR-025 | Company types are Suppliers, Consultants, Service Providers, Distributors & Manufacturers, Agencies, Institutions, and Administrators. Only Administrators has `companyType.admin = 1`. | **Implemented** seed. The `admin` flag is not read by the type or company controllers that were inspected. **Inferred** unused |
| BR-026 | B-BBEE choices are Level 1 through Level 7. | **Implemented** seed |
| BR-027 | Company list search is case-insensitive on legal name, trading name, registration number, registration date, VAT, type name, and country name. Optional filter: `CompanyType`. Default order: legal name. | **Implemented** |
| BR-028 | Venue status labels: 1 Active, 2 Inactive, any other value Abandoned. | **Implemented** in `VenuesController` list HTML |
| BR-029 | Venue save resolves province, city, and suburb by name plus country, and splits a comma-separated coordinate string into latitude then longitude. | **Implemented** |

## Accreditations and events

| ID | Rule | Label |
|---|---|---|
| BR-030 | The application event record is `Accreditation_List`, keyed by `AC`. `Model_Events` does not use the `Events` table. | **Implemented** |
| BR-031 | List status is Closed when the date test fails. Inside the open window, `Active = 1` means Active; otherwise Inactive. Closed rows cannot be edited or deleted from the list actions. Inactive rows cannot be edited. | **Implemented** in `AccreditationController::datatable` |
| BR-032 | The open-window test compares `DateTo` with `strtotime` of today's date formatted `d-m-Y`. | **Implemented**. Whether that format was the intended calendar rule is **Unknown / Requires Confirmation** |
| BR-033 | `BarcodeOption` comment: 0 Reference, 1 Payload, 2 Serial Range, 3 GateKeeper. `PostExternal` comment: 0 no post, 1 FS Stadium Management. | **Documented** as column comments only. No writer of those codes was found in the controllers read |
| BR-034 | `BlockEmails` and `BlockZones` default to 0. `Hidden` defaults to 0. | **Implemented** as column defaults. Enforcement in PHP was not found |
| BR-035 | Accreditation list order in `getAll` is `DateFrom DESC`. | **Implemented** |

## Permits

### Status catalogue in the database

`Permit_Status` seed:

| SID | Value | Note in the dump |
|---|---|---|
| 1 | Incomplete | New with missing information |
| 2 | New | New application all data field completed |
| 3 | Confirmed | Confirmed application — might still be changes by admin |
| 4 | Approved | Approved application |
| 5 | Cancelled | Cancelled by applicant |
| 6 | Declined | Declined by admin |
| 7 | Checked-In | Vehicle checked-In |
| 8 | Delayed | Vehicle delayed |
| 9 | Checked-Out | Vehicle checked-Out |

Provenance: **Implemented** seed data. `Model_AccreditationPermits::getStatus` reads this table.

### Status numbers in EventsController

The permit grid still contains this behaviour (not inside the comment block):

| Code in the grid | UI |
|---|---|
| 7 | Menu text "Checked-Out" |
| 4 | Menu action "Checked-In" |
| 5, or less than 3 | "Delete Permit" is offered |
| any | "View Permit" |
| row has `Mobile` | "Send SMS" is offered |

A commented statistics block, which does not run, counts: status `< 2` Incomplete, `2` Pending, `3` Confirmed, `4` or `> 6` Approved, `5` Declined, `6` Cancelled.

| ID | Rule | Label |
|---|---|---|
| BR-036 | A permit created by `PermitsController::addpermitAction` is stored with status 2. | **Implemented**. Agrees with seed label "New". The commented stats label that same code "Pending" |
| BR-037 | Seed says 5 is Cancelled and 6 is Declined. The commented counter says the opposite. The live grid treats 5 as deletable and does not mention Declined. | **Conflict. Requires confirmation** |
| BR-038 | Seed says 7 is Checked-In and 9 is Checked-Out. The live grid prints "Checked-Out" when status is 7, and prints a Checked-In action when status is 4 (seed: Approved). | **Conflict. Requires confirmation** |
| BR-039 | Service type 1 is "Work Permit" and 2 is "Delivery Permit" in both `PermitType` and `Accreditation_Load_Services_Types`. | **Implemented** seed. The commented stats block uses the same split and is not executed |
| BR-040 | Occupants and the driver are stored as one JSON array on `Accupants`. Driver entries are marked `Driver = 1`. Type `1` is the "person already on the system" branch; the profile lookup in that branch is commented out. | **Implemented** |
| BR-041 | `PermitRefSerial` is a random digit string. `PermitNr` is then set to `PM` plus zero-padding plus the new id, after a call to `getRef` that depends on missing class `Model_AccreditationList`. | **Implemented** |
| BR-042 | The permit profile sets `LOCKED = 1` when the request date at 13:00, minus one day, is already past. | **Implemented**. **Inferred** product meaning: the request locks at 13:00 on the previous calendar day. The view's use of `LOCKED` was not fully confirmed |
| BR-043 | Applicant edits stamp `ApplicantModifiedBy` and `ApplicantModifiedDate`. Columns also exist for admin modify, confirm, approve, decline, and cancel. Those approval columns are not written by `addpermitAction` or `savepermitAction`. | **Implemented** schema; approval writes **not found** in those two actions |
| BR-044 | Permit email is sent only when the loaded permit's `Surname` equals `2`. The body tells the recipient the request will be reviewed and quotes permit number, times, and load-in / load-out dates. | **Implemented**. Why surname must equal 2 is **Unknown / Requires Confirmation** (the column is a name in `users`, and this comparison uses the permit row) |
| BR-045 | Commented zone rule: estimated weight is vehicle weight plus trailer weight; capacity is the sum of parking bay limits times `MaxWeight`; over capacity sets a zone status flag of 1. | **Not executed** (inside a comment). Not an active rule |
| BR-046 | Vehicle class seed includes occupant capacities (for example motorcycles 1, light passenger vehicles 5, tour bus 60) and a walk-in type described as "No vechile - walk-in only". | **Implemented** seed. A controller that blocks a permit for exceeding `Accupants` was not found |

## Email and contact

| ID | Rule | Label |
|---|---|---|
| BR-047 | Outbound mail uses SMTP host `mail.sarugby.co.za`, port 25, from `accreditation@sarugby.co.za`, display name "Registration at sarugby.co.za", subject "SARugby Notification". | **Implemented** |
| BR-048 | Registration validation links always use host `supplier.sarugby.online`. Password-reset links use the host of the current HTTP request. | **Implemented** |
| BR-049 | General enquiry address in the templates is `accreditation@sarugby.co.za`. The permit FAQ names `HealthandSafety7s@sarugby.co.za` and `security@sarugby.co.za`. The registration footer names contact centre `+27219287000`. | **Documented** in templates |
| BR-050 | Footprint email POST is not part of the live submit path. | **Implemented** method, call commented out |

## Data the application expects that this snapshot does not contain

These are rules the code assumes, not rules that can run here:

| ID | Assumption in code | Label |
|---|---|---|
| BR-051 | Company writes go through `Model_CompanyProfile`. The committed company model is `Model_Companies` on table `Companies`. | **Requires confirmation** whether they were the same class |
| BR-052 | A company-to-event link is `Company_Events` via `Model_CompanyEvents`. The dump has no `Company_Events` table. | **Requires confirmation** |
| BR-053 | `UserProfile::Active` reads `CompaniesLogin`. `searchAll` reads `Jobtitles`. `Model_Events::getStats` reads view `AccriditationNewConfirmedApprovedStatus_view` (spelled that way). None of these objects are in the dump. | **Requires confirmation** |

## Explicitly not a rule

The INI locale `af_ZA` does not switch the screens that were read to Afrikaans. The layout is `lang="en"`. Treating the product as bilingual would be an invention.
