Skip to content

Example: application business rules

Doc gen writes docs/application/business-rules.md as the single catalog of every business rule reachable from a web front end's execution paths, whichever layer enforces it.

What reviewers should check

  • Every rule heading is an H2 (## BR-APP-001 ...), including procedure and inherited rules, so screen documents can link to a resolving anchor.
  • The rule scope table counts match the number of rule sections actually present below it, and the total equals the sum of the in-scope categories.
  • Each rule records a Reached from value naming at least one screen or workflow; a rule with no reachable entry point belongs in the not-reachable table instead.
  • ID schemes are correct and never renumbered: BR-APP-NNN, BR-SCR-NNN, canonical library IDs, PROC-RULE-NNN, INH-NNN.
  • Library, procedure, and inherited rules are restated in full so a reader does not have to open the library page.
  • The † note box appears immediately before ## Related documentation whenever any statement is marked †.

Example

The document below is a complete, correct business-rules.md for a fictional application.

View rendered example — the same document as a real page, so you can compare rendering as well as source.

---
title: Application business rules
summary: Every business rule reachable from this application's execution paths, with per-rule trigger, enforcement, outcome, origin, and entry point.
status: draft
owner: Claims Intake Team
last_reviewed: 2026-09-09
---

# Application business rules

This page is the complete catalog of business rules that govern this application's behavior. A rule appears here when it is enforced on a code path this application can actually execute — regardless of where the enforcing code lives: the application project itself, a screen code-behind, a local library, a same-repo package, a stored procedure reached through a library's data access path, or a framework base class inherited from a dependency package.

Rules defined in a consumed library but never reached by this application are **out of scope** and are listed only in [Rules not reachable from this application](#rules-not-reachable-from-this-application).

## Rule scope and coverage

Reachability was determined by tracing outward from this application's entry points — screen event handlers, application workflow steps, and application startup/infrastructure code — through every delegation recorded in the run's analysis findings, including inherited base-class behavior.

| Category | Section | Count |
| --- | --- | --- |
| Application-level (cross-cutting) | [Application-level business rules](#application-level-business-rules) | 3 |
| Screen-enforced | [Screen business rules](#screen-business-rules) | 3 |
| Library-defined, in scope | [Library business rules in scope](#library-business-rules-in-scope) | 2 |
| Stored procedure-enforced | [Stored procedure business rules](#stored-procedure-business-rules) | 1 |
| Inherited from framework packages | [Inherited business rules](#inherited-business-rules) | 1 |
| **Total in scope** | | **10** |
| Library-defined, not reachable | [Rules not reachable from this application](#rules-not-reachable-from-this-application) | 2 |

## Rule index

| Rule ID | Statement | Origin | Reached from |
| --- | --- | --- | --- |
| [BR-APP-001](#br-app-001-duplicate-claim-check-precedes-submission) | A new claim report cannot be submitted until a duplicate check has run against the claimant and date of injury | Application | [Record new claim](operations/record-new-claim.md) |
| [BR-APP-002](#br-app-002-search-results-capped-at-250-claims) | A single claim search returns at most 250 claims | Application | [Claim Search](../screens/claim-search.md) |
| [BR-APP-003](#br-app-003-closed-claims-are-read-only) | Closed claims open in read-only mode and cannot be edited from intake | Application | [Claim Search](../screens/claim-search.md) |
| [BR-SCR-001](#br-scr-001-at-least-one-search-criterion-required) | At least one search criterion must be supplied before a search runs | [Claim Search](../screens/claim-search.md) | [Claim Search](../screens/claim-search.md) |
| [BR-SCR-002](#br-scr-002-injury-date-range-must-be-ordered) | The date-of-injury range end must not precede the range start | [Claim Search](../screens/claim-search.md) | [Claim Search](../screens/claim-search.md) |
| [BR-SCR-003](#br-scr-003-claimant-first-name-requires-last-name) | A claimant first name is only accepted alongside a last name | [Claim Search](../screens/claim-search.md) | [Claim Search](../screens/claim-search.md) |
| [VAL-014](#val-014-claim-number-must-be-eight-digits) | A claim number must be exactly eight digits | [SAIF.Framework.Validation](../libraries/saif-framework-validation/overview.md) | [Claim Search](../screens/claim-search.md) |
| [VAL-021](#val-021-date-of-injury-cannot-be-in-the-future) | A date of injury cannot be later than the current date | [SAIF.Framework.Validation](../libraries/saif-framework-validation/overview.md) | [Record new claim](operations/record-new-claim.md) |
| [PROC-RULE-004](#proc-rule-004-claim-numbers-are-issued-sequentially-per-year) | Claim numbers are issued sequentially within the accident year | [`USP_CLAIM_INSERT`](../libraries/saif-claims-intake-serviceproxy/procedures/usp-claim-insert.md) | [Record new claim](operations/record-new-claim.md) |
| [INH-001](#inh-001-authenticated-windows-identity-required-for-every-request) | Every request must carry an authenticated Windows identity | `SAIF.Framework.Web`† | [Claim Search](../screens/claim-search.md) |

## Application-level business rules

Rules composed at the application level or enforced jointly by multiple libraries, with no single owning library.

## BR-APP-001 Duplicate claim check precedes submission

| Field | Value |
| --- | --- |
| **Statement** | A new claim report cannot be submitted until a duplicate check has run against the claimant name and date of injury, and the intake specialist has acknowledged any matches returned. |
| **Trigger** | The intake specialist clicks **Submit claim** on the New Claim screen. |
| **Enforcement** | `ClaimSubmissionCoordinator.Submit` requires a non-null duplicate check token issued by `DuplicateClaimChecker.Check`; when the token is missing or stale the submission is refused and the duplicate panel is displayed. |
| **Outcome** | Submission is blocked, the candidate duplicate claims are listed, and the specialist must either acknowledge that none match or open the matching claim. |
| **Origin** | Application |
| **Enforcing libraries** | [SAIF.Claims.Intake.Capture](../libraries/saif-claims-intake-capture/overview.md), [SAIF.Claims.Intake.Search](../libraries/saif-claims-intake-search/overview.md) |
| **Reached from** | [Record new claim](operations/record-new-claim.md), [New Claim](../screens/claim-new.md) |
| **Source** | src/SAIF.Claims.Intake/Claims/ClaimSubmissionCoordinator.vb |

---

## BR-APP-002 Search results capped at 250 claims

| Field | Value |
| --- | --- |
| **Statement** | A single claim search returns at most 250 claims; when the cap is reached the user is told the result set is incomplete and asked to narrow the criteria. |
| **Trigger** | Any claim search executed from the Claim Search screen, including paging and sorting reposts. |
| **Enforcement** | `ClaimSearchService.SearchClaims` sets `MaxRows` to 250 on the web service request and sets `ResultTruncated` when the service returns the full 250 rows; `ClaimSearch.aspx.vb` shows `lblResultCap` in response. |
| **Outcome** | The grid binds 250 rows and a warning message instructs the user to add criteria. No error is raised. |
| **Origin** | Application |
| **Enforcing libraries** | [SAIF.Claims.Intake.Search](../libraries/saif-claims-intake-search/overview.md), [SAIF.Claims.Intake.ServiceProxy](../libraries/saif-claims-intake-serviceproxy/overview.md) |
| **Reached from** | [Claim Search](../screens/claim-search.md), [Claimant Search](../screens/claimant-search.md) |
| **Source** | src/SAIF.Claims.Intake/Claims/ClaimSearch.aspx.vb |

---

## BR-APP-003 Closed claims are read-only

| Field | Value |
| --- | --- |
| **Statement** | A claim whose status is `Closed` opens in read-only mode from intake; no intake user may edit claim or claimant data on a closed claim. |
| **Trigger** | The user opens a claim whose status is `Closed` from a search result row or a claim number link. |
| **Enforcement** | `ClaimSearch.aspx.vb` appends `mode=readonly` to the Claim Detail query string for closed rows, and `ClaimDetail.aspx.vb` disables the edit panel whenever the claim status is `Closed`, independent of the query string. |
| **Outcome** | Claim Detail renders with all input controls disabled and the Save command hidden. |
| **Origin** | Application |
| **Enforcing libraries** | [SAIF.Claims.Intake.Capture](../libraries/saif-claims-intake-capture/overview.md), [SAIF.Claims.Intake.Claimants](../libraries/saif-claims-intake-claimants/overview.md) |
| **Reached from** | [Claim Search](../screens/claim-search.md), [Claim Detail](../screens/claim-detail.md) |
| **Source** | src/SAIF.Claims.Intake/Claims/ClaimDetail.aspx.vb |

---

## Screen business rules

Rules enforced directly in a screen's markup validators or code-behind event handlers, not delegated to a library.

## BR-SCR-001 At least one search criterion required

| Field | Value |
| --- | --- |
| **Statement** | A claim search requires at least one of claim number, claimant last name, policy number, or date-of-injury range; an empty search is refused. |
| **Trigger** | The user clicks **Search** on the Claim Search screen. |
| **Enforcement** | `btnSearch_Click` calls `HasAnyCriteria`; when it returns `False` the handler sets `lblSearchError` and returns before any service call. |
| **Outcome** | No search runs, the grid keeps its previous contents, and the message "Enter at least one search criterion" is displayed. |
| **Origin** | [Claim Search](../screens/claim-search.md) |
| **Reached from** | [Claim Search](../screens/claim-search.md) |
| **Source** | src/SAIF.Claims.Intake/Claims/ClaimSearch.aspx.vb |

---

## BR-SCR-002 Injury date range must be ordered

| Field | Value |
| --- | --- |
| **Statement** | When both date-of-injury range values are supplied, the end date must not precede the start date. |
| **Trigger** | The user submits a search with values in both `dtpInjuryFrom` and `dtpInjuryTo`. |
| **Enforcement** | The `cvInjuryDateRange` custom validator runs `cvInjuryDateRange_ServerValidate`, which compares the two chooser values and sets `args.IsValid` to `False` when the end date is earlier. |
| **Outcome** | Page validation fails, the validation summary shows "Injury date range is out of order", and no service call is made. |
| **Origin** | [Claim Search](../screens/claim-search.md) |
| **Reached from** | [Claim Search](../screens/claim-search.md) |
| **Source** | src/SAIF.Claims.Intake/Claims/ClaimSearch.aspx.vb |

---

## BR-SCR-003 Claimant first name requires last name

| Field | Value |
| --- | --- |
| **Statement** | A claimant first name is only used as a search criterion when a claimant last name is also supplied. |
| **Trigger** | The user enters a first name but leaves the last name blank and submits the search. |
| **Enforcement** | `BuildSearchCriteria` omits the first name from the criteria object when `txtClaimantLastName` is empty, so the service never receives a first-name-only filter. |
| **Outcome** | The search runs against the remaining criteria and the ignored first name is reported in `lblCriteriaNote`. |
| **Origin** | [Claim Search](../screens/claim-search.md) |
| **Reached from** | [Claim Search](../screens/claim-search.md) |
| **Source** | src/SAIF.Claims.Intake/Claims/ClaimSearch.aspx.vb |

---

## Library business rules in scope

Rules defined in a library this application consumes, on a code path this application executes. Each rule keeps its canonical library rule ID; the library's own `business-rules.md` remains the rule's home page, and the **Origin** field links to it.

## VAL-014 Claim number must be eight digits

| Field | Value |
| --- | --- |
| **Statement** | A claim number is exactly eight numeric digits with no separators; values of any other length or containing non-digit characters are rejected. |
| **Trigger** | Any screen or workflow that supplies a claim number to search, open, or update a claim. |
| **Enforcement** | `ClaimNumberValidator.Validate` applies the pattern `^\d{8}$` and returns a `ValidationResult` carrying the message "Claim number must be 8 digits". |
| **Outcome** | The caller receives an invalid result, the screen displays the message, and no web service call is made. |
| **Origin** | [SAIF.Framework.Validation](../libraries/saif-framework-validation/business-rules.md#val-014-claim-number-must-be-eight-digits) — `ClaimNumberValidator` |
| **Reached from** | [Claim Search](../screens/claim-search.md), [Claim Detail](../screens/claim-detail.md) |
| **Source** | src/SAIF.Framework.Validation/Validators/ClaimNumberValidator.vb |

---

## VAL-021 Date of injury cannot be in the future

| Field | Value |
| --- | --- |
| **Statement** | A date of injury must be on or before the current date; future dates are rejected on both search criteria and new claim capture. |
| **Trigger** | A date of injury is supplied on the New Claim screen or as a search range bound on Claim Search. |
| **Enforcement** | `InjuryDateValidator.Validate` compares the supplied date to `DateTime.Today` and returns an invalid result when it is later. |
| **Outcome** | Validation fails with "Date of injury cannot be in the future" and the submission or search is blocked. |
| **Origin** | [SAIF.Framework.Validation](../libraries/saif-framework-validation/business-rules.md#val-021-date-of-injury-cannot-be-in-the-future) — `InjuryDateValidator` |
| **Reached from** | [Record new claim](operations/record-new-claim.md), [Claim Search](../screens/claim-search.md) |
| **Source** | src/SAIF.Framework.Validation/Validators/InjuryDateValidator.vb |

---

## Stored procedure business rules

Rules enforced inside stored procedures that this application reaches through a consumed library's data access path. The rule lives in the database, not in application or library code, but it governs application behavior and so belongs in this catalog.

## PROC-RULE-004 Claim numbers are issued sequentially per year

| Field | Value |
| --- | --- |
| **Statement** | A newly inserted claim receives the next sequential claim number within the accident year, reserved under a table lock so no two intake sessions can be issued the same number. |
| **Trigger** | A first report of injury is submitted and the claims web service inserts the claim record. |
| **Enforcement** | `USP_CLAIM_INSERT.AllocateClaimNumber` — reads and increments `CLAIM_NUMBER_SEQUENCE` for the accident year inside the insert transaction with an update lock. |
| **Outcome** | The procedure returns the allocated claim number to the service, which returns it to the New Claim screen for display and printing. |
| **Origin** | [SAIF.Claims.Intake.ServiceProxy](../libraries/saif-claims-intake-serviceproxy/business-rules.md#proc-rule-004-claim-numbers-are-issued-sequentially-per-year) — [`USP_CLAIM_INSERT`](../libraries/saif-claims-intake-serviceproxy/procedures/usp-claim-insert.md) |
| **Reached from** | [Record new claim](operations/record-new-claim.md), [New Claim](../screens/claim-new.md) |
| **Tables** | [CLAIM](../libraries/saif-claims-intake-serviceproxy/database-tables.md#claim), [CLAIM_NUMBER_SEQUENCE](../libraries/saif-claims-intake-serviceproxy/database-tables.md#claim_number_sequence) |
| **SQL** | [SQL-CLAIM-INSERT](../libraries/saif-claims-intake-serviceproxy/sql/sql-claim-insert.md) |
| **Source** | CLAIMS.dbo.USP_CLAIM_INSERT |

---

## Inherited business rules

Rules that reach this application through base classes, services, or utility types provided by dependency framework packages. They are enforced by inherited behavior, not by code in this repository. The originating class and library are recorded in each rule's **Origin** field.

## INH-001 Authenticated Windows identity required for every request

| Field | Value |
| --- | --- |
| **Statement** | Every page request must carry an authenticated Windows identity; anonymous requests are rejected before page initialization completes.† |
| **Trigger** | Any request to a page deriving from `SaifWebPage`, which is every screen in Claims Intake. |
| **Enforcement** | `SaifWebPage.OnInit` inspects `User.Identity.IsAuthenticated` and redirects unauthenticated requests to the access denied page before control reaches the derived page's `Page_Load`. |
| **Outcome** | The unauthenticated request is redirected to `AccessDenied.aspx` and the screen's own handlers never run. |
| **Origin** | `SAIF.Framework.Web` — `SaifWebPage` |
| **Reached from** | [Claim Search](../screens/claim-search.md), [New Claim](../screens/claim-new.md), [Claimant Search](../screens/claimant-search.md) |
| **Consuming type** | `ClaimsIntakePageBase` in [SAIF.Claims.Intake.Capture](../libraries/saif-claims-intake-capture/overview.md) |
| **Source** | Retrieved from indexed documentation via Legends CLI |

---

## Rules not reachable from this application

Rules documented in a consumed library that no application execution path reaches. Listed for completeness so a reader can distinguish "not applicable here" from "missed".

| Library | Rule ID | Statement | Reason not reachable |
| --- | --- | --- | --- |
| [SAIF.Framework.Validation](../libraries/saif-framework-validation/overview.md) | [VAL-032](../libraries/saif-framework-validation/business-rules.md#val-032-provider-tax-id-must-be-nine-digits) | A medical provider tax identifier must be exactly nine digits | Claims Intake never captures provider tax identifiers; `ProviderTaxIdValidator` is not instantiated by any application code path |
| [SAIF.Framework.Validation](../libraries/saif-framework-validation/overview.md) | [VAL-037](../libraries/saif-framework-validation/business-rules.md#val-037-benefit-rate-must-be-positive) | A calculated benefit rate must be greater than zero | Benefit calculation happens in claims adjudication, not intake; no intake screen or workflow calls `BenefitRateValidator` |

## Notes

- Every business rule heading — application, screen, library, procedure, or inherited — must be H2 (`## <RULE-ID>`), so every anchor referenced from a screen or operation page resolves. No rule category is exempt: a rule represented only as a row in a shared table has no anchor and cannot be linked.
- ID schemes: `BR-APP-NNN` for application-level rules, `BR-SCR-NNN` for screen-enforced rules, the canonical library rule ID for library-defined rules, `PROC-RULE-NNN` for procedure-enforced rules, `INH-NNN` for inherited rules. Never renumber a library, procedure, or inherited rule.
- Mark inferred statements with `*` and unknown statements with `**`. Do not use bold inline confidence labels.
- Statements marked with `†` were retrieved from dependency library documentation via the Legends CLI RAG service.
- Every rule must record a **Reached from** value naming at least one application workflow or screen. A rule with no reachable entry point belongs in [Rules not reachable from this application](#rules-not-reachable-from-this-application), not in an in-scope section.

> **Note:** Statements marked with † were retrieved from dependency library documentation
> via the Legends CLI RAG service. They describe inherited behavior from the indicated
> origin library and should be verified if the dependency version changes.

## Related documentation
- [Application index](index.md)
- [Overview](overview.md)
- [Architecture](architecture.md)
- [Dependencies](dependencies.md)
- [Configuration](configuration.md)
- [Operations and processes](operations-processes.md)
- [Security and authorization](security-authorization.md)
- [Data model](data-model.md)
- [Tech debt](tech-debt.md)

## Sources
- src/SAIF.Claims.Intake/Claims/ClaimSearch.aspx.vb
- src/SAIF.Claims.Intake/Claims/ClaimDetail.aspx.vb
- src/SAIF.Claims.Intake/Claims/ClaimSubmissionCoordinator.vb
- src/SAIF.Framework.Validation/Validators/ClaimNumberValidator.vb
- src/SAIF.Framework.Validation/Validators/InjuryDateValidator.vb