# PRD — lifechoiceMVC_net

Evidence date: 2026-10-10.

Source: `git@bitbucket.org:overdrive/lifechoicemvc_net.git`, `master` at `69783e2` (2019-12-03).

There is no separate requirements document in the repository. Requirements below were recovered from controllers, views, models, seed data, and SQL scripts. Provenance is marked. A comment is not treated as the live rule when the code does something else.

## Product purpose

**Inferred** from the layout title, registration emails, and the anonymous Register link:

Healthchoice lets a practice register itself, asks the registrant to confirm an email address, asks an administrator to activate the practice, and then lets signed-in practice staff invite practitioners and maintain people linked to that practice.

The product name in the UI is Healthchoice. The repository is `lifechoiceMVC_net`. The relationship between those names is **Unknown / Requires Confirmation**.

The older SQL project describes additional product areas that the current MVC screens do not implement: chronic medicine authorisation, patient funder capture as a process, and a multi-step provider contracting wizard. Those are documented as schema and seed data, not as working website workflows. See FR-014.

## Users and interactions

| User | How the code treats them | Provenance |
|---|---|---|
| Anonymous visitor | Home, About, Terms, Contact, practice registration, login, invited practitioner registration | **Implemented** in `_Layout.cshtml` and `AccountController` |
| Practice manager | Created by `RegisterPractice` with person type "Practice Manager". Login sets `Session["userLevel"]` to `practiceadmin` only when `AccountType` is 7 | **Implemented** |
| Practitioner | Invited by email, registers against the invite, person type "Practitioner". Login sets `Session["userLevel"]` to `practitioner` when `AccountType` is 1 | **Implemented** |
| Patient | Person type "Patient", edited under the practice. No self-registration action was found | **Implemented** as staff-maintained records |
| Practice admin | Person type "Practice Admin", listed with practice managers on the administrators screen | **Implemented** as a person type. How an account is created as this type is **Unknown / Requires Confirmation** |
| Site roles `SiteAdmin`, `ProviderAdmin`, `MCOAdmin`, `Provider` | Named on `SiteAdminController` and the authenticated menu | **Implemented** as role strings. How those ASP.NET roles are first inserted is **Unknown** (no seed in `HCUserDB`) |
| Activation mailbox | `SendRegistrationEmail` looks up the Identity user `verification@healthchoice.co.za` and emails activate/decline links | **Implemented** |

Person types seeded by Entity Framework (`Migrations/Configuration.cs`): Practice Admin, Practice Manager, Practitioner, Patient, Dependent, Next of Kin, Account Payer, Referring Doctor. Only Practice Manager, Practitioner, Patient, and Practice Admin are queried by the controllers that were read. The other four types have no screen. **Implemented** seed; usage of the unused types is **Unknown**.

`AccountType` is an integer on `ApplicationUser`. Comments disagree about what the numbers mean. The behaviour that actually runs is in the functional requirements below. A single approved catalogue was not found.

## Shared behaviour

**Culture.** `Global.asax.cs` sets the thread and JSON culture to `en-ZA`. `Web.config` `globalization` is `uiCulture="en"` and `culture="en-GB"`. Which one wins at runtime for dates and numbers is **Unknown / Requires Confirmation**.

**Routes.** One conventional route: `{controller}/{action}/{id}`, default `Home/Index`. No areas are registered beyond `AreaRegistration.RegisterAllAreas()`, and no `Areas` folder was found.

**Errors.** `HandleErrorAttribute` is the only global filter. Many catch blocks return the MVC `Error` view or swallow the exception (practice registration email send). There is no custom errors section in `Web.config`.

---

## FR-001 — Public pages

**Who:** Anyone.

**Trigger:** Open Home, About, Terms, FAQ, or Contact.

**Implemented behaviour:** `HomeController` returns the matching view. About, Terms, FAQ, and Contact set `ViewBag.Message` and do not load data. The anonymous menu shows Home, About, Terms, and Contact. FAQ is not in the menu. `Home/Index` is also the post-login landing page unless the user is in role `SiteAdmin`.

**Signed-in home message:** If `EmailConfirmed` is false, the page says a confirmation email was sent and offers a resend. If `AccountType` is 4, the page looks for a role whose `ToString()` equals `"Provider"`. That comparison is against an Identity role object, not the role name. When it does not match, the page shows a message that validation may take 48 hours and that a contract must be initialled and faxed to (086) 730-3236 or emailed to `contracts@healthchoice.co.za`, with phone text `0860blablabla`. **Implemented.** Whether that message is still approved copy is **Unknown / Requires Confirmation**.

**Evidence:** `HomeController.cs`, `Views/Shared/_Layout.cshtml`.

## FR-002 — Register a practice

**Who:** Anonymous visitor. The public Register link targets `Account/RegisterPractice`, not `Account/Register`.

**Trigger:** Submit the practice registration form.

**Inputs:** Practice name, trading name, 7-digit practice number (label "BHF Practice Number / PCNS"), affiliation number (label "SASP Number", length 7–10), VAT number, company registration number, partner initiative (`NetworkAffiliation`), address, city, postal code, primary phone, primary email, manager title, names, gender, South African ID, cellphone, email, password, terms checkbox. `AccountType` is on the view model. The GET action sets it to 4 before showing the form.

**Implemented behaviour:**

1. Terms must be accepted. The error text is "Please accept our terms and conditions before proceeding".
2. Reject when an Identity user already has that email.
3. Reject when a person contact of type description `email` already has that value.
4. Reject when `PracticeNumber` already exists.
5. Reject when trimmed `PracticeName` already exists.
6. Create the Identity user with the submitted password and `AccountType` from the form.
7. In one database transaction, insert `Practice` with `ActiveIndic = false` and `ValidatedDate` left unset, a `Person` linked by `LinkedLogin`, a `PracticePerson` of type "Practice Manager" with `ActiveIndic = true`, and work email plus cellphone contacts with `IsValidated = false`.
8. Practice manager `DateofBirth` is stored as 1 January 1900. Middle name is stored as empty.
9. Send a confirmation email to the user and a registration email to `verification@healthchoice.co.za`. If sending throws, the exception is swallowed and the user still goes to `Thankyou`.
10. If practice creation throws after the Identity user was created, the Identity user is deleted.

**Validation (model):** Practice number must match `^\d{7}$`. Affiliation number is required. Password rules are in FR-010.

**Error:** Duplicate and terms failures redisplay the form. A missing activation user (`verification@healthchoice.co.za`) throws from `SendRegistrationEmail` and is then swallowed by the caller.

**Evidence:** `AccountController.RegisterPractice`, `Models/practice.cs`, `Views/Shared/_LoginPartial.cshtml`.

## FR-003 — Confirm email

**Who:** The person who received the confirmation link. The action is anonymous.

**Trigger:** `Account/ConfirmEmail` with `userId` and `code`.

**Implemented behaviour:**

1. Missing `userId` or `code`, unknown user, missing person, or missing email contact returns `Error`.
2. The person's email contact (`ContactType.Description == "email"`) is marked validated with `DateValidated` now.
3. The latest `PracticePerson` whose practice has `ActiveIndic == false` is loaded. If `AccountType` is 7, that practice's `ActiveIndic` is set to true.
4. `UserManager.ConfirmEmailAsync` runs. Success shows `ConfirmEmail`, failure shows `Error`.

The contact-type query uses the lowercase description `email`. The EF seed stores `Email`. Match depends on database collation. **Requires Confirmation.**

Setting `ActiveIndic` is not the same flag login checks. Login checks `ValidatedDate`. See FR-004 and FR-005.

**Evidence:** `AccountController.ConfirmEmail`, `Migrations/Configuration.cs`.

## FR-004 — Activate or decline a practice

**Who:** Whoever opens the link. The action is anonymous and has no token of its own. The link is emailed to the Identity user `verification@healthchoice.co.za`.

**Trigger:** `Account/ConfirmAccount` with `practiceId`, `userId`, and `active`.

**Implemented behaviour:**

1. Unknown user, unknown practice, missing person, or missing email contact returns `Error`.
2. `active` true sets `Practice.ValidatedDate` to now. `active` false clears `ValidatedDate`, saves, and shows `DenyAccount`.
3. When `AccountType` is 7 and the practice was activated, `SendValidationConfirmationEmail` tells the user the practice is activated and to confirm email before login.
4. The email subject built for the admin is "Healthchoice - Activate Practive Account" (spelling as in source).

**Evidence:** `AccountController.SendRegistrationEmail`, `ConfirmAccount`, `SendValidationConfirmationEmail`.

## FR-005 — Sign in

**Who:** A person with an Identity account.

**Trigger:** POST `Account/Login` with email, password, and optional remember-me.

**Implemented behaviour:**

1. Invalid model redisplays the form.
2. `PasswordSignInAsync` is called with `shouldLockout: false`. Failed passwords do not increment the lockout counter, even though the user manager is configured for 5 failures and a 5-minute lock. **Implemented.**
3. On success, the user is loaded again. `AccountType` 1 sets `Session["userLevel"]` to `practitioner`. `AccountType` 7 sets it to `practiceadmin`. Other values leave the session value unset.
4. Login then requires a `Person` whose `LinkedLogin` is the user id. No person returns `Error`.
5. Login requires at least one `PracticePerson` with `ActiveIndic == true`. None returns `Error`.
6. The first such membership (ordered by practice id) is used. If that practice's `ValidatedDate` is null, the form shows "Practice has not been activated" and does not sign the user through to a landing page. The sign-in cookie may already have been issued by `PasswordSignInAsync` before this check. **Implemented.** Whether the cookie should be cleared is **Unknown / Requires Confirmation**.
7. `Session["PracticeId"]` is set to that practice.
8. Role `SiteAdmin` redirects to `SiteAdmin/Index`. Everyone else redirects to `Home/Index`.
9. `RequiresVerification` goes to `SendCode`. `LockedOut` shows `Lockout`. Other failures show "Invalid login attempt."

The menu reads `Session["userLevel"]`. `practiceadmin` sees Practitioners, Patients, and Administrators. Any other value sees Patients only. A null session value throws when the layout calls `.ToString()`. **Implemented.**

**Evidence:** `AccountController.Login`, `Views/Shared/_LoginPartial.cshtml`, `IdentityConfig.cs`.

## FR-006 — Invite and register a practitioner

**Who:** A signed-in user who already has `Session["PracticeId"]` for the invite. The invite and registration actions do not carry `[Authorize]`. Registration is `[AllowAnonymous]`.

**Trigger:** POST `People/InvitePractitioner`, then the emailed link `Account/RegisterPractitioner?refNo={PracticeInviteId}`.

**Implemented behaviour:**

1. Invite stores a new `PracticeInvite` (practice, name, surname, email, `DateSent`, new GUID) only if SMTP send succeeds.
2. The email is from `info@healthchoice.co.za`, subject "{PracticeName} has invited you", and links to practitioner registration.
3. GET registration loads the invite. Unknown `refNo` returns `Error`. The form is prefilled with invite name, surname, and email.
4. POST requires the terms checkbox. South African ID type (`IDTypeId` 1) requires a 13-digit identity number.
5. Reject when `PersonField` `prov_hpcsa_no` already has that HPCSA number, when email contact type id 1 already has that email, or when `Person.IdentityNumber` already exists.
6. Create the Identity user, the person, a `PracticePerson` of type "Practitioner" with `ActiveIndic = true`, contacts, and person fields for discipline, sub-discipline, and HPCSA number.
7. Discipline lists come from the external API, filtered by lookup type `alloweddisciplines`. See FR-013.

**Error:** Failed mail does not save the invite and shows "Could not send email". Missing practice id shows "Cannot determine practice details".

**Evidence:** `PeopleController.InvitePractitioner`, `AccountController.RegisterPractitioner`.

## FR-007 — Maintain people in the practice

**Who:** The menu shows the links only after login, and only the practice-admin session level sees practitioners and administrators. The controller actions themselves are not marked `[Authorize]`. They use `Session["PracticeId"]`.

**Trigger:** `People/IndexPractitioner`, `IndexPatient`, `IndexAdminPerson`, and the matching edit posts.

**Implemented behaviour:**

- Practitioner list: active `PracticePerson` rows of type "Practitioner" for the session practice.
- Patient list: type "Patient".
- Administrator list: types "Practice Manager" or "Practice Admin".
- Edit patient stores medical scheme and scheme number on `PersonField` (`MedicalScheme`, `SchemeNumber`). Scheme dropdown uses lookup type `medschemes`.
- Edit practitioner stores discipline, sub-discipline, and HPCSA number on `PersonField`.
- Titles for these screens use lookup type `titleid`. ID type dropdown is filtered to the text "South African ID".
- If `PracticeId` is missing, `Index` returns an empty view. Invite returns HTTP 404.

Create-patient and create-admin as separate registration flows were not found beyond the edit actions. **Unknown** whether patients are created only through `EditPatient` insert paths.

**Evidence:** `PeopleController`, `Views/Shared/_LoginPartial.cshtml`.

## FR-008 — Site administration of users

**Who:** Class `[Authorize(Roles = "SiteAdmin,Provider,MCOAdmin,ProviderAdmin")]` on `SiteAdminController`.

**Trigger:** User list, create, edit, delete, and "Add a Support User".

**Implemented behaviour:**

- Index, details, edit, and delete use the Identity `ApplicationUser` store (`HCUserDB`).
- Edit can add and remove roles through `Common/AddUserRole` and `Common/DeleteUserRole`. Role names come from `HCDB.user_role.user_role_value`.
- GET `Register` builds the user-type list. If the current user's `AccountType` is 1, every user type is offered (comment in code: "Healthchoice User"). Otherwise the list is user types linked through `user_role_by_role_permission` for that account type.
- POST `Register` is `[AllowAnonymous]` while the class is `[Authorize]`. A successful post creates a user and adds the role selected by `user_role_key`. **Implemented.** Whether anonymous POST was intentional is **Unknown / Requires Confirmation**.
- The authenticated menu shows "User List" and "Add a Support User" for roles SiteAdmin, ProviderAdmin, MCOAdmin, and Provider. The same menu also shows for every other signed-in user, with the admin dropdown commented out, but the Home link still points at `SiteAdmin/Index`. **Implemented.**

`UserTypeController` and `UserRoleController` are CRUD screens restricted to role `SiteAdmin`.

**Evidence:** `SiteAdminController`, `CommonController`, `UserTypeController`, `UserRoleController`, `_Layout.cshtml`.

## FR-009 — Network affiliations

**Who:** No `[Authorize]` on `NetworkAffiliationsController`.

**Trigger:** Index, details, create, edit, delete.

**Implemented behaviour:** CRUD on `NetworkAffiliation` (`NetworkAffiliationId`, `NetworkName`). Practice registration requires a partner initiative, which is this foreign key. `HCLocalService.GetNetworkList` fills that dropdown from the table. No seed of network names was found in the repository.

**Evidence:** `NetworkAffiliationsController.cs`, `HCLocalService.GetNetworkList`.

## FR-010 — Passwords, lockout, and two-factor

**Who:** The account holder, plus anonymous forgot-password.

**Implemented behaviour:**

- Password rules: length at least 6, one digit, one lowercase, one uppercase, one non-letter. User names may contain non-alphanumeric characters. Email must be unique.
- Lockout is enabled by default: 5 failures, 5 minutes. Login does not count failures (`shouldLockout: false`).
- Forgot password sends a reset link only when the user exists and the email is confirmed. Otherwise it still shows the confirmation page.
- Two-factor cookie support is configured (5-minute sign-in cookie). SMS send is `Task.FromResult(0)` and does not deliver a message. Email can send a security code.
- `ManageController` is `[Authorize]` and still contains the ASP.NET Identity template actions: change password, set password, phone, two-factor, external logins. The account menu does not link to Manage. The link is commented out.

**Evidence:** `IdentityConfig.cs`, `AccountController` forgot/reset actions, `ManageController.cs`, `_LoginPartial.cshtml`.

## FR-011 — External login

**Who:** Anonymous, via `Account/ExternalLogin`.

**Implemented behaviour:** Facebook authentication is registered with an application id, secret, and `email` scope. The access token is added as claim `FacebookAccessToken`. Microsoft, Twitter, and Google registrations are commented out. The external-login confirmation view includes an account-type dropdown.

The Facebook secret is in source. It is not copied into this PRD.

**Evidence:** `Startup.Auth.cs`, `AccountController` external login actions.

## FR-012 — Legacy provider screens

**Who:** `ProviderController` and `ProviderRegistrationController` have no class-level `[Authorize]`.

**Trigger:** `Provider/Index`, `Create`, `CreateShort`, `Edit`, `EditShort`, `TermsAndConditions`, and the registration controller's create.

**Implemented behaviour:** These actions still create and edit `provider` rows and, in the short-create path, call `AddUserRole` with the literal role keys `"3"` and `"7"` (comments say Provider and ProviderAdmin). `Account/Register` used to redirect account type 4 to `Provider/CreateShort`. That redirect is commented out (comment date 2019-08-22) and registration now returns `Home/Index` after sending confirmation email. `Home/AccountRedirect` still redirects account type 4 to `Provider/Edit` using `user_link.entity_key`. Nothing in the live menu calls `AccountRedirect`. The menu item is commented out.

**Evidence:** `ProviderController`, `ProviderRegistrationController`, `AccountController.Register`, `HomeController.AccountRedirect`.

## FR-013 — Discipline list from the external API

**Who:** Any screen that calls `HCLocalService.GetDisciplines` (provider screens, practitioner registration, people).

**Trigger:** Building the discipline dropdown.

**Implemented behaviour:**

1. Read lookup type `alloweddisciplines` from `HCDB.lookup_table`.
2. GET disciplines from the external API (`HCWebService`, mode `WEB`).
3. Keep only discipline codes present in that lookup.

The lookup type `alloweddisciplines` is not in `Script.LookupTableData.sql`. If the live database has no rows, the dropdown is only "-Please select-". **Requires Confirmation** that the rows exist in Azure SQL.

Sub-disciplines are loaded from the same API by discipline code. `AccountController` preloads sub-disciplines for discipline code 72.

**Evidence:** `HCLocalService.GetDisciplines`, `HCWebService`.

## FR-014 — Process model present only as database seed

**Who:** Nobody in the MVC application. No controller references `process`, `process_step`, `auth_main`, or `patient`.

**Documented in SQL seed:**

| `proc_type_id` | Name | Description |
|---|---|---|
| 1 | `provider_registration_process` | Provider registration process |
| 2 | `patient_registration_process` | Patient registration process |
| 3 | `chronic_medicine_auth` | Chronic Medicine Authorisation Process |
| 4 | `provider_registration_process_non_network` | Provider registration process non-network |

Steps for type 1 run from provider details through address, practice, unit, banking, business details, send registration, and complete. Type 4 is the same without banking. Type 2 is patient details, funder, and address. Type 3 is auth details, diagnosis, product, and send to scheme.

Provenance: **Implemented** as post-deploy SQL. **Not implemented** as a website workflow in the C# that was read.

## Integrations

| System | Direction | Trigger | Data | Auth | Failure |
|---|---|---|---|---|---|
| Azure SQL `HCDB` and `HCUserDB` on `hcdev1.database.windows.net` | App reads and writes | Every screen that uses EF or Identity | Practices, people, lookups, users | SQL user in `Web.config`. Value not repeated here | Exceptions surface as MVC errors or, on registration email failure, are swallowed |
| SendGrid SMTP `smtp.sendgrid.net:587` | Outbound mail | Registration, confirmation, activation, invite, password reset | HTML email from `info@healthchoice.co.za` | SMTP credentials in `Web.config`. Not repeated here | Invite rolls back. Practice registration hides the error and still shows Thankyou |
| Azure App Service `lcservices20190802115129.azurewebsites.net/api/` | Outbound HTTPS | Discipline and sub-discipline dropdowns | Discipline code and description | HTTP Basic. Credentials in `HCWebService.cs`. Not repeated here | Not caught in `GetDisciplines`. A failed call fails the page |
| Facebook | Inbound login | External login button | Email scope, access token claim | App id and secret in `Startup.Auth.cs` | Identity external-login failure views |
| `verification@healthchoice.co.za` | Identity user that receives activation mail | Practice registration | Practice name, number, phones, emails, activate and decline links | Must already exist as a user | `FindByEmailAsync` returning null throws when the mail is sent |

Local API base `https://localhost:44300/api/` exists behind `ServiceMode.LOCAL`. The compiled default in `HCWebService` is `ServiceMode.WEB`.

## Important constraints

- Framework target is .NET Framework 4.5, MVC 5, EF 6. Building the SQL projects needs SQL Server Data Tools.
- `EnvironmentSetting` is `TEST`. The test banner inside confirmation emails is commented out, so test mail is not labelled.
- Session is `InProc`. The session provider's connection string name is `DefaultConnection`, and that connection string is commented out. The provider is unused while mode stays `InProc`. A web-farm comment in `Web.config` says to switch to custom session state. No such switch is configured.
- Practice number uniqueness and name uniqueness are application checks, not attributes that declare a unique index. Whether the database enforces them is **Unknown**.
- South African ID is checked for length 13 when the type id is 1. Check digit validation was not found.

## Assumptions not treated as requirements

- That account type 4 means provider and account type 7 means practice manager. The code uses both numbers in different places. See `business-rules.md`.
- That the SSDT clinical schema is still a product requirement. It is in the repo. The website does not use it.
- That Facebook login is offered to practices. It is configured. The public menu that was read shows Register and Log in only.
