# Business rules — lifechoiceMVC_net

Evidence date: 2026-10-10.

Source: `master` at `69783e2`. Rules are taken from code that runs. Comments are quoted only when they conflict with that code. Nothing here is an approved policy beyond what the repository enforces.

Provenance labels: **Implemented**, **Inferred**, **Conflict**, **Unknown / Requires Confirmation**.

## Identity and passwords

| Rule | Label |
|---|---|
| Email is the user name | **Implemented** (`UserName = email` on practice, practitioner, and generic registration) |
| Email must be unique among Identity users | **Implemented** (`RequireUniqueEmail = true`, plus an explicit lookup on practice registration) |
| Password length at least 6, with upper, lower, digit, and a non-alphanumeric character | **Implemented** in `ApplicationUserManager` |
| Five failed attempts lock the account for five minutes | **Implemented** as manager defaults |
| Login does not count a failed password toward that lock | **Implemented** (`shouldLockout: false`). The lockout rule does not apply to the login action that was read |
| Forgot-password reveals nothing when the user is missing or the email is unconfirmed. The same confirmation page is shown | **Implemented** |
| Password reset requires a code. GET `ResetPassword` without a code returns `Error` | **Implemented** |

## Account types

`ApplicationUser.AccountType` is an integer. The repository does not contain a single table of meanings that all screens use. `HCDB.user_type` is only `user_type_key` and `user_type_desc`, with no seed rows in the SQL project.

| Place | What the code does with the number | Label |
|---|---|---|
| `AccountController.Login` | 1 stores session `practitioner`. 7 stores session `practiceadmin`. Any other number stores nothing | **Implemented** |
| `HomeController.AccountRedirect` comments | 1 Healthchoice user, 2 patient, 3 MHC user, 4 provider. The switch only has code for those cases; 1, 2, and 3 return the view with no redirect; 4 redirects to `Provider/Edit` | **Implemented** behaviour, **Conflict** with the login switch |
| `HomeController.Index` | 4 is treated as provider and may show the 48-hour contract message | **Implemented**, **Conflict** with login, where 7 is the practice admin |
| `SiteAdminController` comment | 1 is "Healthchoice User" and may assign any user type | **Implemented** for that screen |
| `Register` and `RegisterPractice` GET | Prefill `AccountType` 4, marked "remove this after testing" | **Implemented** |
| `ConfirmEmail` and `ConfirmAccount` | Only account type 7 turns the practice active or sends the activation email | **Implemented** |

**Conflict.** A practice registered with the prefilled account type 4 does not receive the account-type-7 activation behaviour, and login will not mark the session as `practiceadmin`. A practitioner path expects account type 1 for the practitioner menu, while the invite form posts whatever `AccountType` the view submitted.

**Unknown / Requires Confirmation:** which integers are approved, and whether production data uses 7 for practice managers despite the prefill of 4.

## Who may see which menu

| Condition | Menu | Label |
|---|---|---|
| Anonymous | Home, About, Terms, Contact, Register (practice), Log in | **Implemented** |
| Signed in, role SiteAdmin or ProviderAdmin or MCOAdmin or Provider | Home (`SiteAdmin/Index`), Site Admin dropdown (user list, add support user) | **Implemented** |
| Signed in, any other role | Home still links to `SiteAdmin/Index`. Admin dropdown is not rendered | **Implemented** |
| Session `userLevel` equals `practiceadmin` | Practitioners, Patients, Administrators | **Implemented** |
| Session `userLevel` is anything else, including null | Patients only. Null throws on `.ToString()` in the layout | **Implemented** |

Controller authorization does not match the menu:

| Controller | Attribute | Label |
|---|---|---|
| `AccountController` | `[Authorize]`, with `[AllowAnonymous]` on login, register, confirm, forgot password, external login | **Implemented** |
| `ManageController`, `CommonController` | `[Authorize]` | **Implemented** |
| `SiteAdminController` | Roles `SiteAdmin,Provider,MCOAdmin,ProviderAdmin`. POST `Register` is `[AllowAnonymous]` | **Implemented**. Anonymous support-user registration is **Unknown / Requires Confirmation** as an intended rule |
| `UserTypeController`, `UserRoleController` | Role `SiteAdmin` | **Implemented** |
| `HomeController`, `PeopleController`, `ProviderController`, `ProviderRegistrationController`, `NetworkAffiliationsController` | No authorize attribute | **Implemented**. Practice data actions rely on session, not on a role |

## Practice registration rules

| Rule | Label |
|---|---|
| Terms must be accepted | **Implemented** |
| Practice number is required, exactly 7 digits, label "BHF Practice Number / PCNS" | **Implemented** on the model |
| Affiliation number is required, length 7 to 10, label "SASP Number" | **Implemented** |
| Partner initiative (`NetworkAffiliationId`) is required | **Implemented** |
| VAT number max 10, company registration number max 30, both optional | **Implemented** |
| Practice number must not already exist | **Implemented** as a query, not as a unique constraint in the EF model |
| Trimmed practice name must not already exist | **Implemented** as a query |
| New practice is stored inactive (`ActiveIndic = false`) and without `ValidatedDate` | **Implemented** |
| The registering person is stored as person type "Practice Manager" and the practice membership is active | **Implemented** |
| Practice manager date of birth is 1900-01-01 and is not collected on this form | **Implemented** |
| Work email and work cellphone are stored unvalidated | **Implemented** |
| A second Identity user with the same email is rejected | **Implemented** |
| A person contact described as `email` with the same value is rejected | **Implemented**. Seed description is `Email`. Match depends on collation. **Requires Confirmation** |
| If the database transaction fails after the Identity user is created, that user is deleted | **Implemented** |
| If the confirmation or admin email throws, the user still sees the thank-you page | **Implemented** |

## Activation rules

Two flags exist. They are set by different actions.

| Flag | Set by | Read by | Label |
|---|---|---|---|
| `Practice.ActiveIndic` | Starts false. `ConfirmEmail` sets true only when `AccountType` is 7, on the latest inactive practice for that person | `GetPracticeIdFromCode` requires it true. People lists use `PracticePerson.ActiveIndic`, not this flag | **Implemented** |
| `Practice.ValidatedDate` | Set to now when `ConfirmAccount` is called with `active` true. Cleared when `active` is false | Login refuses the practice when it is null ("Practice has not been activated") | **Implemented** |
| Person email `IsValidated` | `ConfirmEmail` | Not checked by login | **Implemented** |
| Identity `EmailConfirmed` | `ConfirmEmail` via the Identity token | Forgot-password. Home page copy when false | **Implemented** |

**Inferred:** the intended sequence is email confirmation, then an administrator setting `ValidatedDate`, then login. The code only half-matches that sequence, because `ActiveIndic` and `ValidatedDate` are different fields and account type 7 is special-cased.

**Implemented:** activate and decline links are anonymous query strings (`practiceId`, `userId`, `active`) with no separate secret. Possession of the URL is enough.

**Unknown / Requires Confirmation:** whether activation was supposed to require an authenticated Healthchoice administrator.

## Login rules

| Rule | Label |
|---|---|
| A person row must exist for the Identity user | **Implemented** |
| At least one active practice membership must exist | **Implemented** |
| The first membership by practice id is the session practice. Later memberships are not offered as a switcher | **Implemented** |
| That practice must have `ValidatedDate` | **Implemented** |
| Role name `SiteAdmin` lands on the site-admin index. The role check happens after the practice checks, so a site admin without a person and a validated practice does not reach the redirect | **Implemented**. **Unknown / Requires Confirmation** whether site admins were supposed to skip the practice gate |
| Remember-me is passed through to Identity | **Implemented** |

## Practitioner invite and registration

| Rule | Label |
|---|---|
| Invite is stored only after the email is sent | **Implemented** |
| Unknown invite id shows the error view | **Implemented** |
| Terms must be accepted | **Implemented** |
| ID type 1 (comment and filter treat this as South African ID) must be 13 characters. No checksum | **Implemented** |
| Duplicate HPCSA number (`PersonField` name `prov_hpcsa_no`) is rejected | **Implemented** |
| Duplicate email on contact type id 1 is rejected | **Implemented**. Seed contact type 1 is `Email` |
| Duplicate identity number on `Person` is rejected | **Implemented** |
| New practitioner membership is active and typed "Practitioner" | **Implemented** |
| Discipline choices are the external list intersected with lookup `alloweddisciplines` | **Implemented**. Lookup rows are not in the SQL seed. **Requires Confirmation** |

## Patient and person data

| Rule | Label |
|---|---|
| Patients, practitioners, and admins are scoped by `Session["PracticeId"]` | **Implemented** |
| Patient medical scheme and membership number are `PersonField` values `MedicalScheme` and `SchemeNumber` | **Implemented** |
| Scheme dropdown reads lookup `medschemes`, which is not in the SQL seed | **Requires Confirmation** |
| People title dropdown reads lookup `titleid`. Legacy provider screens read lookup `title`, which is seeded | **Conflict** in lookup names |
| Person types Dependent, Next of Kin, Account Payer, and Referring Doctor are seeded and have no screen | **Implemented** seed, unused by controllers |
| Gender lookup seed is M/Male and F/Female | **Implemented** in SQL seed. The people screens load it when they call `GetLookUpDropDown("gender")` |

## Provider legacy rules

| Rule | Label |
|---|---|
| Short create and provider registration add Identity roles using the literal keys `"3"` and `"7"`. Comments say those keys are Provider and ProviderAdmin | **Implemented**. The keys are not role names. `AddUserRole` treats the argument as `user_role_key` and looks up `user_role_value`. If the lookup fails, the behaviour depends on `GetUserRoleValueByKey` |
| Home page, account type 4, tells the user to wait up to 48 hours, initial every page of a contract, and fax (086) 730-3236 or email `contracts@healthchoice.co.za` | **Implemented** copy. The phone number text in the same paragraph is `0860blablabla` |
| The role check that suppresses that message compares `role.ToString()` to `"Provider"` | **Implemented**. Identity role objects do not stringify to the role name, so the message is shown whenever account type is 4 |

## Reference processes that the website does not run

**Implemented** only as SQL seed (`Script.ProcessTypeTableData.sql`, `Script.ProcessStepTableData.sql`):

- Provider registration, including banking, then send and complete.
- Non-network provider registration, without the banking step.
- Patient registration: personal details, funder, address.
- Chronic medicine authorisation: details, diagnosis, product, send to scheme.

No MVC action advances `process_step`. These are not enforced business rules of the website. **Unknown / Requires Confirmation** whether they remain business requirements for another system.

## Mail rules

| Rule | Label |
|---|---|
| From address `info@healthchoice.co.za` | **Implemented** |
| User confirmation subject "Healthchoice - Confirm your account" | **Implemented** |
| Admin subject "Healthchoice - Activate Practive Account" | **Implemented**, including the spelling |
| Activation notice subject "Healthchoice - Practice Activated" | **Implemented** |
| Invite subject "{practice name} has invited you" | **Implemented** |
| Test environment does not add a test banner. The banner code is commented out | **Implemented** |
| SMS is not sent | **Implemented** |

## Security-relevant behaviour (as implemented, not as a recommendation)

Documented so the rules are visible. This is not a change request.

- Support-user POST is anonymous.
- Practice activate and decline links are anonymous and unguessable only to the extent the URL stays private.
- Several data controllers have no authorize attribute.
- Database, mail, Facebook, and API credentials are stored in the repository. Values are intentionally omitted here.

**Unknown / Requires Confirmation:** which of these are accepted risks for a 2019 test environment (`EnvironmentSetting` = `TEST`) and which were defects.
