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.

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.

Resolving a multi-state claim through a versioned precedence matrix and a timezone-aware Date-of-Loss gate A single AUTO claim is eligible under three statutory regimes at once: loss location (Louisiana, America/Chicago), insured domicile (Texas, America/Chicago), and underwriting state (Florida, America/New_York). A compiled precedence matrix for the AUTO line at version 2024.10.1, cached with lru_cache, ranks the roles loss location, then domicile, then underwriting. The ranked candidates flow into a timezone-aware Date-of-Loss gate that converts the DOL with astimezone into each jurisdiction's local time and tests effective_start less-than-or-equal dol_local less-than-or-equal effective_end. Louisiana is in window and, being first in rank order, wins as the governing jurisdiction; Texas and Florida are also in window but are retained as overrides rather than dropped. The output is a frozen MultiStateRoutingDecision with governing jurisdiction LA via LOSS_LOCATION, overridden jurisdictions TX and FL, queue id QUEUE_LA_AUTO, matrix version 2024.10.1, and the canonical payload hash. That decision is written to an append-only audit ledger — recording payload hash, precedence rule id, matrix version, and the full governing-plus-overridden set — before any routing action is published. One AUTO claim · three simultaneously-eligible statutory regimes Loss location Louisiana · America/Chicago Insured domicile Texas · America/Chicago Underwriting state Florida · America/New_York Compiled precedence matrix AUTO · v2024.10.1 · lru_cache key 1 · Loss location highest rank → governs 2 · Domicile retained as override 3 · Underwriting retained as override ranked Timezone-aware DOL gate dol_local = DOL.astimezone(tz) start ≤ dol_local ≤ end LA · in window first eligible → governs TX · in window lower rank → override FL · in window lower rank → override MultiStateRoutingDecision · frozen governing: LA via LOSS_LOCATION overridden: (TX, FL) queue_id: QUEUE_LA_AUTO matrix_version: 2024.10.1 payload_hash: sha256:abc… Append-only audit ledger written before any routing action is published — replayable on examination payload_hash · [email protected] · matrix_version · governing=LA · overridden=(TX,FL)

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 | Y unions, frozen dataclasses, and structural match.
  • 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 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.

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.

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.

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.