# PRD — ieb-inde-5 (IEB CPS)

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

Provenance labels: **Documented** (readme or comments), **Implemented** (code path), **Inferred** (reasonable reading of that code), **Unknown**.

The website root used below is `INDE-5/Evolution Software Unified Framework/Evolution Software Unified Framework/core_custom/INDE-5/`.

## Product purpose

CPS is the INDE-5 website in the Evolution Software family. `INDE-5/ReadME.txt` states **CPS = INDE-5**. The implemented behaviour lets operators keep collaborative-problem-solving teams of up to five people, record question solutions and a collaboration log, keep schools and clusters, and mirror team membership into Rocket.Chat groups.

The same website still contains the framework's admin, catalogue, loyalty, and medical surfaces, and it contains exam-moderation classes. Those are present in the tree. This PRD treats them as framework or shared code unless a CPS class calls them. Whether the exam surfaces are switched on for INDE-5 in the database is **Unknown / Requires Confirmation**.

## Users and roles

`business/enums/pNums.cs` defines:

| Value | Name |
|---|---|
| 1 | WebsiteUser |
| 2 | PowerUser |
| 3 | SuperUser |
| 4 | AdminUser |
| 5 | CustomUser |

**Implemented** behaviour that uses these roles:

- A user whose `userType` is below PowerUser (so WebsiteUser only; CustomUser is 5) sees a filtered CPS team grid and a filtered schools grid.
- The same threshold decides whether cluster member and meeting "new" buttons are hidden unless the user is a cluster resource.
- Excel import of school contacts and of cluster members creates a `WebsiteUser` on the current setup `customerCode`, with `isReset` true for school-contact imports.
- Rocket.Chat login treats WebsiteUser and users at or above PowerUser differently when choosing a chat username. See FR-012.

School contacts and cluster resources store a framework user id, not a separate login type. How canvases combine `userTypes`, `isRestricted`, and `customUsers` at page-render time was not fully traced. **Unknown / Requires Confirmation.**

## Major workflows

### 1. Save a collaborative problem solving team

Trigger: save of the Collaborative Problem Solving Teams surface. `CollaborativeProblemSolvingTeams_form.ascx.cs` `SaveSurfaceFormCustom`.

The published row stores the academic year (1 January of the year typed in the year box), topic, team code, guiding notes, team status lookup id, five member slots (name, email, school, avatar nickname), question solution overviews and document fields for questions 1 to 5, a solution title and overview, evolution documents, and a collaboration log.

On a new row (`recId == 0`), after the team code is copied from the form, the code calls `RocketChatHelper.RCCreateGroup` with the trimmed team code. The moderator username from configuration is added to that group.

When an existing team code is non-empty and a member email changes, `UpdateGroupMembers` runs for that slot before the new name and email are stored. Member 1's email is stored trimmed. Members 2 to 5 are stored without `Trim()`. **Implemented.**

After the first save (`SurfaceAppItemId > 0`), academic year, topic, team code, and guiding notes are read-only. Save-and-new is hidden. Comment: Graham Rook, 2018-09-14. **Implemented.**

`PerformCustomSaveAddOn` on this surface is empty.

### 2. List teams and schools

Trigger: grid bind.

CPS teams: if `userType` is below PowerUser, the filter keeps rows where the signed-in email equals any of the five member email columns. The email is concatenated into SQL. **Implemented.**

Schools: the same user type sees schools whose `itemID` is the parent of a `publ_SchoolContacts` row where `SchoolContacts_SchoolContacts_Name` equals the user id and `SchoolContacts_SchoolContacts_IEBLiaison` is `'1'`. **Implemented.**

PowerUser and above do not get these two filters.

### 3. Save a cluster and generate its code

Trigger: save of Cluster Information. `ClusterInformation_form.ascx.cs`.

If the cluster code box is empty, `PerformCustomSaveAddOn` builds `{level abbreviation}-{subject code}-{province custom code}-` and appends a two-digit sequence. The sequence is `COUNT(*)` of `publ_ClusterInformation` codes with that prefix. If the count is 0 the suffix is `01`; otherwise the count is incremented and padded. The code is written only when level, subject, and province each returned a row (`validCount == 3`).

The cluster code box is disabled. Once it is non-empty, province, level, subject name, cluster name, and area are also disabled.

If the cluster has no `publ_ClusterResources` rows, the save copies every resource whose `ParentSurfaceItemId` is `0` onto new surface items for surface id `3048`, then rebinds that child grid.

A website user sees the new-member button (`btnNewChild_3046_...`) only when a resource on this cluster has `ResourceName` equal to their user id. They see the new-meeting button (`btnNewChild_3076_...`) only when that resource type is `Cluster Administrator` or `Cluster Leader`.

### 4. Save a cluster meeting

Trigger: save of Cluster Meetings.

Before insert or update, the code copies reporting labels from the parent cluster (`ParentSurfaceItemId`): area, cluster code, cluster name, level name, province display text, and subject name. Comment: Graham Rook, 2018-05-17, "Handle savign to labels for reporting". **Implemented.**

### 5. Pick a cluster member from a school

Trigger: populate of Cluster Members.

`BindUserPicklist` loads active users for the current customer whose ids appear as `SchoolContacts_SchoolContacts_Name` on the selected school, and whose contact active year is the current calendar year. Choosing a user copies email, SACE number, and active year from that school contact. Email, SACE number, and active year are then disabled. **Implemented.**

Cluster resource names are a picklist of all active users for the customer, plus an "Add New" item (`-1`) that opens `#modContactAdd`. Choosing a user copies that user's email.

### 6. Import users from Excel

Two import buttons create framework users before the generic surface import:

| Form | Excel columns read | User created |
|---|---|---|
| Schools, any published child import | Email, Mobile, Title, RSA ID Number, Name | WebsiteUser, random 8-character password, `isReset` true, current customer code |
| Cluster Information, any published child import | Member Email, Member Name | WebsiteUser, random 8-character password, current customer code. `isReset` is not set in this block |

If a user with the same email and customer code already exists, the row is left unchanged. Name parsing: a comma splits surname and given name; a space splits given name then surname on the Schools import, and the Cluster Information import takes the surname with `IndexOf(" ") - 1` (one character short). **Implemented.**

The custom block does not check which child surface is being imported. A published child workbook that lacks those column names will throw. **Inferred** that the intended children are school contacts and cluster members. **Unknown / Requires Confirmation** which child imports are actually offered in the database.

### 7. Import a CPS team sheet into Rocket.Chat

Trigger: admin surface upload when the surface name equals `CollaborativeProblemSolvingTeams` and the published import succeeds. `controls/admin/surface.ascx.cs` `lnkUpload_Click`.

`On_TeamMembersImport` reads a team-code column and up to five member-email columns. For each email it loads the active user for the current customer, creates a Rocket.Chat user named `{teamCode}_{user.name}`, and creates a group named with the team code containing those usernames plus the configured moderator username.

### 8. Register or update the signed-in profile

Trigger: `Core.master.cs` `btnRegister_Click`.

On update, if the telephone is non-empty, the code writes that telephone into `CPSTeamMembers_..._AvatarNickname` for every team slot whose email equals the user's email. A password change that passes the old-password check then calls `RocketChatHelper.UpdateUser`, and when `RocketChatSelfAuth` is `true` it logs the chat session out and back in.

`Reset.aspx.cs` does the same Rocket.Chat password update after a reset.

Navigation logout calls `RCLogoutUser` when a `RocketChatData` session value exists.

### 9. Examination moderation mail

`examHandler` emails examiners, reviewers, and moderators from the template named `Examination Moderation Status` when a subject status changes, and it emails a collaboration-log note. Callers found in this website: `controls/module/surfaceNotes.ascx.cs` and `controls/admin/attachments.ascx.cs`. Status ids are hardcoded in `examNums.SubjectStatus` (for example `NoActivity = 1093`, `ExaminerUploadedReviewReady = 1083`). There is no `Examination*` control under `surface_custom`. Whether those surfaces exist for INDE-5 is a database question. **Unknown / Requires Confirmation.**

## Integrations

| System | Direction | Trigger | Evidence |
|---|---|---|---|
| SQL Server `Framework_INDE-5_Demo` | App reads and writes | Every surface and user action | Active `conn` app setting. Host and credentials omitted. Commented alternative is local catalog `Framework_INDE-5` with integrated security. |
| Rocket.Chat `https://chat.ieb.co.za` | Outbound REST | Team save, team import, login, password change, logout | `RocketChatServerURL` and `RocketChatHelper`. Tokens and passwords omitted. |
| SMTP `smtp.evolutionsoftware.co.za` port 25 | Outbound mail | Framework mailer; exam status mail | `Web.config` `mailSettings`. Password omitted. |
| WCF `http://localhost:58913/framework_service.svc` | Client endpoint `Iframework_service` | Present in config | Security mode `None`. |
| WCF `http://service.ieb.co.za/framework_service.svc` | Client endpoint `Iframework_serviceLive` | Present in config | Same binding. |
| `http://sms.netgen.co.za/SMS_V2.asmx` | Outbound SMS | Framework SMS client | Endpoint present. SMS user and password are in app settings. Values omitted. |
| MediSwitch QA gateway hosts | Configured | `MediSwitchResponses` = `0` | Live gateway URLs are commented out. |
| Country web services at webservicex.net | Configured | `getCountriesFromWebService` = `0` | Not called while the switch is 0. **Inferred** from the flag. |

Rocket.Chat failure behaviour: `UpdateGroupMembers` throws if a kick or an invite does not return HTTP 200. The team save catches that and redirects to `/error`. Login and import failures are written through `exception.HandleException` and do not throw out of `On_TeamMembersImport`. **Implemented.**

## Functional requirements

### FR-001 — Identify the site as INDE-5 CPS

Requirement: The project is the CPS website in the INDE family.

Evidence: `INDE-5/ReadME.txt`.

Implementation: Implemented as the website project `core_custom/INDE-5`.

Confidence: High for the code name. The expansion of CPS is inferred from the surface names.

### FR-002 — Store a team of up to five members

Requirement: A team has a summary, five member slots, and solution content for five questions.

Evidence: `oCollaborativeProblemSolvingTeams` and `SaveSurfaceFormCustom`.

Who: A user who can open the surface. Grid visibility for WebsiteUser is FR-004.

Trigger: Surface save.

Inputs: Year, topic, team code, guiding notes, status lookup, member name/email/school/nickname, solution text and document fields.

Expected behaviour: Persist a `publ_` row. Year is stored as 1 January of the entered year. New rows are active.

Validation: Status is `int.Parse` of the selected value. An empty year is not written. No other field validation was found in the custom block.

Error: Exceptions redirect to `/error`.

Implementation: Implemented.

Confidence: High.

### FR-003 — Lock team identity fields after create

Requirement: After a team exists, academic year, topic, team code, and guiding notes cannot be edited, and save-and-new is hidden.

Evidence: `PerformCustomPopulateAddOn` when `SurfaceAppItemId > 0`.

Implementation: Implemented.

Confidence: High.

### FR-004 — Website users see only their teams

Requirement: A user below PowerUser sees teams where their email is one of the five member emails.

Evidence: `CollaborativeProblemSolvingTeams_grid.ascx.cs` `BindSurfaceGrid`.

Implementation: Implemented. The comparison is exact and case-sensitive as SQL Server collation decides. **Unknown / Requires Confirmation** which collation the database uses.

Confidence: High for the filter. Medium for who is allowed to open the page; that is surface security stored in the database.

### FR-005 — Website users see schools where they are the IEB liaison

Requirement: A user below PowerUser sees schools linked to a school contact whose name id is their user id and whose IEB liaison flag is 1.

Evidence: `Schools_grid.ascx.cs`.

Implementation: Implemented.

Confidence: High.

### FR-006 — Generate a cluster code

Requirement: An empty cluster code becomes `{level abbreviation}-{subject code}-{province custom code}-{nn}`.

Evidence: `ClusterInformation_form.ascx.cs` `PerformCustomSaveAddOn`.

Expected behaviour: Sequence starts at 01. Later saves count existing codes with that prefix and add one. If any of the three lookups is missing, the code stays empty.

Error: Redirect to `/error`.

Implementation: Implemented. Two concurrent first saves can compute the same suffix. That race is not handled. Provenance: **Inferred** from the count-then-write.

Confidence: High.

### FR-007 — Freeze cluster identity after a code exists

Requirement: Cluster code is not editable. Province, level, subject, name, and area lock once a code is present.

Evidence: `PerformCustomPopulateAddOn` on Cluster Information.

Implementation: Implemented.

Confidence: High.

### FR-008 — Copy template resources onto a new cluster

Requirement: The first save of a cluster that has no resources copies resources whose parent item id is 0 onto surface 3048.

Evidence: custom include dated 2018-05-16 in `ClusterInformation_form.ascx.cs`.

Implementation: Implemented. Surface id 3048 is hardcoded.

Confidence: High for the copy. **Unknown** what the template rows contain in the database.

### FR-009 — Restrict who can add cluster members and meetings

Requirement: A website user can add members only if they are a resource on the cluster, and can add meetings only if that resource type is Cluster Administrator or Cluster Leader.

Evidence: `PerformCustomPopulateAddOn` on Cluster Information. Buttons for surfaces 3046 and 3076.

Implementation: Implemented for those two buttons. Other edit paths were not traced.

Confidence: Medium.

### FR-010 — Denormalise cluster meeting labels

Requirement: Saving a meeting copies the parent cluster's area, code, name, level, province display, and subject name onto the meeting.

Evidence: `ClusterMeetings_form.ascx.cs`.

Implementation: Implemented. Later edits to the parent cluster do not show a matching update of existing meetings.

Confidence: High for the save-time copy.

### FR-011 — Denormalise school name onto a contact

Requirement: Saving a school contact copies the parent school's name and school-type display.

Evidence: `SchoolContacts_form.ascx.cs`.

Implementation: Implemented.

Confidence: High.

### FR-012 — Mirror teams into Rocket.Chat groups

Requirement: A new team creates a chat group named with the team code. Changing a member email removes the previous chat user from the group and invites the new one. Importing the team surface creates chat users and the group.

Evidence: `RocketChatHelper`, the team form save, `surface.ascx.cs`.

Inputs: Team code; member name and email; active framework user for that email and customer code.

Expected behaviour:

- `RCCreateGroup` always adds `RocketChatModerator_username`.
- Import usernames are `{teamCode}_{framework user name}`.
- `UpdateGroupMembers` uses the given-name portion after a comma, and does not prefix the team code. A comment in that method says the team-code prefix is the old method.
- `GetUsername` for a WebsiteUser still returns `{teamCode}_{user.name}` from the first matching team.

Implementation: Implemented, and the three username rules disagree. **Requires Confirmation** which username Rocket.Chat actually has for a team member.

Error: A failed kick or invite during a team save redirects to `/error`. Import failures are logged and the loop continues.

Confidence: High for the calls. Low for a single username rule.

### FR-013 — Sign the current user into Rocket.Chat

Requirement: When self-auth is true, a successful password change logs the user into Rocket.Chat and stores user id and auth token in session `RocketChatData`. Logout clears that session.

Evidence: `Core.master.cs`, `Reset.aspx.cs`, `navigation.ascx.cs`, `RocketChatLogin`.

The chat password is the decrypted framework password. Website and power users both call `RCLoginUser` with email and password. A comment above the power-user branch says "Login to special Moderator account"; the code still logs in with the current user's email. **Implemented.** **Requires Confirmation** whether power users should use the moderator account.

`RocketChatLogin` also logs the raw response body through `exception.HandleException` before parsing it.

Confidence: High.

### FR-014 — Write the profile telephone into avatar nickname

Requirement: Updating a user who has a telephone copies that telephone into every CPS team avatar-nickname column that matches the user's email.

Evidence: `Core.master.cs` `btnRegister_Click`.

Implementation: Implemented. The column is named AvatarNickname. Whether the business wants a phone number there is **Unknown / Requires Confirmation**.

Confidence: High for the write.

### FR-015 — Create website users from school and cluster imports

Requirement: Importing a published child on Schools or on Cluster Information creates a WebsiteUser when the email is new for the customer.

Evidence: `btnImportDataChild_Click` on both forms.

Validation: Existence check is email plus customer code. Existing users are not updated. Schools import sets `isReset`. Cluster import does not set `isReset` in the custom block.

Error: Missing Excel columns throw, and the handler redirects to `/error`.

Implementation: Implemented.

Confidence: High.

### FR-016 — Cluster member list follows this year's school contacts

Requirement: The member-name picklist is limited to contacts of the selected school whose active year is the current calendar year.

Evidence: `ClusterMembers_form.ascx.cs` `BindUserPicklist`.

Implementation: Implemented. Active year on the contact is stored as 1 January of the typed year, and the filter uses `YEAR(...)`.

Confidence: High.

### FR-017 — Keep subject, affiliation, and school reference data

Requirement: The site has published shapes for subjects (name, code, cluster level), school affiliations (name, abbreviation), and schools (name, registration name, centre number, type, addresses, phone, primary email, affiliations, NSC year, PSI year).

Evidence: `oSubjectManagement`, `oSchoolAffiliations`, `oSchools`.

Implementation: Implemented as object classes and generated surfaces. Extra validation beyond the generated save was not found in their custom save add-ons (Subject Management's custom add-on was not a CPS-specific rule).

Confidence: High for the fields. Business meaning of NSC year and PSI year is **Unknown / Requires Confirmation**.

### FR-018 — Examination status mail when those surfaces are used

Requirement: Status changes and collaboration-log notes on an examination authoring item email the role named by `examNums`.

Evidence: `examHandler`, `surfaceNotes.ascx.cs`, `attachments.ascx.cs`.

Implementation: Implemented in code. Not confirmed as an INDE-5 canvas. See workflow 9.

Confidence: Medium.

## Constraints

- Published CPS data is SQL Server rows, not files in git. Surface ids 3046, 3048, and 3076 are hardcoded.
- `Web.config` `compilation debug="true"`.
- `httpRuntime maxRequestLength="10048"` (about 10 MB).
- Two `website` app settings exist: `IEB (INDE-5 Local)` and later `IEB CRM`. Two `defaultCulture` keys both set `en-ZA`. Globalization on the website is `culture="en-US"`. Which title the mailer uses is **Unknown / Requires Confirmation**.
- `WebAddy` is `http://localhost:3083`.
- Support, from, admin, cc, and bcc addresses in app settings point at `graham@evolutionsoftware.co.za` or `no-reply@evolutionsoftware.co.za`.
- No captcha control was found under the INDE-5 website.
- Secrets are in source. They are not restated in this PRD.

## Non-goals of this document

This PRD does not promote framework admin pages (banners, products, loyalty, campaigns, medical) to CPS requirements. They are compiled into the same website. Loyalty email is switched off (`loyaltyEmail` = `0`). MediSwitch responses are switched off.
