# Business rules — ieb-inde-5

Evidence date: 2026-10-12. Inspected revision: `main` at `c854df0`.

Rules below are taken from code. Each rule is marked **Implemented** or **Inferred**. Nothing in the repository states these as an approved business policy. Where the code is the only source, a human still needs to confirm that the behaviour is intended.

Lookup displays (team status, school type, meeting status, province names) live in SQL Server `oSurfaceLookup` rows. The ids are not in git, except the exam status ids in `examNums.cs`.

## Identity

### BR-001 — This site is CPS / INDE-5

**Documented** in `INDE-5/ReadME.txt`: `CPS = INDE-5`.

The readme also lists sibling codes. Those siblings are not implemented by this repository's custom surfaces, except that exam-moderation classes from the exams family are compiled into this website.

### BR-002 — Roles are the framework user types

**Implemented.** `pNums.UserType`: WebsiteUser 1, PowerUser 2, SuperUser 3, AdminUser 4, CustomUser 5.

CPS filters use `userType < PowerUser`. That includes WebsiteUser and excludes CustomUser, PowerUser, SuperUser, and AdminUser.

## Teams

### BR-003 — A team has five member slots

**Implemented.** The published object has member 1 through member 5, each with name, email, school, and avatar nickname. Empty strings are stored when the boxes are empty. There is no check that the five emails are distinct, or that a slot is complete.

### BR-004 — Academic year is the calendar year, stored as 1 January

**Implemented.** If the year box is non-empty, the save uses `new DateTime(year, 1, 1)`. The view prints `.Year` only when the year is greater than 1901.

### BR-005 — Team status is a lookup id

**Implemented.** The save parses the selected dropdown value as an integer. The view resolves that id through `oSurfaceLookup.display`. The allowed statuses are **Unknown** (database).

### BR-006 — After the team exists, identity fields are read-only

**Implemented.** When `SurfaceAppItemId > 0`, academic year, topic, team code, and guiding notes are read-only, and the save-and-new button is hidden.

Member fields, status, and solution fields stay editable. **Inferred** from what the custom block does not lock.

### BR-007 — A new team opens a Rocket.Chat group named with the team code

**Implemented.** `RCCreateGroup` runs only when `recId == 0`, using the trimmed team code, including when that code is empty. The configured moderator username is added to the member list.

### BR-008 — Changing a member email updates the chat group before the row is saved

**Implemented.** For each slot, if the team code is non-empty and the stored email differs from the trimmed form email, `UpdateGroupMembers` runs. An empty new email removes the old user and does not invite a replacement.

The old username is the portion of the stored name after the first comma, trimmed. The new username is the same portion of the new name. If the name has no comma, `IndexOf(",") + 1` still runs and uses the whole name. **Implemented.**

A kick or invite that is not HTTP 200 throws, and the save redirects to `/error`. The published row is written only after these calls. **Implemented.**

### BR-009 — Website users only see teams that list their email

**Implemented.** Filter is equality against the five member email columns. PowerUser and above are not filtered by this rule.

The value is concatenated into SQL text. Provenance of the injection shape: **Implemented**. It is recorded as behaviour, not as an approved rule.

### BR-010 — A profile telephone overwrites avatar nickname

**Implemented.** `btnRegister_Click`, when the user already exists and `tel` is non-empty, sets `AvatarNickname` to that telephone on every team slot whose email equals the user's email.

**Inferred:** the field is being used as a contact number, despite the name. **Requires Confirmation.**

## Schools and contacts

### BR-011 — Website users only see schools where they are the IEB liaison

**Implemented.** The schools grid, for `userType` below PowerUser, keeps schools that have a `publ_SchoolContacts` child where `SchoolContacts_SchoolContacts_Name` equals the user id and `SchoolContacts_SchoolContacts_IEBLiaison` equals `'1'`.

Principal (`SchoolContacts_SchoolContacts_Principal`) is stored and is not used in this filter.

### BR-012 — A school contact copies the parent school name and type

**Implemented.** On save, school name comes from the parent `oSchools` row. School type is the lookup display for `Schools_SchoolDetails_SchoolType`, not the lookup id.

### BR-013 — Contact active year is stored as 1 January

**Implemented.** Same year-box pattern as the team academic year.

## Rocket.Chat identity

### BR-014 — Chat usernames are built three different ways

**Implemented.** This is a conflict in the code, not a single rule:

| Path | Username |
|---|---|
| Team spreadsheet import | `{teamCode}_{framework user.name}` |
| `GetUsername` for WebsiteUser | `{teamCode}_{framework user.name}` from the first team whose member emails contain the user |
| `GetUsername` for PowerUser and above | The user's email |
| `UpdateGroupMembers` | Given name after the comma, with no team-code prefix |

`RocketChatLogin` comments that a power user should use a moderator account. The method logs in with the current user's email and password in both branches.

**Requires Confirmation** before anyone treats one of these as the business rule.

### BR-015 — Chat users are created only for an active user of this customer

**Implemented.** Import and `UpdateGroupMembers` load `oUser` with email, `isActive = 1`, and `customerCode` equal to `handler.ReturnSetup().code`. If no user matches, no chat user is created. `UpdateGroupMembers` still attempts the invite.

### BR-016 — Self-auth refreshes the chat session after a password change

**Implemented.** When `RocketChatSelfAuth` is `true` and the old password matched, the master page and the reset page push the new password to Rocket.Chat, log out, and log in again. The decrypted framework password is the chat password.

Logout from navigation calls `RCLogoutUser` when `RocketChatData` is in session.

## Clusters

### BR-017 — Cluster code format

**Implemented.** When the code box is empty and level, subject, and province all resolve:

`{ClusterLevels_ClusterLevels_CodeAbbreviation}-{Subjects_SubjectDetails_SubjectCode}-{province lookup customCode}-{nn}`

`nn` is `01` when no existing code has that prefix. Otherwise it is one more than the count of matching codes, padded to two digits (`D2`). A count of 99 becomes `100`, which is three digits. **Implemented** by the format string.

If any of the three parts is missing, the code is left empty.

### BR-018 — Cluster code and, once set, the identity fields do not stay editable

**Implemented.** The code textbox is always disabled in `PerformCustomPopulateAddOn`. When it is non-empty, province, level, subject name, cluster name, and area are disabled too.

### BR-019 — A cluster inherits a template resource list

**Implemented.** If no `publ_ClusterResources` row has this cluster's `itemID` as parent, every resource with `ParentSurfaceItemId = 0` is copied onto new items of surface `3048`.

What those template rows are is **Unknown** (database).

### BR-020 — Website users add cluster children only when they are a resource

**Implemented.**

- New member button (surface `3046`) is visible when some resource on the cluster has `ResourceName` equal to the user's id.
- New meeting button (surface `3076`) is visible when that same resource's type is exactly `Cluster Administrator` or `Cluster Leader`.

The loop stops at the first resource whose name matches the user. **Implemented.** If two resource rows exist, only the first match is considered.

PowerUser and above do not pass through this visibility block.

### BR-021 — Cluster member names are this year's contacts of the selected school

**Implemented.** The picklist query requires `ParentSurfaceItemId` of the school contact to equal the selected school, and `YEAR(SchoolContacts_SchoolContacts_ActiveYear)` to equal `DateTime.Now.Year`. Display text is `surname + ', ' + name` from `pal_User`.

Email, SACE number, and active year on the member form are disabled. They are filled from the matching school contact when a user is chosen.

### BR-022 — A meeting stores a copy of the parent cluster's labels

**Implemented.** On every save, area, cluster code, cluster name, level name, province display, and subject name are overwritten from the parent cluster and its lookups. The copy is not described as a live link.

### BR-023 — Cluster resource "Add New" opens a contact modal

**Implemented.** Selecting value `-1` runs `$('#modContactAdd').modal()`. The cluster member form contains the same modal script, and its "Add New" list item is commented out.

## Imports

### BR-024 — A new imported person becomes a WebsiteUser

**Implemented.** Both custom imports set `userType` to WebsiteUser, `isActive` true, and `customerCode` to the current setup code. The password is a random 8-character value from `utils.RandomString(8, true)`, stored encrypted.

Schools import also sets title, mobile, RSA ID number, and `isReset` true. Cluster-member import does not set `isReset`, title, mobile, or id number.

If email plus customer code already exists, the user row is not updated. **Implemented.**

### BR-025 — Imported names are split on comma, otherwise on space

**Implemented.**

- Comma: text before the comma is the surname. Schools import then strips spaces from the remainder. Cluster import keeps the remainder as the given name without that space strip.
- Space, no comma: Schools import uses the first token as the given name and the rest as the surname. Cluster import sets the surname with `Substring(0, IndexOf(" ") - 1)` and the given name after the space.
- Neither: the whole value is the surname. Given name stays unset in that branch.

### BR-026 — CPS team import also creates chat users

**Implemented.** Only when the admin upload's surface name equals `CollaborativeProblemSolvingTeams` and the published import reported success. Other surfaces do not call `On_TeamMembersImport`.

Column detection is loose: a header that contains `teamcode` after spaces are removed and lowercased, and a header that contains `teammember1email`, which is then reused for members 2 to 5 by replacing `1`.

## Examination mail

### BR-027 — Exam status ids and who is emailed

**Implemented** in `examHandler.EmailExamStatus` and `examNums.SubjectStatus`. These run only when an examination authoring row is loaded for the current surface item.

| Status id | Name | Mail behaviour in code |
|---|---|---|
| 1093 | NoActivity | No recipient |
| 1083 | ExaminerUploadedReviewReady | Reviewer |
| 2160 | ReviewerQuery | Examiner |
| 1084 | ReviewerUploadedIMReleaseReady | No recipient |
| 1085 | IMModerationRequired | Internal moderator |
| 1086 | IMNotCompliant | Reviewer and examiner |
| 1087 | IMCompliantEMReleaseReady | Reviewer and examiner |
| 1088 | EMModerationRequired | External moderator |
| 1106 | EMNotCompliant | Reviewer and examiner |
| 1107 | EMCompliantMinorChanges | Reviewer and examiner |
| 1108 | EMCompliantTranslationReady | Reviewer and examiner |
| 1109 | PostTranslationChangeRequired | No recipient |
| 1110 | PrintReady | No recipient |

The template loaded is `Examination Moderation Status` with template type `OutboundResponse`. If that template is missing, the body stays empty. When a recipient was set, the subject is `IEB: {subject name} ({status})`, bcc is the `bcc` app setting, and from-address is `setup.communicationEmail`. A commented line would have forced the recipient to a test address; it is not active.

Subject resource roles in `examNums`: Examiner 1, Reviewer 2, InternalModerator 3, ExternalModerator 4, AssessmentSpecialist 5.

Whether INDE-5 publishes these surfaces is **Unknown / Requires Confirmation**.

## Permissions and access

### BR-028 — New-button visibility follows grid options, with a CustomUser mimic

**Implemented** on the CPS team grid. The new button is visible when `userType >= addAccessType` and the user is not CustomUser, or when the user is CustomUser and `mimicUserType >= addAccessType`.

`addAccessType` comes from grid options stored for the surface. The numeric threshold is **Unknown** (database).

### BR-029 — Website user link

**Implemented.** If the surface has `linkUser` and the user is a WebsiteUser, the surface item's `userLink` is set to the user id on save. The same flag limits the grid query. Whether the CPS surfaces set `linkUser` is **Unknown** (database).

### BR-030 — No captcha was found

**Implemented** absence: no captcha or reCAPTCHA reference under the INDE-5 website project. This is not a rule that captcha is forbidden. It is a statement that the tree does not enforce one.

## Data and configuration rules that affect behaviour

### BR-031 — Active database is the demo catalog in config

**Implemented.** The live `conn` key points at catalog `Framework_INDE-5_Demo`. A commented pair would use local `Framework_INDE-5`. Which catalog production uses is **Unknown / Requires Confirmation**.

### BR-032 — Switches that are off

**Implemented** in `Web.config`:

| Key | Value | Effect seen in config |
|---|---|---|
| `MediSwitchResponses` | `0` | QA gateway URLs are present; live URLs are commented |
| `getCountriesFromWebService` | `0` | Country service URLs are present |
| `loyaltyEmail` | `0` | Loyalty mail hour is `20` but the switch is off |
| `RocketChatSelfAuth` | `true` | Chat login on password change is on |

### BR-033 — Duplicate website title keys

**Implemented.** App settings contain `website` = `IEB (INDE-5 Local)` and a second `website` = `IEB CRM`. Two `defaultCulture` keys are both `en-ZA`. The page globalization culture is `en-US`.

How `ConfigurationManager` combines duplicate keys was not executed. **Unknown / Requires Confirmation.**

## Explicitly not inferred

- A business definition of CPS, NSC, PSI, or SACE beyond the field names.
- Which cluster resource types exist besides the two strings compared in code.
- That the exam mail rules are in production use on INDE-5.
- That secrets in `Web.config` are acceptable. They are present. Values are omitted. Human review is required.
