# Business rules — Kyalami9Hour2019

Evidence date: 2026-10-10. Source tip: `master` at `5b60e52`.

No business-rules document existed in the repository. Rules below are taken from controllers, views, and models. Each rule is marked **Implemented** or **Inferred**. Nothing here is a signed business policy unless the view itself prints the sentence.

Passwords are not reproduced.

## Event identity

1. **Implemented.** The home page and the sign-in page load accreditation code `5F9298188128B` and no other code (`IndexController::indexAction`, `ApplicationController::loginAction`).
2. **Implemented.** `/application/apply/code/{code}` loads `Accreditations_view` for the code in the URL (`Model_AccreditationList::getProfile`).
3. **Implemented.** If that lookup has no `AC`, the response is the text "Sorry this page does not exits" and the action exits.
4. **Inferred.** Commit `afcd3f3` ("Update to event ID 245") is the change that introduced code `5F9298188128B` and the "2020" title. The number 245 is not stored in the diff. **Unknown / Requires Confirmation** whether 245 is the accreditation primary key.
5. **Implemented.** The browser title and footer say "Kyalami 9 Hour 2020". The Finish step states that the pass holder accepts that Kyalami 9 Hour 2020 decides the level of access, and that parties comply with the event rules and the Safety of Sports and Recreational Events Act of 2010. That paragraph is hard-coded in `index/index.phtml`. The checkbox modal body is the database field `TermsAndConditions`.

## Who may apply

6. **Implemented.** The form is anonymous. No login is required to insert an application.
7. **Implemented.** Categories offered on the form are rows from `AccreditationCategory_view` for this accreditation where `Teams = 0` and `hidden = 0`, ordered by category name (`getAccreditationCategoryPublic`).
8. **Implemented.** Roles offered are rows from `AccreditationRoles_view` for this accreditation (`getAccreditationRoleLive`), then filtered in the browser to the selected category (`role.ACID` equals the category value).
9. **Implemented.** Required personal fields in the browser: first name, surname, email (format checked by a regular expression), mobile, ID or passport number, gender, suburb. Gender options are the strings `Male` and `Female`.
10. **Implemented.** Required organisation fields in the browser: organisation, category, role, and "Reason for application" (`data[Reason_for_pplication]` — the key is spelled that way in the form).
11. **Implemented.** The server insert does not repeat these checks. Whatever is posted is inserted.
12. **Implemented.** A duplicate email for the same `AC` is detected by `checkEmails` and labelled "Email already in use,please use different email address." On `master` the off-screen confirm field is cleared, which blocks the wizard because that field is `required` and is not `:hidden`. On `qa` and `prod` (`d80a39c`) the confirm field is set to the duplicate address, so the required check can pass while the label is still shown. The database insert does not reject a duplicate.

## Media and supplier-registration note

13. **Implemented.** If the accreditation `MediaOnly` flag equals 1, the media-only inputs are rendered with class `required`. Those inputs are invite source (`data[Invitefrom]` values `1` "open field" and `2`), producer full name, and producer email.
14. **Implemented.** If `MediaOnly` is not greater than 0, choosing category value `1` shows the media block and adds class `required` to `.MediaOnly`. Any other category hides that block and removes the class. **Unknown / Requires Confirmation:** that category `1` is the media category in `teamwizz_SARU`. The comparison is hard-coded.
15. **Implemented.** If the selected role has `SupplierReg = 1`, the page shows a note that an owner, director, or manager who needs the load-in/out schedule must register the company at `http://supplier.sarugby.online`. The note does not block submit.

## Headshot

16. **Implemented.** A chosen file whose size is greater than 2,097,152 bytes is rejected in the browser with the alert "File size must not be larger than 1.5mb".
17. **Implemented.** The photo textarea starts as `required`. If the selected role has `photo = 1`, the class is removed. Otherwise the class is added. The flag is stored in a variable that is never returned to 0, so one photo-flagged role makes later roles skip the required class as well.
18. **Requires Confirmation.** The column is named `photo`. The class toggle treats `photo = 1` as "not required". Which of those was intended is not written down.
19. **Implemented.** On submit, an empty `User_pic` is replaced with a hard-coded JPEG data URI before insert (`applyAction`).
20. **Implemented.** After insert, and when the profile photo is changed, the image is POSTed to Footprint. The profile displays the Footprint file `{Ref}.jpg` when that URL returns HTTP 200.

## Closing date

21. **Implemented.** The form is replaced by the closed page when `strtotime(CloseDate + " 24:00:00")` minus the current timestamp is less than 1. Time zone is `Africa/Johannesburg`.
22. **Implemented.** The closed page tells the visitor to email `ContactName` at `ContactEmail`.
23. **Implemented.** The same check does not `return`. A POST that still reaches `applyAction` after the renderer swap continues into the insert block.
24. **Implemented.** `OpenDate` is not compared to the current time in the controllers that render the form.
25. **Implemented.** Sign-in and profile do not use the close check.

## What is stored

26. **Implemented.** A new row in `AccreditationApplycation` receives `AC`, `TID`, and `FID` from the accreditation profile, `DateApply` as the current timestamp, `Ref` as a new 6-character uppercase hex reference from 3 random bytes, and `History` = `None`.
27. **Implemented.** If `DaysAttending` is not posted it is stored as an empty string. The home wizard does not render a days control.
28. **Implemented.** The terms checkbox posts `acceptTerms` = 1. No PHP reads that field.
29. **Implemented.** Each ticked linked event is inserted as another row with the same reference and photo, and with `AC` set to the checkbox value (`OID`). **Unknown / Requires Confirmation** that `OID` is an accreditation id.
30. **Implemented.** Linked rows are those in `LinkedAccreditations` for this accreditation with `DateTo` greater than or equal to today. `applymultipleAction` loads links with no date filter. The home form does not post to that action.

## Status and editing

31. **Implemented.** Profile labels, in order of precedence:

    | Condition | Label | Fields |
    |---|---|---|
    | `Pending` = 1 | Pending | Editable. Message panel shows `RejectMessage` under the heading "Requirement" |
    | `Status` = 0 and `DateApply` < `LastUpdate` | In Review | Editable. The line that would lock this state is commented out |
    | else `Status` = 1 | Confirmed | Disabled in the browser |
    | else `Status` = 2 | Declined | Disabled. Message panel shows `DeclinedMessage` |
    | else `Status` = 3 | Approved | Disabled |
    | else | New Application | Editable |

32. **Implemented.** Disabled is a browser attribute. `updateAction` updates `AccreditationApplycation` for the posted `AAID` and sets `LastUpdate` to `date("Y-m-d H:s:i")` (minutes and seconds are swapped relative to `H:i:s`). It does not check the session or the status.
33. **Implemented.** This repository does not set Status to confirmed or approved. The only status write found is attendance cancel, which sets `Status` = 2, `History` = "Declined by Applicant", and `DeclinedDate`.
34. **Inferred.** Pending, in review, confirmed, and approved are written by another system that shares `AccreditationApplycation`. **Unknown / Requires Confirmation** which system that is. Footprint is the system this app already calls for photos.

## Attendance

35. **Implemented.** `att=1` on a matching `AttendanceKey` sends `EmailAttandanceConfirmed`, clears the key, sets `AttendanceRequest` = 1, and does not change `Status`. The page says "Thank you for confirming your attendance".
36. **Implemented.** `att=2` declines the application (rule 33) and sets `AttendanceRequest` = 2. If `EmailRejected` is not empty, the mail includes `[REASONNOTE]` "Applicant confirmed NON-ATTENDANCE / CANCELLATION for this event."
37. **Implemented.** A key that does not match assigns an empty data array. No product error sentence was found in the action.
38. **Implemented.** The profile shows both attendance links only when `AttendanceKey` is not empty.

## Email content

39. **Implemented.** Applicant mail is sent only when `EmailNotes` is not empty. Decline mail from the attendance path is sent only when `EmailRejected` is not empty. Attendance-confirm mail uses `EmailAttandanceConfirmed` with no empty check before `sendMail`.
40. **Implemented.** Tokens replaced in those bodies: `[ACCREDITATIONNAME]`, `[CONTACTNAME]`, `[CONTACTTELEPHONE]`, `[CONTACTEMAIL]`, `[REFNUMBER]`, `[APPLICANTNAME]`. Decline mail also replaces `[REASONNOTE]`.
41. **Implemented.** The subject sent is "Kyalami 9 Hour 2020 Notification". Other subject strings are assigned to unused variables.
42. **Implemented.** On `master` the From address is `accreditation@kyalami9hour.com`, display name "Accreditation - Kyalami 9 Hour 2020". On `qa` and `prod` the address is `accreditation@magus.events` with the same display name.

## Access and session

43. **Implemented.** Applicant authentication is email plus reference for one `AC`. There is no applicant password.
44. **Implemented.** A match stores Zend namespace `user_session` with `AAID` and `Code` (`aCode`). Cookie lifetime in config is 864000 seconds.
45. **Implemented.** `applyAction` and `loginAction` redirect to the profile when `AAID > 0`. `profileAction` redirects to apply when `AAID` is not greater than 0.
46. **Implemented.** Logout unsets `user_session` and redirects to `/application/login/code/{code}` using the code that was in the session.
47. **Implemented.** `loginnowAction`, `updateAction`, and `updateimageAction` do not check a session before doing their work. Login still requires a matching email, reference, and `AC`. Update requires only a posted `AAID`.
48. **Implemented.** Queries for login, email check, and profile concatenate request values into SQL.

## Browser gate

49. **Implemented.** User agents matching Firefox, Safari, Chrome, Flock, or Opera receive the portal. `MSIE` and an empty agent receive the unsupported-browser page. Chrome is classified as Safari because `Safari` is tested first. Both are allowed.

## Supplier module rules (not the public journey)

50. **Implemented.** `Supplier_LoginController` hashes the posted password with MD5 and looks up `Companies_Profile`. If the posted password equals one hard-coded value (not reproduced), it substitutes the stored password hash and continues.
51. **Implemented.** A successful password reset sets `AllowLogin` = 1 and `Email_Confirmed` = 1 and emails the new password in clear text, with the sign-off "SA Rugby Event Administration" and contact `accreditation@sarugby.co.za`.
52. **Inferred.** These rules belong to the copied supplier portal, not to the Kyalami applicant wizard. The Kyalami header does not link to this module.

## Deploy rules that affect behaviour

53. **Implemented.** Only pushes to `qa` and `prod` run the pipeline. `master` does not.
54. **Implemented.** `qa` config host is `qadb.footprintapp.net`. `prod` and the production section of the tracked ini use `db.footprintapp.net`. Database name is `teamwizz_SARU`.
55. **Implemented.** Production config sets `display_errors` and `display_startup_errors` to 1, and `resources.frontController.params.displayExceptions` to 1. The `development` and `testing` sections turn those off. `public/index.php` defaults `APPLICATION_ENV` to `production` when the environment variable is absent, and it also sets `display_errors` to 1 itself.
