# Business rules — SHOU-1 (The Shoulder Practice)

Evidence date: 2026-10-04.

Source: `git@bitbucket.org:overdrive/shou-1.git`, `main` at `30a710c`.

Rules below are taken from code, SQL script names, and `tools/password-migrate/README.md`. Where the code behaves a certain way and no comment states it as policy, the rule is marked **Inferred**. Where a comment or the README states it, it is **Documented**. Where the executable check exists, it is **Implemented**.

This file does not add rules that the practice might want. It records rules the repository enforces.

## Identity

| ID | Rule | Provenance |
|---|---|---|
| BR-001 | The public site title is "The Shoulder Practice \| Cape Town \| Orthopaedic Surgeon". The meta description names Dr Christelle van der Watt, shoulder surgery, Milnerton and Claremont. | **Implemented** in `core_custom/SHOU-1/Core.master` |
| BR-002 | Outbound mail From is `no-reply@theshoulderpractice.net`. The `website` appSetting value in the committed file is "The Shoulder Practice SHOU-1 DEV". | **Implemented** in `Web.config`. The "DEV" suffix means this file is a development label. Production wording is **Unknown** |
| BR-003 | Practice-only behaviour is gated on setup code `SHOU-1`. | **Implemented** |
| BR-004 | A medical practice row is looked up by the name "The Shoulder Practice". | **Implemented** in `xSurfaceCustomMethod.cs`. **Inferred** that this string must match the database row or the lookup returns nothing |

## Authentication and passwords

| ID | Rule | Provenance |
|---|---|---|
| BR-010 | Password writes and user status writes go to the central catalog (`pal_User`, `pal_UserPassword`). The tenant catalog is used for reads through `pal_vUserShared` and `pal_vUserPasswordShared`. | **Documented** in `tools/password-migrate/README.md` and `.env.example`. **Implemented** in `PasswordService` and `ConfigResolver` |
| BR-011 | Default central catalog name is `Framework_EVOL-1`. Default tenant catalog in the example string is `Framework_SHOU-1`. Optional overrides are `DB_CENTRAL_CATALOG` and `DB_TENANT_CATALOG`. | **Documented** in `.env.example`. **Implemented** in `ConfigResolver` |
| BR-012 | New passwords are stored as BCrypt (`hashAlgorithm = 'BCrypt'`). `SetPassword` clears the legacy `pal_User.password` column. | **Implemented** in `PasswordService.SetPassword` |
| BR-013 | Login accepts a BCrypt hash first. If that does not match, it accepts the legacy cipher. | **Implemented** in `VerifyUserPassword`. **Inferred** that legacy passwords remain valid until migrated or changed |
| BR-014 | BCrypt work factor is `BCRYPT_WORK_FACTOR` when it parses as an integer from 4 to 31. Otherwise it is 12. | **Documented** in `.env.example`. **Implemented** in `PasswordService.GetWorkFactor` |
| BR-015 | An empty password is rejected when hashing. | **Implemented** (`Hash` throws `ArgumentException`) |
| BR-016 | Minimum length is `PASSWORD_MIN_LENGTH`, clamped to 8–128. If unset or not an integer, the minimum is 12. | **Documented** and **Implemented** in `PasswordPolicy.GetMinLength` |
| BR-017 | A password must contain an upper-case letter, a lower-case letter, a digit, and a character that is not a letter or digit. | **Implemented** in `PasswordPolicy.Validate`. The user-facing sentence is `GetRequirementsMessage` |
| BR-018 | A password equal to a fixed denylist is rejected. The denylist includes common passwords and the string `ev0luti0ns0ftware`. | **Implemented** in `PasswordPolicy`. The full denylist is in that file and is not copied here |
| BR-019 | If an email is supplied and its local part is at least 3 characters, the password must not contain that local part (case-insensitive). | **Implemented** |
| BR-020 | `PasswordPolicy.Enforce` throws `InvalidOperationException` with the joined messages. Registration, reset, and admin user screens call this path. | **Implemented** |
| BR-021 | The password-migration method may call `SetPassword` with policy enforcement off. | **Implemented** in `MigrateShou1LegacyPasswords`. **Documented** as an operator tool, not as an end-user exception |
| BR-022 | Login candidates must have `isActive = 1`, `isDeactivated = 0`, and a matching email. | **Implemented** in `LoadActiveUsersByEmail` |
| BR-023 | A user is locked when `verifyCodeWrongAttempts` is greater than or equal to `LOGIN_MAX_ATTEMPTS`. Default maximum is 5. A configured value of 0 or below disables lockout. | **Documented** and **Implemented** |
| BR-024 | A locked user is skipped during password validation. A failed attempt increments `verifyCodeWrongAttempts` on the central user. Success sets the counter to 0 and persists the user. | **Implemented** |
| BR-025 | Wrong password, locked user, deactivated user, and inactive user all surface as "Your email or password is incorrect." on `Login.aspx` and `Admin_SignIn.aspx`. | **Implemented**. A distinct lockout or deactivation message is **not** implemented. Whether that is intentional is **Unknown / Requires Confirmation** |
| BR-026 | A user must belong to the current tenant `customerCode`, except an `EVOL-1` user whose type is above Power User and is not Custom User. | **Implemented** in `MatchesLoginTenant` |
| BR-027 | User types are Website User 1, Power User 2, Super User 3, Admin User 4, Custom User 5. | **Implemented** in `pNums.UserType` |
| BR-028 | Registration creates a Website User. | **Implemented** in `Register.aspx.cs` |
| BR-029 | If `securePath` is not empty, a new registrant is saved inactive and must open the confirmation link before `ConfirmRegistrationUser` sets `isActive = 1`. Confirmation does not run when `isDeactivated` is set. | **Implemented** |
| BR-030 | The registration terms checkbox must be checked before the Register button is enabled. | **Implemented** in `Register.aspx.cs` |
| BR-031 | A duplicate email on the tenant is rejected at registration and in admin user maintenance. | **Implemented** |
| BR-032 | Deactivation sets `isDeactivated = 1` and `dateDeactivated` on the central user. Reactivation clears deactivation, failed attempts, `isReset`, and `resetCode`. | **Implemented** in `SetUserDeactivated` |
| BR-033 | An admin cannot deactivate the account they are signed in with. | **Implemented** in `controls/admin/users.ascx.cs` |
| BR-034 | Forgot-password runs only for an active, non-deactivated user on the tenant. Unknown email: "Email address not found." Mail queued: "Success: We have sent you further instructions." | **Implemented** |
| BR-035 | The reset code is 6 characters. It expires 12 hours after `dateUpdated`. A deactivated or inactive user cannot complete reset. Success clears reset flags and login attempts. | **Implemented** in `Reset.aspx.cs` |
| BR-036 | A non-registration auth link is accepted when the auth date is today. | **Implemented** in `Core.master.cs` |
| BR-037 | Google sign-in on the admin page does not register a new person. The email must already be allowed to log in. | **Implemented** in `Admin_SignIn.aspx.cs` |
| BR-038 | On SHOU-1, a Website User who is not the month-end user is redirected to `{securePath}/pages/patient~info` with an auth query. | **Implemented**. Comment in `Login.aspx.cs`: "to do we need to handle better" — that note is not a second rule |
| BR-039 | On SHOU-1, if appSetting `MonthEndUser` equals the login email, the month-end checklist modal is shown instead of the patient redirect. | **Implemented**. The email and the checklist contents are **Unknown** |

## Permissions

| ID | Rule | Provenance |
|---|---|---|
| BR-050 | `ValidatePermission` allows the page when the user is an Admin User and the module id for that path is contained in `oUser.modules`. | **Implemented** in `handler.cs` |
| BR-051 | `ValidatePermission` allows every page when `oUser.recId` is `-1`. | **Implemented**. **Inferred** that `-1` is a break-glass identity. No comment in the method was required for this reading; confirm before treating `-1` as a supported operator account |
| BR-052 | An unrestricted canvas is available to any user who can open it. A restricted canvas requires the user type in `canvas.userTypes`, or a Custom User listed in `canvas.customUsers`. | **Implemented** in `validateCanvas` |
| BR-053 | `validateCanvas` contains a branch for tenants `STUD-1` and `DISA-1` that lets an EVOL admin through. That branch is not the SHOU-1 rule. | **Implemented** for those codes only |
| BR-054 | Surface add, edit, view, remove, and clone compare `MainGridOptions` access types with `userType`. Custom User uses `mimicUserType`. | **Implemented** in the surface grid, form, and view controls |
| BR-055 | A Website User on a surface that links to a user sees only rows linked to themselves. | **Implemented** in `surface_grid.ascx.cs` |
| BR-056 | The user-link column on a surface grid is visible to Admin User. | **Implemented** |
| BR-057 | Field and group visibility compare `field.accessLevel` with the user type (mimic type for Custom User). | **Implemented** |
| BR-058 | On SHOU-1, a user below Power User gets the wizard-only surface form (full save buttons hidden). Power User and above can jump to the first non-hidden wizard tab when editing. | **Implemented** in `surface_form.ascx.cs`. **Inferred** as a data-entry restriction for website users and anyone mapped below Power User |
| BR-059 | Credit notes in the enquiry module require `oUserAccess.allowCredit` for the current user. | **Implemented** in `AllowCreditNotes` |
| BR-060 | Admin sign-in sends an Admin User of this tenant, or an `EVOL-1` admin, to `Admin.aspx`. Other signed-in users go to `/home`. | **Implemented** |

## Calendar

| ID | Rule | Provenance |
|---|---|---|
| BR-070 | Calendars offered to the user come from `sp_CalendarAccess` for the current user id. | **Implemented** call. The procedure's own rules are **Unknown** (body not in the repo) |
| BR-071 | The fetch window starts at the first day of the week of the visible start, and ends at the first day of the week of the last visible day plus 7 days. First day of week comes from the calendar control. | **Implemented** in `StartOfWeek` and `BindCalendar` |
| BR-072 | An event is included when it overlaps that window (`start` before the window end and `end` after the window start). | **Implemented** |
| BR-073 | A recurring master outside the window is included when the series is still active (UNTIL on or after the window start, or an open-ended series). | **Implemented** |
| BR-074 | Patient search on the calendar does not apply the date window. | **Implemented** |
| BR-075 | When billing is finalised, calendar events for that patient on the billing calendar that are not yet billed and whose date is on or before the billing date are marked `isBilled`. | **Implemented** from `enquiry.ascx.cs` `FinaliseBilling` |

## Billing, tariffs, and debtors

| ID | Rule | Provenance |
|---|---|---|
| BR-080 | SHOU-1 account header fields are mapped from the patient surface, with title-case normalisation. | **Implemented** in `xDebtors.SetAccountItem` when setup code is `SHOU-1` |
| BR-081 | SHOU-1 billing lines are built from examination booking procedures when the account has a `surfaceItemId`. | **Implemented** in `SetAccountBillingLines` |
| BR-082 | For SHOU-1, if the scheme plan code is empty, or the plan is not `WCA` and the scheme name does not contain `FEDHEALTH`, the tariff code is `DISC B`. | **Documented** by the 2018-04-13 comment in `xDebtors.cs`. **Implemented** there and as a dropdown default in `enquiry.ascx.cs` when the item exists |
| BR-083 | Modifier procedure types (`codeType == 2`) are skipped when those booking lines are built. A tendon value of `REMOVE` skips the line. | **Implemented** in `xDebtors` |
| BR-084 | Liability follows the tariff `liableIndicator`. `0` means medical aid is liable; any other value means the patient is liable. | **Implemented**. **Inferred** wording of "medical aid" versus "patient" from the indicator comment in that method; confirm the indicator legend if a statement disagrees |
| BR-085 | Emergency modifier `0011` is calculated as `(minutes / 30) * (12 * 16.998)`. Obese modifier `0018` adds 50 percent via the procedure modifier. Assistant modifiers `0008` and `0009` use percentages from the procedure table. | **Implemented** in `AddModifierLines` |
| BR-086 | Tariff `0083` (ultrasound equipment) does not add modifier lines, and Set Modifiers is disabled, so posting produces one line. | **Documented** in commit `1318028` (2026-02-25). **Implemented** as that commit's resulting code |
| BR-087 | A macro procedure (`codeType` 3) expands into multiple billing lines on post. | **Implemented** in `enquiry.ascx.cs` `PostBillingLine` |
| BR-088 | Finalise requires the session billing lines, assigns one invoice number plus a sequence, and saves the lines. | **Implemented** in `FinaliseBilling` |
| BR-089 | A locked billing line is shown with inputs disabled. Unlock sets `isLocked` false. | **Implemented** in `PopulateEditLineValues` |
| BR-090 | Statement lines exclude procedure types `PA` and `CA` from the running balance. | **Implemented** in `BuildAccountLines` |
| BR-091 | Transactions after `statementDate` are excluded unless the caller is in date-range mode. | **Implemented** |
| BR-092 | When hidden lines are not requested, lines with `isVisible` false are excluded. | **Implemented** |
| BR-093 | `fromZero` drops history before the last zero or negative running balance. | **Implemented** |
| BR-094 | A claim-sent date is marked with `*` on the statement line. | **Implemented** |
| BR-095 | Modifier display on statements has explicit handling for codes `0008`, `0009`, `0011`, and `0018`. | **Implemented** |
| BR-096 | Emailing a statement uses a different subject and body when the balance is zero, and checks that medical-scheme or patient email addresses are present. | **Implemented** in `EmailAccountStatement` |
| BR-097 | Debtors age report filter `1` excludes zero balances (the default). Filter `2` is debit balances only. | **Implemented** in `debtorsReports.ascx.cs` |
| BR-098 | An async postback failure on debtor reports shows an inline failure message rather than redirecting. | **Implemented** in `HandleReportException` |
| BR-099 | Unprocessed remittance advices are opened from the enquiry screen (ERA link buttons), not from a separate SHOU-1 remittance report page. | **Implemented** in `enquiry.ascx` |

## Clinical documents and schemes

| ID | Rule | Provenance |
|---|---|---|
| BR-110 | `MedicalSchemeDisplay` maps a stored scheme code to the scheme name for display. It does not write the name back. | **Implemented** |
| BR-111 | Display values `0`, `-1`, `Select`, and `select..` become an empty string. An unknown code is shown unchanged. | **Implemented** |
| BR-112 | SHOU-1 document generation fills physiotherapist letter fields and can attach `Physio Referral - Shoulder Rehabilitation Protocols.pdf` when a physiotherapist letter is being sent and that file exists under the site `upload/documents` path. | **Implemented** in `xSurfaceCustomAction.cs` |
| BR-113 | Post-consultation and post-operation letter file names are handled on the SHOU-1 path. | **Implemented** (`PostConsultationLetter`, `PostOperationletter`) |
| BR-114 | The same WCA / FedHealth / empty-plan test used for `DISC B` also appears when scheme names are resolved during SHOU-1 document generation. | **Implemented** in `xSurfaceCustomAction.cs` near the FedHealth check |

## Banners, campaigns, and registration mail

| ID | Rule | Provenance |
|---|---|---|
| BR-120 | A banner is shown when it is active and today is between `startDate` and `endDate`, ordered by `sequence`. | **Implemented** in `handler.ReturnBannerCarousel` |
| BR-121 | Admin banner sequence choices are 0 through 19. | **Implemented** in `controls/admin/banners.ascx.cs` |
| BR-122 | Campaign templates are those with template type 3. | **Implemented** in `campaigns.ascx.cs` |
| BR-123 | If SMS `availableBalance` is below the configured threshold, an alert email is sent to the configured admin. | **Implemented** in `CheckThreshold` |
| BR-124 | Saving a patient on the SHOU-1 surface form can send the template named "patient registration notification". | **Implemented** in the surface form save path. **Inferred** that the template must exist in the database or the send does nothing useful |

## Scheduler

| ID | Rule | Provenance |
|---|---|---|
| BR-130 | With no arguments, `tools/scheduler` runs the Palette batch (campaigns, loyalty email, transaction lock, MediSwitch delayed responses, surface import) across tenant catalogs. | **Implemented** in `Program.cs`. Loyalty email is also gated by appSettings `loyaltyEmail` (committed value `0`, so off) and `loyaltyEmailHour` (committed value `20`) |
| BR-131 | A first argument containing `googlesheetimport` runs Google Sheet import instead of the default batch. | **Implemented** |
| BR-132 | The connection string may contain the literal `{catalog}`, which the scheduler replaces per tenant. | **Documented** in `.env.example`. **Implemented** in the scheduler |
| BR-133 | `SchedulerTool` exits when started with no arguments. It is not the unattended batch. | **Implemented** in `SchedulerTool/frmScheduler.cs` |

## Rules that are not in source

Stored procedures named by the app (`sp_CalendarAccess`, invoice numbering, debtor age report builders) enforce additional rules that this repository does not contain. Those rules are **Unknown**.

Which modules a given role can open is data (`oUser.modules`, `oModule`). It is **Unknown** until read from `Framework_SHOU-1`.

## Conflicts inside the tree

| Topic | What disagrees |
|---|---|
| MediSwitch host | Active appSettings are the QA gateway. A commented block contains the live gateway. The file does not say which one production uses |
| Culture | `defaultCulture` is `en-ZA`. `globalization culture` is `en-US` |
| Framework target | `compilation targetFramework` is `4.8`. `httpRuntime targetFramework` is `4.6` |
| Service security | The WCF binding uses security mode None, while the web sign-in path hashes passwords and locks accounts |
| Legacy passwords | Policy applies to new passwords. Login still accepts the legacy cipher |

These are recorded so they are not silently treated as one rule.
