# PRD — ieb (Evolution Software Unified Framework)

Evidence date: 2026-10-13. Inspected revision: `main` at `193927fc`.

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

Paths below are relative to `Evolution Software Unified Framework/Evolution Software Unified Framework/` unless they start at the repository root.

This PRD covers the repository as one product: the shared framework, the tenant inventory, and the behaviours that commits on `main` actually changed. It does not restate every tenant rule already traced in `silicon_ark/ieb-inde-1` through `ieb-inde-6` and `silicon_ark/SHOU-1`. Those documents describe other repositories. Where this tip differs, the difference is stated here.

## Product purpose

Ship the Evolution Software Framework as a multi-tenant ASP.NET Web Forms application. Each tenant is a website under `core_custom/{code}` over a SQL Server catalog, using shared `framework_library` and `framework_business` assemblies. **Implemented** as the solution layout. A written purpose statement does not exist. **Unknown** as a signed-off product goal.

`ReadME.txt` additionally names seven IEB projects. Only INDE-1, INDE-2, INDE-4, INDE-5, and INDE-6 have both a readme name and a `core_custom` folder. **Documented** versus **Implemented**. See `project-plan.md`.

The active change history after 2025-08-18 is INDE-1 (Exams). The 2025-08-18 commit is INDE-6 (Postbox) email queueing.

## Users and roles

### Framework user types

`business/enums/pNums.cs` `UserType`. **Implemented.**

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

### Sign-in

`core/Admin_SignIn.aspx.cs`, and the same page copied into `core-dev` and into every `core_custom` tenant except PROP-1 (that file did not contain the hardcoded bypass email).

FR-001 — Database sign-in

- Who: a person with a `pal_User` row.
- Trigger: submit on the admin sign-in page.
- The submitted password is encrypted, then the code loads users whose email, encrypted password, and `isActive = 1` match, prefix `pal_`.
- The user is stored in `Session["user"]` when `oSetup.code` equals `customerCode`, or when `customerCode` is `EVOL-1` ("allow on all instances").
- If that match fails, `pnlResult` is shown.
- Exceptions redirect to `/error`.

**Implemented.** The encryption key is hardcoded. It is omitted here.

FR-002 — Hardcoded administrator bypass

- The same page, before the database lookup, accepts one hardcoded email and password.
- On match it builds an in-memory `oUser`: `customerCode` `EVOL-1`, `isSetup` true, `recId` -1, `isActive` true, `userType` AdminUser, and stores it in `Session["user"]`.
- No `pal_User` row is read for that path.

**Implemented.** The secret values are omitted. This is a committed bypass, not a documented requirement. Human review should remove it and rotate the credential. Whether PROP-1's sign-in page still has an equivalent bypass was not confirmed.

FR-003 — Redirect after sign-in

- If `handler.ReturnSetup(false).isWebsite` is false, redirect to `/home`.
- Otherwise redirect to `Admin.aspx`.

**Implemented.**

FR-004 — Google sign-in method

`Admin_SignIn.aspx.cs` exposes a `WebMethod` `GoogleLogin`. The method body was not traced. What it creates or updates is **Unknown / Requires Confirmation**.

### Exam subject role (INDE-1)

`examHandler.GetSubjectResourceRole` is called by the examination form and view. The role ids themselves are defined in the INDE-1 custom code and are documented against the July 2025 snapshot in `silicon_ark/ieb-inde-1`. This inspection confirmed the call site below, not the full role enum. **Implemented** call. Role numbering: see that other document, and treat any drift since `972cb4e` as **Requires Confirmation** except where a later commit in this file says otherwise.

FR-005 — Block direct URL access to an exam subject

Trigger: populate of Examination Authoring and Moderation, form and view, when the surface item already has a `recId` (view: whenever `recId > 0`; form: `recId > 0` and not `isNew`). Commit `972cb4e` (2026-04-17). Files: `core_custom/INDE-1/controls/surface_custom/ExaminationAuthoringandModeration_form.ascx.cs` and `ExaminationAuthoringandModeration_view.ascx.cs`.

- Applies when `user.userType` is less than `SuperUser` (WebsiteUser and PowerUser).
- `GetSubjectResourceRole(item.recId, user)` is called.
- Role `0` redirects to `/pages/examinationauthoringandmoderation` and completes the request.
- SuperUser, AdminUser, and CustomUser skip this check because their values are not less than SuperUser.

**Implemented.**

## Major workflows

### 1. Open a canvas

The generic site and each tenant expose `Canvas.aspx`. A canvas name selects a surface control. Much of the screen is not hardcoded: surface fields are rows the renderer draws. **Implemented** as the page and the `pNums.Module` / `FieldType` model. The full renderer was not re-read for every tenant.

FR-006 — INDE-1 examination form session

`core_custom/INDE-1/Canvas.aspx.cs`, commits `972cb4e` and `193927fc`.

- For surface name `ExaminationAuthoringandModeration`, the control is attached to the page before `ReloadControl`, so view state tracks the bound picklists and entity.
- A `SurfaceControlParcel` in session is kept only when its surface `recId` equals the surface being opened. A parcel from another surface (the comment cites Subject Management) is disposed, so the URL id is not discarded.
- When no matching parcel exists, `Session["ExaminationFormItemId"]` loads the `oSurfaceItem`, builds a parcel if the surface is published, and clears that session key.
- `ActiveTabPanel` is cleared only when the request is not a postback.

**Implemented.**

FR-007 — Resource dropdowns on first load

Same form file, `972cb4e`.

- `PopulateSurfaceFormCustom` can run from `ReloadControl` and again from `Page_Load`. A flag skips the second non-postback bind.
- Examiner, internal moderator, reviewer, external moderator, and assessment specialist picklists clear selection, bind, insert a "Select" item with value `0`, then select the stored id or `0` if the stored id is not in the list.
- The external-moderator query is `pal_User` where `isActive = 1` and `customerCode` equals setup `code`, ordered by surname. Display text is `surname + ', ' + name`.

**Implemented.** The query concatenates `customerCode` into SQL.

### 2. Exam maintenance shipped after the email-queue commit

These are **Implemented** as commit subjects plus the files each commit touched. The generated form files are about 17,000 lines. Only the diffs were read, not every branch inside them.

| ID | Commit | Behaviour evidenced by the diff or subject |
|---|---|---|
| FR-008 | `bc7f1f2` | Duplicate examination papers: SQL scripts `dedupe_publ_ExaminationAuthoringandModeration_soft_delete.sql` and `backfill_publ_ExaminationAuthoringandModeration_NoneSubjectSummary_SubjectName.sql` sit under `core_custom/INDE-1/scripts/sql/`. Form and grid code-behind grew in the same commit. The exact soft-delete predicate was not line-read. |
| FR-009 | `0010068` | Additional paper types. `insert_examination_type_lookups_backup_variants.sql` inserts lookup `4188` "AET - June - Back-up" code `AETJ` and lookup `4189` "AET - November - Back-up" code `AETN`. Print-ready paper surfaces and the examination grid markup changed in the same commit. An earlier add/revert/re-add of an AET backup option is `08358ed`, `cc9d2fe`, `03f9be2`. |
| FR-010 | `bdc6799` | Long file names: four document-pack form code-behinds (`ExamDocs`, `ExternalMod`, `InternalMod`, and the pack form) each changed by a few lines. The resulting filename rule was not extracted line by line. |
| FR-011 | `fd80a94`, `7cdc9bc` | Opening a record fills missing information (`examHandler.cs` and the examination form). New subject create gained a further 74 lines (`#140`). The field list was not extracted. |
| FR-012 | `509c08f` | Unlink moderator (`#152`, `#153`, `#154`). The examination form, view, grid, document-pack forms, and `surfaceNotes.ascx.cs` changed. The resulting unlink rule was not extracted line by line. |
| FR-013 | `2997ebf` | Exam paper filter: five lines in `ExaminationAuthoringandModeration_grid.ascx.cs`. The filter expression was not extracted. |
| FR-014 | `bec948b` | Revision numbering sequence: one line in the examination form. The new sequence was not extracted. |

`e69e29e` added `EMComplianceFeedbackCRITERION*` surface controls (criteria 1–7 and 9–11 were visible in the stat; criterion 8 was not in the first page of the stat). **Implemented** as added files. Whether criterion 8 exists is **Unknown / Requires Confirmation**.

Exam status mail, document-pack revision, and the subject-resource role matrix are specified in `silicon_ark/ieb-inde-1` against a different repository tip (`9bf5ac3`, 2025-07-03). Those rules were not re-traced here. **Requires Confirmation** that they still match this tip aside from FR-005 through FR-014.

### 3. Postbox email queue

Commit `38b1cbb` (2025-08-18). **Implemented.**

FR-015 — Queue Postbox mail instead of sending it inline

`core_custom/INDE-6/App_Code/classes/PostboxEmails.cs` calls `xEmailQueue.AddToQueue` for:

| Source module string | When |
|---|---|
| `Postbox` | School-contact mail and IEB-team submissions mail. Priority 5. `sourceId` is the file-explorer metadata `recId` |
| `WelcomeEmail` | Welcome / reset mail. Priority 5. `sourceId` is the user `recId`. The method returns true after the queue call |
| `Postbox_RegModDocs` | Regional-moderator document mail |
| `Postbox_RegModReport` | Regional-moderator report mail |

From-address for the school and IEB-team messages is setup `communicationEmail`. Subject and body builders are the existing Postbox helpers. The welcome subject prefix in that method is `"IEB - "`.

`AddToQueue` returns `-1` when it catches an exception. The welcome method does not check that return value. **Implemented.**

FR-016 — Queue row

`business/classes/xEmailQueue.cs` and `library/objects/oEmailQueue.cs`.

- Saved with prefix `pal_` via `xData.SaveTyped`. The physical table name is **Inferred** as `pal_EmailQueue` from the prefix plus the type name with the leading `o` removed. No `CREATE TABLE` for this object was found under `*.sql`.
- Status values: Pending 0, Processing 1, Sent 2, Failed 3, Retry 4.
- New rows start Pending, retry count 0, `scheduledFor` and `processedAt` set to now, priority from the caller (Postbox uses 5).
- `createdBy` is `handler.ReturnUser().recId` when a user is in context.

Defaults when app settings are absent:

| Key | Default |
|---|---|
| `EmailQueue_BatchSize` | 20 |
| `EmailQueue_DelayBetweenBatchesMs` | 5000 |
| `EmailQueue_DelayBetweenEmailsMs` | 1000 |
| `EmailQueue_MaxRetries` | 3 |
| `EmailQueue_ProcessIntervalMinutes` | 2 (read by the Windows service) |

INDE-6 `Web.config` does not define the `EmailQueue_*` keys. `tools/EmailQueueProcessor` has no `App.config` in git. A deployed config may override the defaults. **Unknown / Requires Confirmation.**

FR-017 — Drain the queue

`tools/EmailQueueProcessor/EmailQueueProcessorService.cs`. Service name `IEB Email Queue Processor`. The project is in `INDE-6.sln` and is not in the unified solution's project-name list.

- On start, a timer calls `xEmailQueue.ProcessQueueAsync` on the interval above.
- Processing loads rows with status Pending, ordered by `priority,createdDate`.
- Rows are grouped into batches. Each item is marked Processing, then `communication.SendAnEmail` runs.
- Success sets Sent. Failure increments an in-memory retry count. At `maxRetries` the status becomes Failed. Otherwise the status becomes Retry and an in-memory `scheduledFor` is set to now plus `2 ^ retryCount` minutes.
- `UpdateStatus` reloads the row and writes status, `processedAt`, `errorMessage`, and `modifiedDate`. It does not write `retryCount` or `scheduledFor`.
- `ProcessQueueAsync` does not load status Retry, and it does not filter on `scheduledFor`.
- A `pal_` log row (`oEmailQueueLog`) is written on each status change.
- Inside a batch, sends are started together (`Task.WhenAll`). The per-email delay is awaited after each send, so it does not serialise the batch.

**Implemented.** The retry fields are stored on the object and then not persisted, and Retry rows are not selected again. Whether that is an accepted defect is **Requires Confirmation**. See `business-rules.md`.

### 4. Outbound SMTP

FR-018 — TLS before send

`library/classes/communication.cs`, commit `1ee1420`. Immediately before `SmtpClient.Send`, the code sets `ServicePointManager.SecurityProtocol` to TLS 1.2, TLS 1.1, and TLS. **Implemented.**

### 5. Framework modules that the generic site still contains

`core/` includes admin pages for users, canvases, surfaces, content, products, banners, campaigns, calendar, FAQ, gallery, media, loyalty, medical practice tables, sales items, tasks, templates, setup, SMS credit, support, and reports. `pNums.Module` enumerates the same areas (content through purchases, including medical, debtors, loyalty, and file explorer).

These pages are in the generic site and are copied into tenant sites. This PRD treats them as framework capability. It does not treat every page as a requirement of every tenant. Whether a database canvas still opens a module for a given tenant is **Unknown / Requires Confirmation** except where a commit above names the surface.

### 6. WCF service

FR-019 — Framework service contract

`service/App_Code/Iframework_service.cs`. **Implemented** as the contract. Callers were not traced.

| Operation | Inputs |
|---|---|
| `ProcessInteraction` | interaction bytes, connection key, transactional flag, table prefix, collection flag |
| `GetVersion` | customer code, licence key, version number, file name |
| `SaveVersion` | version bytes, version number, file name |

`service/Web.config` catalogs include `Framework_EVOL-1` and `Framework_REAP-1`. Which key is the active `conn` was not isolated from commented alternatives.

### 7. Unmerged proposal branch

`feature/ieb-client-proposals` at `4740d58` (2026-08-21) is not on `main`. The subject says it adds an AI model selection line item to a pilot proposal. The diff was not read. It is not a requirement of `main`. **Unknown / Requires Confirmation.**

## Integrations

| System | Direction | Evidence | Notes |
|---|---|---|---|
| SQL Server | Read/write | `conn` appSetting, `xData`, `data.sqlproj` | Catalog per tenant. Credentials omitted |
| SMTP | Outbound | `communication.SendAnEmail` | TLS protocols set before send. Used directly and by the email queue |
| SendEasy SMS | Outbound client | `business/Service References/send_easy`, `xSendEasy.cs`, app settings `smsU` and `smsP` | Credential values omitted. Which screens still send SMS was not traced |
| Mediswitch | Outbound HTTPS | `Mediswitch.Gateway` keys. INDE-6 leaves QA URLs active and live URLs commented | `pNums.MediswitchOperationType`: MSV, Claim, Reversal |
| Country web services | Outbound HTTP | `www.webservicex.net` country ASMX URLs | INDE-6 `getCountriesFromWebService` is `0` |
| `service.ieb.co.za` | Configured host | Present in several tenant `Web.config` files, including INDE-1, INDE-2, INDE-4, INDE-5, INDE-6 | The binding and payload were not re-read |
| PayGenius | Types only | `library/Payment`, `PayGeniusModels` | No call path was traced |
| Google sign-in and Google Sheets | Partial | `GoogleLogin` web method; `GoogleSheetsHelper`; `SchedulerTool/GoogleSheetImports.cs` | Secrets file contents omitted. End-to-end behaviour **Unknown** |
| SonarQube | CI only | Root pipeline | Does not deploy the site |

## Important constraints

- Websites compile as ASP.NET Web Forms on .NET Framework 4.5 in the configs that were read. Library and business projects target v4.5.
- Culture app setting `defaultCulture` is `en-ZA` on the configs that were read. `DateFormat` on INDE-6 is `yyyy-MM-dd hh:mm tt`.
- User data for the framework is `pal_` prefixed. Published surface tables in the exam scripts use the `publ_` prefix (`publ_ExaminationAuthoringandModeration`).
- There is no automated test project in the tree (no `*.Tests` project was listed).
- The pipeline does not build or publish the websites.

## Error and edge behaviour that was read

| Situation | Behaviour | Provenance |
|---|---|---|
| Sign-in match fails | Result panel shown | Implemented |
| Sign-in throws | Redirect `/error` | Implemented |
| Exam URL, role 0, user below SuperUser | Redirect to the examination list URL | Implemented |
| Stored picklist id missing from the user list | Selection set to `0` ("Select") | Implemented |
| Stale surface parcel | Parcel disposed; URL item id can load | Implemented |
| Email send throws or returns false | In-memory retry, then Failed or Retry status | Implemented, with the persistence gap in FR-017 |
| Welcome queue insert throws | Exception is caught inside `AddToQueue` and the welcome method still returns true | Implemented |
| Empty school-contact address | Postbox loop `continue`s | Implemented in the `38b1cbb` diff |

## Out of scope of this PRD

- Line-by-line exam moderation status matrix. See `silicon_ark/ieb-inde-1`, with the drift note above.
- Postbox folder, school, and download rules beyond the queue change. See `silicon_ark/ieb-inde-6`.
- Shoulder Practice billing and the later password policy. See `silicon_ark/SHOU-1`.
- `ieb_template` / `EvoSoft_Lib`. Different product.
