# Business rules — ieb_template

Evidence date: 2026-10-12. Revision: `main` `493c34f`.

No business-rules document existed in the repository or in `silicon_ark/ieb_template/` before this file. Rules below are what the code enforces. Each rule is marked **Implemented** or **Inferred**. None of these were found as a signed policy. Where the code looks accidental, that is stated.

## Sample website users

### BR-001 — Anyone who can open the page can change users

**Implemented.** `Default.aspx` has no login. `Web.config` enables anonymous identification. Add, update, and delete run on postback with no role check.

Evidence: `SampleWebsite/Default.aspx.cs`, `SampleWebsite/Web.config`.

### BR-002 — A new user is stored only when that email is not already present

**Implemented.** Add calls `VerifyExists` with identity field `recId`, type `oUser`, criteria column `email`, and the typed address. A match sets the label and does not call `SaveTyped`.

The comparison is built as `email = '{value}'` after `utils.formatSqlString`, which doubles single quotes and also rewrites backticks and a curly apostrophe to doubled quotes.

Evidence: `btnAdd_Click`, `xData.VerifyExists`, `utils.formatSqlString`.

### BR-003 — Update does not repeat the duplicate-email check

**Implemented.** `gvUsers_RowUpdating` always calls `UpdateTyped`. Two rows can end up with the same email if the second was edited into the first's address.

Evidence: `gvUsers_RowUpdating`. Whether that is accepted policy is **Unknown / Requires Confirmation**.

### BR-004 — Add and update force user type 1

**Implemented.** Both handlers set `usr.userType = 1`. The visitor cannot choose a type. `oUserType` is not consulted.

What type 1 means is **Unknown / Requires Confirmation**.

### BR-005 — Add does not require a name, surname, email format, or password

**Implemented.** The text boxes have no `RequiredFieldValidator`. `utils.validateEmail` is not called. `oUser.password` stays empty. An empty email is checked for duplicates and then inserted when none exist.

Evidence: `Default.aspx`, `btnAdd_Click`.

### BR-006 — Add reports success only when the new identity is greater than zero

**Implemented.** Label `New user added.` and the inputs are cleared only when `SaveTyped` returns an id `> 0`. A non-positive id leaves the label blank and still reloads the grid.

Evidence: `btnAdd_Click`.

### BR-007 — Delete reports a missing user when the typed delete returns false

**Implemented.** Success text is `User deleted.` Failure text is `The user {recId} could not be found.`

Evidence: `gvUsers_RowDeleting`.

### BR-008 — The grid identity is read from cell 1

**Implemented** as written. Update parses a text box in cell 1 as `recId` and reads name, surname, and email from cells 2, 3, and 4. Delete parses cell 1's text. The grid also auto-generates an edit button and a delete button, which add command columns before the data columns.

**Inferred:** cell 1 may be a command column rather than `recId`. The page was not run. **Requires Confirmation**.

## Typed persistence

### BR-009 — A type maps to prefix plus the type name without its first character

**Implemented.** `oUser` with prefix `evo_` is table `evo_User`. The sample SQL uses that table name. `xData` default prefix is `evo_`. Callers can pass another prefix.

Evidence: `dynamicInteraction.buildSelectInteraction`, `dataInteraction.buildSelectInteraction`, `xData` constant `_tablePrefix`.

### BR-010 — The identity column is omitted from insert and update assignments

**Implemented.** Insert and update skip the property whose name matches `identityField` (case-insensitive). Insert appends `SELECT Ident_Current('{table}')`. The sample identity field is `recId`.

Evidence: `dataInteraction.buildInsertInteraction` and `buildUpdateInteraction`. The dynamic interaction uses the same table-name rule. A full line-by-line compare of every dynamic SQL branch was not copied into this rule; the sample save path goes through `dynamicInteraction.ProcessInteraction`.

### BR-011 — Update and delete append the caller's where text

**Implemented.** `UpdateTyped` and `DeleteTyped` set `whereClause` to `{identityField} = {identityValue}` with no quotes around the value. `VerifyExists` quotes string criteria.

Evidence: `xData.UpdateTyped`, `xData.DeleteTyped`, `xData.VerifyExists`.

### BR-012 — A custom query replaces the generated statement

**Implemented.** When `oInteraction.query` is non-empty after trim, `dataInteraction` uses that text for select, insert, update, delete, verify, and select distinct. `GetCustomTypedTable` is the sample's custom select.

Evidence: `dataInteraction.HandleInteraction`, `xData.GetCustomTypedTable`.

### BR-013 — Dates before 1900 are rewritten to 1900-01-01 for display formatting

**Implemented.** `utils.fixDate` parses the value, replaces `DateTime.MinValue` and any year below 1900 with `1900/01/01`, then formats with appSetting `DateFormat`. On failure it returns that same sentinel in the configured format.

The sample site's `DateFormat` is `yyyy-MM-dd hh:mm tt`. The constructor `app.config` does not define `DateFormat`, while the constructor's copy of `utils.fixDate` reads that key.

Evidence: `BaseLibrary/business/classes/utils.cs` `fixDate`, `SampleWebsite/Web.config`, `EvoSoft_Constructor/app.config`.

### BR-014 — Age calculation treats 1 January 1900 as "no date"

**Implemented.** `utils.FormatAge` leaves years, months, and days unset when the start date's short-date string is `1900/01/01` or `01/01/1900`. Otherwise it returns whole years, months, and days between start and end.

Evidence: `utils.FormatAge`. Not called by the sample page.

## Identity numbers and phones

### BR-015 — A South African ID number must pass length, digits, date, citizenship digit, and check digit

**Implemented.** `utils.ValidateIDNumber` returns an empty string when the value is accepted, and a message when it is not.

- Trimmed length must be 13.
- Digits only (`0-9`). A dot or a minus fails even if the numeric parse would succeed.
- Characters 3-4 are a month 1-12.
- Characters 5-6 are a day that exists in that month. The day count is computed from a date built with `utils.fixDate` and year offset `iYY` (00-99 added onto a base date). Century handling inside this check is not the same code path as `FormatBirthdayFromID`.
- Character 11 (index 10) must be 0 or 1. The comment does not name this digit. A value above 1 returns `This is an invalid SA ID Number.`
- A Luhn-style odd/even sum over the first 12 digits must match the 13th digit.
- The method comment says a gender check is not applied here because gender is not known. `formatGenderFromID` is a separate method.

The sample page does not call this method. `oUser.idNumber` is not on the form.

### BR-016 — Gender from an ID number uses the first digit of the sequence

**Implemented.** `formatGenderFromID` requires length greater than 6 and an integer parse. If the character at index 6 is greater than 4, the result is `M`. Otherwise it is `F`. A shorter or non-numeric value returns empty.

The comment inside `ValidateIDNumber` describes males as the four-digit sequence from position 7 being at least 5000. `formatGenderFromID` looks at one digit and a threshold of 4, which is the same first digit of that sequence. **Inferred** that both comments describe the same SA ID gender digit. The page uses neither method.

### BR-017 — Birthday from an ID number treats a leading zero as year 2000

**Implemented.** `FormatBirthdayFromID` requires length greater than 5 and an integer parse. If the first character is `0`, the two-digit year is increased by 100 before it is added to a base of year 1899 (`DateTime.MinValue.AddYears(1899)`). Other leading digits use the two-digit year as-is on that base, so a leading `1` through `9` lands in the 1900s. Month and day are taken from characters 3-4 and 5-6.

Not called by the sample page. Whether IDs from 1900-1909 (leading zero in the old century) are mis-read is a consequence of this branch. **Requires Confirmation** if this helper is still a product rule.

### BR-018 — SMS numbers are rewritten with prefix +27 and the last nine digits

**Implemented.** `SendSMSMessage` strips spaces and calls `FormatMobile(number, "+27")`. `FormatMobile` returns the prefix plus the last nine characters when the input length is greater than 8. Shorter inputs return empty. The send continues only when the formatted value's length is at least 10, which `+27` plus nine digits satisfies.

`SendingActive` must parse as true or the method returns false without posting. Gateway keys are not in the sample `Web.config`. The sample page does not send SMS.

## Mail and faults

### BR-019 — Outbound From is the config value, not the object's from address

**Implemented.** When `oEmail.fromAddress` is non-empty, `SendAnEmail` sets `MailMessage.From` from appSetting `from` and adds the object's address to `ReplyToList`. To, cc, and bcc split on `;`.

Evidence: `communication.SendAnEmail`.

### BR-020 — Exceptions are inserted into evo_Exceptions and emailed unless a debugger is attached

**Implemented.** `HandleException` writes class, function, message (quotes doubled), user id, and `getdate()`. A null user becomes user id 0. A non-int user is read as `user.recId`. Then `EmailException` runs. The email is skipped when `Debugger.IsAttached` is true.

If the insert throws, the transaction is rolled back and the new exception is swallowed. `finally` still calls `CompleteSQLTransaction`.

Evidence: `exception.cs`.

### BR-021 — A failed SMS gateway response is emailed to support

**Implemented.** A HTTP response that does not contain `OK` builds an email to appSetting `support` with subject `SMS Failed Report - sending to{number}` (no space before the number) and body equal to the response plus the SMS body.

Evidence: `communication.SendSMSMessage`.

## Constructor and export

### BR-022 — Constructor metadata includes tables and views; the library's own table list is tables only

**Implemented.** The constructor and the exporter query `sysobjects` types `U` and `V`. A comment says views were added on 2015-04-18 (JasR). `EvoSoft_Lib.business.constructor.GetTables` filters type `U` only.

Evidence: `EvoSoft_Constructor/classes/Constructor.cs`, `BaseLibrary/business/classes/constructor.cs`.

### BR-023 — Column type sysname and tAppName are dropped when reading a table

**Implemented.** `GetTableFields` excludes `o3.[name]` values `sysname` and `tAppName`.

Evidence: `EvoSoft_Constructor/classes/Constructor.cs`.

### BR-024 — Export reads every column of the selected object

**Implemented.** The query is `SELECT * FROM ` plus the combo text. There is no row filter and no column list.

Evidence: `ExportData.cs`.

## Access rules that were not found

No password policy, lockout, session timeout, row-level tenant, or audit table besides `evo_Exceptions` was found. `oUser.isActive` is not checked by the sample page. **Unknown / Requires Confirmation** whether a host outside this repo adds those rules.
