# PRD — ieb-inde-2 (IEB CRM)

Evidence date: 2026-10-13. Inspected revision: `main` at `8e2b139`.

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

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

## Product purpose

`INDE-2/ReadME.txt` names **CRM = INDE-2**. **Documented.**

The implemented custom website keeps schools and their contacts, a subject list, and clusters (level, subject, province, members, resources, and meetings). Cluster mail uses the subject prefix `IEB CRM`. **Implemented.**

The same website still contains the framework's admin, catalogue, loyalty, medical, and sales pages. Those files are present. This PRD does not treat them as CRM requirements unless a custom INDE-2 class calls them.

`controls/module_custom/billingInstructions.ascx.cs` models landlord, tenant, and supplier billing rows. `CriterionFeedback.ascx.cs` and `App_Code/classes/examHandler.cs` implement examination moderation. No cluster or school form was found calling them. They are leftover in this tree, not an INDE-2 CRM requirement. Whether a database canvas still opens an examination surface is **Unknown / Requires Confirmation**.

## Users and roles

`business/enums/pNums.cs` defines framework user types:

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

**Implemented.**

Cluster permissions are not a separate enum. On Cluster Information populate, a user below PowerUser is matched to a cluster resource whose `ResourceName` equals that user's `recId` as text:

| Resource type text | What that user may add |
|---|---|
| Any matching resource | Cluster members (child surface button `3046`) |
| `Cluster Administrator` or `Cluster Leader` | Cluster meetings as well (child surface button `3076`), and edit or remove meeting rows |

A user at PowerUser or above skips that block, so the generated access rules stay in force. **Implemented.**

How the framework combines `userTypes`, `isRestricted`, and `customUsers` at page-render time was not fully traced. **Unknown / Requires Confirmation.**

School-contact behaviour adds one more case: a WebsiteUser editing an existing contact cannot change the name dropdown or the email box. A WebsiteUser adding a contact sees only "Select" and "Add New" until a contact has just been added. **Implemented.** Dated 2019-04-25.

## Major workflows

### 1. Open a cluster

Trigger: populate of Cluster Information. Custom region dated 2018-05-16. `ClusterInformation_form.ascx.cs` `PerformCustomPopulateAddOn`, called from populate because a 2018-05-21 region moved that call earlier in the stack.

- Cluster code is disabled.
- If the cluster code already has text, province, level, subject name, cluster name, and area are disabled.
- Subject names are rebound from active `publ_SubjectManagement` rows whose cluster level equals the selected level (`BindSubjectNames`).
- Below PowerUser, the new-member and new-meeting buttons follow the resource rules above.

`PerformCustomSaveAddOn` builds the cluster code only when the code text is empty. **Implemented.**

### 2. Generate a cluster code

Trigger: save of Cluster Information, before the generated assignment persists the code field. Dated 2018-05-16.

The code is `{level abbreviation}-{subject code}-{province custom code}-{sequence}`.

- Level abbreviation comes from `publ_ClusterLevels` for the selected level item.
- Subject code comes from `publ_SubjectManagement` for the selected subject item.
- Province segment is `oSurfaceLookup.customCode` for the selected province.
- All three lookups must succeed. If any fails, the code is left empty.
- Sequence is `COUNT(*)` of `publ_ClusterInformation` rows whose code starts with that prefix. Count 0 becomes `01`. Any greater count is incremented and padded to two digits.

The count is a concatenated SQL string. **Implemented.**

### 3. Copy template resources onto a new cluster

Trigger: save of Cluster Information, after the cluster row is written. Dated 2018-05-16.

If the cluster has no `publ_ClusterResources` rows, every resource whose `ParentSurfaceItemId` is `0` is copied: a new surface item is created on surface `3048`, and the resource is saved against the new cluster. The child grid is rebound. What those template rows contain is database data. **Unknown / Requires Confirmation.**

### 4. Add a cluster member

Trigger: save of a new Cluster Members row. Dated 2018-05-17 (field locks) and 2019-04-16 (mail).

- The name list is active school contacts of the selected school whose active year is the current calendar year, joined to `pal_User` for the setup customer code (`BindUserPicklist`).
- Choosing a user fills email, SACE number, and related fields through `populateUserInfo`. Email, SACE number, and active year are then disabled.
- On insert, if the member email is not empty, `SendClusterLinkedNotification` loads template `Cluster Linked Email Notification`, merges cluster fields, and sends subject `IEB CRM - Cluster Notice`. The member placeholder is the call with one argument (the email only). An update of an existing member does not send this mail.

### 5. Add or change a cluster resource

Trigger: save of Cluster Resources. Dated 2018-05-16 and 2019-03-06 / 2019-04-16.

- Resource type and email are disabled on populate.
- The name list is active cluster members of the parent cluster who have an email, joined to `pal_vUserShared` (`BindResourceUserPicklist`). An older query that listed users directly is commented out.
- Choosing a user copies that user's email. Choosing "Add New" (`-1`) opens `#modContactAdd`.
- Mail is sent only on update, and only when the email text changed and is not empty. The same template and subject are used, with `{ResourceTypeOrMember}` replaced by the resource type. A brand-new resource row does not call the mail method. The comment says "when linking new". **Implemented** as the update-only call. **Inferred** that the comment and the branch disagree.

### 6. Save a cluster meeting and optionally email members

Trigger: `btnSave_Click` on Cluster Meetings after `PerformSave` returns true. Dated 2018-05-17 (label copy) and 2019-03-06 (mail).

On save, the meeting copies the parent cluster's area, code, name, level display, province display, and subject name onto the meeting row. The comment says this is for reporting.

If `ClusterMeetings_ClusterMeetings_SendEmail` is checked, `SendClusterMeetingNotification` loads template `Cluster Meeting Email Notification`, merges meeting and cluster fields, and emails every active member of the cluster who has an email. Subject: `IEB CRM - Cluster Meeting Notice`. The loop that also emailed resources is commented out, with the note that resources are members and would receive the message twice.

### 7. Import cluster members from a spreadsheet

Trigger: child import on Cluster Information (`btnImportDataChild_Click`). Dated 2018-05-14.

For a published child surface, each import row is read for columns `Member Email` and `Member Name`. If no `pal_` user exists for that email and the setup customer code, a WebsiteUser is created with a random password stored through the framework encryption helper. The import then continues into `ImportDataToSurfacePublished`. The block is not limited to surface `3046`. A file that lacks those columns fails inside the row loop. **Implemented.**

### 8. Maintain a school contact

Trigger: populate and picklist change on School Contacts. Graham Rook 2018-05-15; Charlene van Heerden 2019-03-06 through 2019-04-26.

- Name list: active `pal_User` rows for the setup customer code, plus "Select" and "Add New". A WebsiteUser creating a contact does not get the user list unless `Session["contAdded"]` is set.
- "Add New" opens `#modContactAdd`. After a contact is added, a 2019-04-26 note says the screen is redirected back to the grid because a refresh did not run.
- Selecting a user fills mobile, email, and title from `pal_User`.
- Subjects are bound with a custom query of active `publ_SubjectManagement` rows (2019-04-23). The comment says `sp_GetSurfaceModulePicklist` returned no data value on INDE-2.
- Cluster name and cluster code sort controls on the contact form are hidden (2019-03-06).
- On the school child grid, the contact's cluster name and code are filled from the first active cluster member with the same email, then that member's first parent cluster. Only the first cluster is used.

### 9. Subjects, levels, affiliations, and schools

`SubjectManagement`, `ClusterLevels`, and `SchoolAffiliations` forms have empty custom save methods. Their populate methods only special-case setup code `STUD-1` (profile button text and device surfaces). **Implemented.** That branch is framework leftover. `Schools_form` populate has the same `STUD-1` branch and no other custom save. School identity fields are the properties on `oSchools`.

## Integrations

| System | Trigger | Direction | What was seen |
|---|---|---|---|
| SQL Server | Page and surface data access | Out | Active catalog `Framework_INDE-2_Demo` on `156.38.132.194`. Commented catalogs named `Framework_INDE-2`. |
| SMTP | Cluster member, resource, and meeting mail; framework mail | Out | Host `smtp.evolutionsoftware.co.za`, port 25, from `mailer@evolutionsoftware.co.za`. App-setting from address is `no-reply@evolutionsoftware.co.za`. Credentials omitted. |
| WCF `framework_service` | Framework client in `Web.config` | Out | `http://localhost:58913/framework_service.svc` and `http://service.ieb.co.za/framework_service.svc`. Binding security mode `None`. |
| SendEasy SMS | Framework client endpoint | Out | `http://sms.netgen.co.za/SMS_V2.asmx`. No CRM form was found calling it. |
| MediSwitch | App settings | Config only | QA gateway URLs are active. Live URLs are commented. No CRM form was found calling them. |
| Country web services | App settings | Config only | `getCountriesFromWebService` is `0`. |
| SonarQube | Pipeline on `main`, `develop`, and pull requests | Out | Scan only. See technical documentation for the project-key clash. |

Failure behaviour of mail: meeting notification catches exceptions and logs them without redirecting. Other surface custom methods redirect to `/error`. **Implemented.**

## Constraints

- Session timeout is 60 minutes. `anonymousIdentification` is enabled. `compilation debug` is `true`. `customErrors` is not set in this `Web.config`.
- `httpRuntime maxRequestLength` is `10048` (about 10 MB).
- Culture settings disagree: `globalization` is `en` / `en-US`, and `defaultCulture` is `en-ZA` (the key is present twice with the same value).
- The `website` app setting is present twice: `IEB (INDE-2 Local)` and `IEB CRM`. Cluster mail treats the value as a test environment when it contains `local`, `dev`, or `demo`. If the first or the combined value is what the runtime reads, notices are redirected to the admin mailbox and the original recipient is appended to the subject. Which duplicate key wins was not executed. **Unknown / Requires Confirmation.**
- Queries that build cluster codes, subject lists, member lists, and resource lists concatenate values into SQL text.
- There is no automated test project for these workflows.

## Functional requirements

### FR-001 — School record

The user can keep a school with name, type, centre number, phone, primary email, physical address, postal address, affiliations, NSC year, and PSI year.

Evidence: `oSchools`, `Schools_form.ascx.cs`.

Implementation: Implemented. Custom save is empty aside from the `STUD-1` branch.

Confidence: High for the fields. Low for any validation beyond the generated surface.

### FR-002 — School contact

The user can keep a contact on a school: user, title, email, mobile, RSA and non-RSA identity numbers, SACE number, area of responsibility, active year, principal flag, IEB-liaison flag, and subjects.

Evidence: `oSchoolContacts`, `SchoolContacts_form.ascx.cs`, `data/Scripts/INDE-2`.

Implementation: Implemented.

Confidence: High. The 2020 SQL script and the 2019 form disagree on the subject field type. See business rules.

### FR-003 — Subject and cluster level catalogues

The user can keep a subject (code, name, cluster level) and a cluster level (name, code abbreviation).

Evidence: `oSubjectManagement`, `oClusterLevels`.

Implementation: Implemented. No INDE-2 custom rule beyond the generated surface.

Confidence: High.

### FR-004 — Cluster identity and code

Saving a cluster with an empty code, when level, subject, and province all resolve, writes a code and then locks province, level, subject name, cluster name, and area.

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

Implementation: Implemented.

Confidence: High.

### FR-005 — Cluster membership and resources

A user below PowerUser can add members only when they are a resource on that cluster, and can add or change meetings only when their resource type is Cluster Administrator or Cluster Leader. Resource names are chosen from members who have an email.

Evidence: Cluster Information populate; `ClusterResources_form.ascx.cs` `BindResourceUserPicklist`.

Implementation: Implemented.

Confidence: High for the code path. Unknown whether resource type text in the database matches those two strings exactly.

### FR-006 — Cluster notices

A new member with an email, and an updated resource whose email changed, receive template `Cluster Linked Email Notification` with subject `IEB CRM - Cluster Notice`. A saved meeting with Send Email checked notifies active members with template `Cluster Meeting Email Notification` and subject `IEB CRM - Cluster Meeting Notice`.

Evidence: `SendClusterLinkedNotification`, `SendClusterMeetingNotification`, `btnSave_Click`.

Implementation: Implemented.

Confidence: High for the code. The test-mailbox redirect depends on FR confirmation of the `website` setting.

### FR-007 — Member spreadsheet import

Importing a published child file on a cluster creates a WebsiteUser for each new email in columns `Member Email` and `Member Name`, then imports the sheet.

Evidence: `ClusterInformation_form.ascx.cs` `btnImportDataChild_Click`.

Implementation: Implemented.

Confidence: Medium. The block is not tied to the members surface id.

## Error and empty cases

- Custom surface methods other than meeting mail redirect to `/error` on exception.
- Cluster code generation does nothing when any of the three lookups fails. The cluster can still be saved without a code. **Implemented.**
- Meeting mail skips members with an empty email and skips the whole send when the checkbox is clear.
- Resource mail does not run on insert.
- School-grid cluster columns stay empty when no active member has that email.
- A duplicate `website` key can force every cluster notice into the admin mailbox. **Inferred.** Not executed.
