Handling Multi-State Compliance in Claims Routing
This guide drills into the hardest case the State Regulation Mapping layer has to resolve: a single claim whose loss location, insured domicile, and underwriting state fall under three different statutory regimes at once, and which therefore cannot be routed by a flat per-state lookup.
Problem Statement
Permalink to "Problem Statement"A vehicle accident occurs in Louisiana, the insured is garaged in Texas, and the policy was underwritten in Florida. Each jurisdiction asserts its own prompt-payment clock, its own mandatory disclosures, and its own deductible arithmetic — and all three rule sets are simultaneously eligible to govern the claim. A routing engine built around if state == "TX" conditionals resolves this the way Python happens to order its branches: whichever conditional runs first wins, and the same claim can route to a different adjudication queue on replay.
That non-determinism is not a cosmetic bug. During a market-conduct examination an examiner asks why claim POL-00481923 settled under Texas timelines rather than Louisiana’s, and the system has to answer with the exact precedence rule and matrix version that drove the branch — not “it depended on dictionary insertion order.” Three distinct failure modes converge here:
- Precedence ambiguity — overlapping mandates resolved by evaluation order instead of an explicit, declared priority.
- Temporal drift — a Date of Loss (DOL) compared against policy effective windows without timezone awareness, producing off-by-one-hour misroutes across daylight-saving boundaries and retroactive rate filings.
- Audit gaps — a routing decision that records the winning jurisdiction but discards the secondary mandates it overrode, leaving no reconstructable lineage.
The pattern below resolves all three: a compiled precedence graph evaluated as a pure function, timezone-aware DOL validation, and an immutable decision that preserves every overridden flag.
Prerequisites
Permalink to "Prerequisites"This pattern sits on top of the deterministic evaluation engine described in the parent State Regulation Mapping component, and it consumes the field-level contract declared by Policy Schema Design. Before applying it, confirm:
- Python 3.10+ — for
X | Yunions, frozen dataclasses, and structuralmatch. - A versioned rule registry exposing the active matrix version through
REGULATION_RULE_VERSION(e.g.2024.10.1), so a compiled graph can be keyed to the exact statutes in force. - An append-only audit store — the governance backbone shared across the Core Architecture & Compliance Mapping domain — written before any routing action is published.
- IANA timezone data available (
zoneinfo, standard library on 3.9+), so every jurisdiction’s effective window is evaluated in its own local time, not the server’s.
Pin the precedence matrix as data, never as control flow: the priority ordering between loss location, domicile, and underwriting state is a versioned document, so a regulatory reinterpretation is a new version string rather than an in-place code edit.
Step-by-Step Implementation
Permalink to "Step-by-Step Implementation"Step 1 — Compile the precedence matrix into a cached resolver
Permalink to "Step 1 — Compile the precedence matrix into a cached resolver"Precedence is declared once, as an ordered tuple per coverage line, and compiled into a resolver that ranks the candidate jurisdictions for a claim. Caching the compiled order with functools.lru_cache means repeated evaluations of the same jurisdictional combination bypass the registry lookup entirely.
from __future__ import annotations
import logging
from dataclasses import dataclass
from enum import Enum
from functools import lru_cache
logger = logging.getLogger("insurtech.multistate")
class Jurisdiction(str, Enum):
CA = "CA"
FL = "FL"
LA = "LA"
NY = "NY"
TX = "TX"
class PrecedenceRole(str, Enum):
LOSS_LOCATION = "LOSS_LOCATION"
DOMICILE = "DOMICILE"
UNDERWRITING = "UNDERWRITING"
# Declared precedence per line of business: the ranked role order that
# decides which jurisdiction's statutes govern when several overlap.
PRECEDENCE_MATRIX: dict[str, tuple[PrecedenceRole, ...]] = {
"AUTO": (PrecedenceRole.LOSS_LOCATION, PrecedenceRole.DOMICILE, PrecedenceRole.UNDERWRITING),
"DWELLING": (PrecedenceRole.LOSS_LOCATION, PrecedenceRole.UNDERWRITING, PrecedenceRole.DOMICILE),
}
@lru_cache(maxsize=512)
def compile_precedence(line_of_business: str, matrix_version: str) -> tuple[PrecedenceRole, ...]:
"""Return the ranked role order for a line, cached per matrix version."""
try:
order = PRECEDENCE_MATRIX[line_of_business]
except KeyError as exc:
raise ValueError(f"No precedence rule for line {line_of_business!r}") from exc
logger.debug("Compiled precedence %s @ %s -> %s", line_of_business, matrix_version, order)
return order
Keying the cache on matrix_version as well as the line of business guarantees that publishing a new matrix version invalidates the cached order without a manual flush — the new version is simply a different cache key.
Step 2 — Resolve the governing jurisdiction with timezone-aware DOL validation
Permalink to "Step 2 — Resolve the governing jurisdiction with timezone-aware DOL validation"The resolver walks the ranked roles, but a candidate only qualifies if the Date of Loss falls inside that jurisdiction’s policy effective window, evaluated in the jurisdiction’s own local time. Comparing naive timestamps is the single most common source of cross-jurisdictional misroutes; the Python datetime documentation covers the timezone-aware arithmetic this step depends on.
from datetime import datetime
from zoneinfo import ZoneInfo
_TZ_BY_JURISDICTION: dict[Jurisdiction, str] = {
Jurisdiction.CA: "America/Los_Angeles",
Jurisdiction.FL: "America/New_York",
Jurisdiction.LA: "America/Chicago",
Jurisdiction.NY: "America/New_York",
Jurisdiction.TX: "America/Chicago",
}
@dataclass(frozen=True)
class JurisdictionWindow:
jurisdiction: Jurisdiction
role: PrecedenceRole
effective_start: datetime # timezone-aware
effective_end: datetime # timezone-aware
def dol_within_window(date_of_loss_utc: datetime, window: JurisdictionWindow) -> bool:
"""Validate the DOL against an effective window in the jurisdiction's local tz."""
if date_of_loss_utc.tzinfo is None:
raise ValueError("date_of_loss_utc must be timezone-aware")
local_tz = ZoneInfo(_TZ_BY_JURISDICTION[window.jurisdiction])
dol_local = date_of_loss_utc.astimezone(local_tz)
return window.effective_start <= dol_local <= window.effective_end
def resolve_primary(
line_of_business: str,
matrix_version: str,
candidates: dict[PrecedenceRole, JurisdictionWindow],
date_of_loss_utc: datetime,
) -> tuple[JurisdictionWindow, list[JurisdictionWindow]]:
"""Return (governing window, overridden windows) deterministically."""
order = compile_precedence(line_of_business, matrix_version)
ranked = [candidates[role] for role in order if role in candidates]
eligible = [w for w in ranked if dol_within_window(date_of_loss_utc, w)]
if not eligible:
raise ValueError("No jurisdiction window contains the date of loss")
primary, *overridden = eligible
logger.info(
"Primary jurisdiction %s via %s; overrode %s",
primary.jurisdiction.value, primary.role.value,
[w.jurisdiction.value for w in overridden],
)
return primary, overridden
Because the eligible list preserves the declared order, the first qualifying window is the governing one and every lower-ranked window is retained as an explicit override — never silently dropped.
Step 3 — Emit an immutable, audit-ready routing decision
Permalink to "Step 3 — Emit an immutable, audit-ready routing decision"The resolved jurisdiction is stamped into a frozen decision alongside the overridden mandates, the precedence rule applied, and the matrix version, so the branch is fully reconstructable on replay.
@dataclass(frozen=True)
class MultiStateRoutingDecision:
governing_jurisdiction: Jurisdiction
governing_role: PrecedenceRole
overridden_jurisdictions: tuple[Jurisdiction, ...]
queue_id: str
matrix_version: str
payload_hash: str
def route_multistate_claim(
line_of_business: str,
matrix_version: str,
candidates: dict[PrecedenceRole, JurisdictionWindow],
date_of_loss_utc: datetime,
payload_hash: str,
) -> MultiStateRoutingDecision:
primary, overridden = resolve_primary(
line_of_business, matrix_version, candidates, date_of_loss_utc
)
return MultiStateRoutingDecision(
governing_jurisdiction=primary.jurisdiction,
governing_role=primary.role,
overridden_jurisdictions=tuple(w.jurisdiction for w in overridden),
queue_id=f"QUEUE_{primary.jurisdiction.value}_{line_of_business}",
matrix_version=matrix_version,
payload_hash=payload_hash,
)
The resulting queue_id is the contract that hands the claim downstream to the Adjuster Assignment Algorithms engine, while the retained overridden_jurisdictions let Coverage Validation Rules re-check any secondary mandate a claimant later disputes.
Verification & Testing
Permalink to "Verification & Testing"Determinism is the property under test: identical inputs must always produce an identical decision, and overridden mandates must never be lost. Drive the resolver from a fixed fixture and assert the full decision shape, not just the winning jurisdiction.
from datetime import datetime, timedelta
from zoneinfo import ZoneInfo
def _window(j: Jurisdiction, role: PrecedenceRole) -> JurisdictionWindow:
tz = ZoneInfo(_TZ_BY_JURISDICTION[j])
start = datetime(2024, 1, 1, tzinfo=tz)
return JurisdictionWindow(j, role, start, start + timedelta(days=365))
def test_loss_location_wins_for_auto() -> None:
candidates = {
PrecedenceRole.LOSS_LOCATION: _window(Jurisdiction.LA, PrecedenceRole.LOSS_LOCATION),
PrecedenceRole.DOMICILE: _window(Jurisdiction.TX, PrecedenceRole.DOMICILE),
PrecedenceRole.UNDERWRITING: _window(Jurisdiction.FL, PrecedenceRole.UNDERWRITING),
}
dol = datetime(2024, 6, 15, 14, 30, tzinfo=ZoneInfo("UTC"))
decision = route_multistate_claim("AUTO", "2024.10.1", candidates, dol, "sha256:abc")
assert decision.governing_jurisdiction is Jurisdiction.LA
assert decision.governing_role is PrecedenceRole.LOSS_LOCATION
assert decision.overridden_jurisdictions == (Jurisdiction.TX, Jurisdiction.FL)
# Replay must be byte-identical.
assert decision == route_multistate_claim("AUTO", "2024.10.1", candidates, dol, "sha256:abc")
Add a daylight-saving regression: set a DOL one hour either side of a spring-forward boundary and assert the eligibility result flips only when it genuinely crosses the local window edge. A passing replay assertion plus a DST boundary case is the minimum bar before this resolver touches production traffic.
Compliance & Audit Note
Permalink to "Compliance & Audit Note"The governing_role, overridden_jurisdictions, and matrix_version fields are what make a multi-state decision defensible. By hashing the canonical payload and stamping the precedence rule and matrix version into a frozen decision, the resolver lets an examiner replay claim POL-00481923 against the exact statutes in force at its Date of Loss and see precisely why Louisiana’s loss-location mandate overrode the Texas and Florida regimes. Retaining the overridden jurisdictions — rather than discarding everything but the winner — satisfies NAIC market-conduct expectations and state-DOI examination requests that a carrier demonstrate it evaluated every applicable mandate, not just the one it acted on. The append-only audit record, written before the routing action publishes, is the artifact that proves the precedence resolution was deterministic and version-bound; for the authoritative coordination framework see the National Association of Insurance Commissioners (NAIC) uniform reporting resources.
Troubleshooting Checklist
Permalink to "Troubleshooting Checklist"Precedence flips between runs for the same claim
The resolver is reading evaluation order rather than the declared matrix, usually because a candidate dict is being iterated directly. Always walk compile_precedence(...)'s ranked roles, and assert a replay equality in tests so a regression in ordering fails CI rather than an audit.
Off-by-one-hour misroute near a daylight-saving boundary
A naive (tzinfo-less) Date of Loss is being compared against a local effective window. Reject any DOL without tzinfo at the boundary, convert with astimezone(ZoneInfo(...)) per jurisdiction, and add the DST regression case from the verification section.
Stale precedence after a registry publish
lru_cache is still serving the prior order because the cache key omits the version. Key compile_precedence on matrix_version so a new publish is a new key; never mutate an active matrix in place — that breaks the link between a decision and the rules that produced it, the same drift the Data Boundary Enforcement discipline exists to prevent.
No eligible jurisdiction for the date of loss
Every candidate window excludes the DOL, typically a back-dated endorsement or a retroactive rate filing whose effective window was never extended. Surface the explicit ValueError, quarantine the claim for manual review, and reconcile the effective window in the registry rather than widening the comparison silently.
Overridden mandates lost downstream
A worker consumed only governing_jurisdiction and dropped overridden_jurisdictions, so a disputed secondary mandate has no trail. Keep MultiStateRoutingDecision frozen and propagate the full tuple into the Claims Lifecycle Architecture so secondary flags remain reconstructable through settlement.
Related
Permalink to "Related"- State Regulation Mapping — the parent component this precedence pattern extends
- Policy Schema Design — the field-level contract the candidate jurisdiction windows are validated against
- Data Boundary Enforcement — keeps non-conforming multi-state payloads out of core systems
- Adjuster Assignment Algorithms — consumes the resolved
queue_idto staff the governing jurisdiction’s queue - Coverage Validation Rules — re-checks overridden secondary mandates when a claimant disputes them