---
title: Policy history tracking
summary: Records a new policy-history row whenever a claim's policy number changes, updates the claim, and exposes the claim's policy assignment history read-only.
status: draft
owner: SAIF Doc Gen
last_reviewed: 2026-07-30
library: SAIF.Business.Claim.Maintenance
reference_type: package
operation_id: OP-CM-11
trigger: Caller sets clsClaimPolicy.PolicyNum and calls Save(); the policy history page also triggers a read-only retrieval when the claim policy is loaded
---

# Policy history tracking

Policy history tracking pairs an editable claim-policy object with an append-only history collection. Changing the policy number on the claim stages a history row in memory, and repeated changes before the save collapse into a single pending row — or remove it entirely if the user reverts to the original value. Saving updates the claim and then inserts the pending history row. The history itself is never updated or deleted: its update and delete statement properties deliberately throw.

| Field | Value |
| --- | --- |
| **Operation ID** | OP-CM-11 |
| **Trigger** | Caller sets `clsClaimPolicy.PolicyNum` and calls `clsClaimPolicy.Save()`; loading the claim policy also retrieves the history collection for the read-only history view* |
| **Inputs** | `ClaimID` (Decimal), the new `PolicyNum` (Decimal), `PolicySubCode` (String), and the data-portal info carrying the acting user |
| **Outputs** | New rows in the claim policy history table; an updated claim row carrying the policy number, sub-code, and related employer-provided fields; an in-memory history collection reflecting current and historical policy assignments |
| **Primary components** | `clsClaimPolicy` (`PolicyNum` setter, `IsValid`, `IsDirty`, `Save`, rule methods), `clsClaimPolicyHistory` (`Create`, `BeforeInsert`, `AfterInsert`, `Load`, `Fill`), `clsClaimPolicyHistoryCollection` (`AddPolicyHistory`, `GetLatestRecord`, retrieve and save hooks) |
| **Entry point** | `Business\SAIF.Business.Claim.Maintenance\clsClaimPolicy.vb` |

## Steps

1. **Retrieve the claim policy.** `GetClaimPolicy` invokes the data portal to read the claim's policy data ([SQL-045](../sql/SQL-045.md), [CL_INJURY_CLAIM](../database-tables.md#cl_injury_claim)).
2. **Fill the policy and attach the history.** `AfterRetrieve` reads the single claim row and calls `Fill`, which populates the policy fields and, as its final step, retrieves the full policy-history collection for the claim and registers it as a child object ([SQL-046](../sql/SQL-046.md), [CL_CLAIM_POLICY_HISTORY](../database-tables.md#cl_claim_policy_history)).
3. **Materialize the history rows.** The collection's `AfterRetrieve` loops the reader, constructs one history object per row through `Load`, which calls `Fill`, builds a criteria, and marks the object old, and adds each item to the list.
4. **Change the policy number.** The setter records the new value on the tracking smart decimal and, when the value actually changed, marks the object dirty, calls the history collection's add-policy-history helper with the new number and sub-code, and re-sorts the collection by update date descending.
5. **Stage or collapse the pending history row (`AddPolicyHistory`).**
   - If the latest item is still new — an uncommitted history row from an earlier change in the same edit session — and the new policy number equals the claim's original policy number, remove the pending row entirely, because the user reverted the change.
   - If the latest item is still new but the number differs, update that pending row in place rather than stacking another one.
   - If the latest item is not new, create a brand-new history object through the data portal, whose `Create()` sets the claim identifier, policy number, sub-code, an effective date of today, the current update date, and the acting user, and add it to the collection.
6. **Resolve the latest record.** `GetLatestRecord` sorts the list by update date descending and returns the first item that is not marked deleted.
7. **Validate the policy change.** The rules evaluate as part of validity and save processing:
   - The change-permitted rule fires only when the policy number value was modified and fails when unposted payments or adjustments exist for the claim, reporting that the policy number cannot be changed until all payments and adjustments are posted.*
   - The valid-policy rule fires only when the object is dirty and the value differs from the original, rejecting a zero or blank number and any number the policy service reports as invalid.*
   - The coverage-at-injury rule is warning-only and is checked when the policy number is greater than seventeen, asking the policy service whether coverage existed on the injury date.*
   - Sub-step: the claim and policy services resolve unposted payment or adjustment existence, policy validity, and coverage existence, returning booleans consumed directly by the rules.

   > **Origin**: `ClaimServices.HasUnpostedPaymentsOrAdjustments`, `PolicyServices.IsValidPolicy`, and `PolicyServices.CoverageExists` are implemented in `SAIF.Business.Claim` and executed on behalf of this operation. That library is documented in the previous format, so no per-operation page exists; see [Operations and processes](../../saif-business-claim/operations-processes.md) in the `SAIF.Business.Claim` documentation set.
8. **Aggregate history validity.** The claim policy's validity also requires the history collection to be valid and, when the history is dirty, propagates the history's rules onto the parent so history-side failures surface on the policy object.
9. **Save the claim policy.** `Save()` accepts the pending changes, and when the history is dirty re-synchronizes its own policy number from the latest pending history entry so the claim matches the staged history row, then runs the base save.
10. **Update the claim row.** The update is keyed on claim identifier and update number for optimistic concurrency and writes the policy number, sub-code, employer-provided department, area, and shift fields, the received dates, and the audit fields; zero rows affected adds the DB contention error ([SQL-089](../sql/SQL-089.md), [CL_INJURY_CLAIM](../database-tables.md#cl_injury_claim)) ([BR-CM-001](../business-rules.md#br-cm-001-optimistic-concurrency-updates-fail-if-rowsaffected-1)).
11. **Cascade the history save.** On a successful base save, the history collection is saved, which applies its pending edit and runs the base save over its items.
12. **Insert the pending history row.** Each new history item applies its edit and inserts a row binding the policy number, sub-code, acting user, and claim identifier, with a truncated effective date and the current update date supplied by the statement; zero rows affected adds an insert error carrying the DB contention message ([SQL-005](../sql/SQL-005.md), [CL_CLAIM_POLICY_HISTORY](../database-tables.md#cl_claim_policy_history)) ([BR-CM-001](../business-rules.md#br-cm-001-optimistic-concurrency-updates-fail-if-rowsaffected-1)).
13. **Finish the save.** The claim policy marks itself old and returns the overall result.
14. **Serve the read-only history view.** On the policy history page, the latest item is surfaced as the current effective policy history record; no write path exists from that page, and only a policy-number change on the claim policy produces new history rows.

## Process flow

The following diagram visualizes this operation's control flow.

```mermaid
flowchart TD
	Start([GetClaimPolicy claimID]) --> Read[Read claim policy fields]
	Read --> Hist[Fill retrieves the policy<br>history collection as a child]
	Hist --> Set[Caller sets PolicyNum]
	Set --> Changed{Value actually changed?}
	Changed -->|No| Idle([No history staged])
	Changed -->|Yes| Pending{Latest item is new?}
	Pending -->|Yes, equals original number| Drop[Remove the pending row]
	Pending -->|Yes, different number| Upd[Update the pending row in place]
	Pending -->|No| Create[Create a new history object<br>with today's effective date]
	Drop --> Rules
	Upd --> Rules
	Create --> Rules[Rules: change permitted,<br>valid policy, coverage warning]
	Rules --> Ok{Any error-severity<br>rule broken?}
	Ok -->|Yes| Blocked([Save blocked;<br>history rules propagated to parent])
	Ok -->|No| Save[Save accepts changes;<br>syncs PolicyNum from latest history]
	Save --> Claim[Update claim row keyed on<br>claim id and update num]
	Claim --> Rows{Rows affected?}
	Rows -->|None| Contention([DB contention error;<br>history not saved])
	Rows -->|Yes| Child[Save history collection]
	Child --> Ins[Insert each new history row]
	Ins --> IRows{Rows affected?}
	IRows -->|None| IErr([Insert error with<br>DB contention message])
	IRows -->|Yes| Done([MarkOld; save succeeded])
```

## Failure and edge-case behavior

- If the claim update affects no rows because another user updated the record first and the update number no longer matches, a DB contention message is added and the save is treated as failed for that object ([BR-CM-001](../business-rules.md#br-cm-001-optimistic-concurrency-updates-fail-if-rowsaffected-1)).
- If the history insert affects no rows, an insert error carrying the DB contention message is added ([BR-CM-001](../business-rules.md#br-cm-001-optimistic-concurrency-updates-fail-if-rowsaffected-1)).
- The change-permitted rule blocks the policy-number change at validation time, before the save, whenever unposted payments or adjustments exist.
- The valid-policy rule rejects a zero or blank policy number and any number the policy service rejects, but evaluates only when the value is actually dirty and changed.
- The coverage-at-injury check is a non-blocking warning that applies only when the policy number is greater than seventeen.
- Changing the policy number back to the original value within the same edit session silently removes the pending unsaved history row rather than persisting it, avoiding a round-trip no-op entry.
- Changing the policy number several times before saving overwrites the pending row in place, so only the final value is inserted rather than one row per keystroke.
- The latest-record lookup skips items marked deleted, so soft-deleted in-memory items are never returned as current.
- No update or delete statement exists for the history object or collection — those criteria properties throw an invalid-statement application exception — confirming that history rows are append-only and are never modified or removed once inserted.
- The claim policy is invalid whenever either the base object or the history collection is invalid, and dirty history propagates its rules onto the parent so consumers see history-side errors even though history is not directly editable.

## Related documentation

- [Operations and processes](../operations-processes.md)
- [Business rules](../business-rules.md)
- [SQL catalog](../sql/index.md)
- [Database tables](../database-tables.md)

> **Note:** Statements marked with `*` were inferred from code structure, naming
> conventions, or contextual evidence where direct confirmation was not available
> in source artifacts.

## Sources

- `Business\SAIF.Business.Claim.Maintenance\clsClaimPolicy.vb`
- `Business\SAIF.Business.Claim.Maintenance\clsClaimPolicyHistory.vb`
- `Business\SAIF.Business.Claim.Maintenance\clsClaimPolicyHistoryCollection.vb`
