Fallback Routing for Unclassified Deductions
A deduction code that passes structural validation but matches no canonical entry must resolve to a defensible statutory treatment or stop the record — never default to NULL, GENERAL, or a guessed pre-tax flag. This guide is the deduction-specific application of the Fallback Routing Strategies pattern within the broader Core Architecture & Compliance Mapping framework: it covers the failure surface unique to deductions — vendor suffixes, truncated codes, pre/post-tax ambiguity, and garnishment caps — layered on top of the general tier hierarchy.
Problem Framing
Naive deduction handling assumes an exact-match dictionary is sufficient: look up deduction_code, apply the mapped tax treatment, move on. That assumption breaks the moment a vendor renames 401K to RET_PRETAX_V2, a CSV export truncates an 8-character code to 6, or two vendors reuse the same string for different statutory buckets. The dictionary returns nothing, and an unguarded pipeline does the worst possible thing: it coerces the unknown to a catch-all bucket and keeps running. The deduction still hits the paycheck, but with the wrong tax treatment.
The cost is asymmetric. A misrouted deduction does not raise an exception — it produces a wrong-but-plausible paycheck that reconciles to itself and surfaces only at a Department of Labor audit or year-end W-2 reconciliation. A deduction defaulted to pre-tax when it should be post-tax understates taxable wages and corrupts the ACA Tracking Logic affordability denominator. A garnishment routed without a cap check can breach the federal Consumer Credit Protection Act ceiling under 15 U.S.C. § 1673 and illegally erode wages below the FLSA Threshold Mapping floor. Each of these is a per-employee, per-pay-period violation that compounds silently.
Fallback routing replaces the silent default with a deterministic decision: match by similarity if confident, otherwise apply a threshold-guarded statutory default, otherwise quarantine for human review. It never infers intent.
Prerequisites & Data Requirements
The router runs only after structural validation guarantees a record is the right shape. Boundary enforcement is owned upstream by Data Boundary Definitions; a malformed payload must be rejected there so the router never confuses noise with an unmapped-but-valid deduction. Every record reaching this stage must carry:
record_id— a stable, deduplicated identifier so a retried run resolves to the same decision rather than minting a new one.raw_code— the vendor deduction code preserved verbatim. Normalization happens inside the router so the original survives in the audit trail.work_state— a resolved jurisdiction key, not free-text. An unresolved state is itself a quarantine condition, never a silent default to federal.amount— aDecimal, parsed withDecimal(str(value)). A nativefloatamount must be rejected at the boundary; binary rounding must never enter monetary state.disposable_earnings,hours_worked,gross_pay—Decimalvalues needed to evaluate the garnishment cap and FLSA floor.
You also need a versioned alias registry: a list of canonical code strings plus their known vendor aliases, with a registry version stamped into every routing decision so a re-run can be tied to the exact mapping that produced it.
Step-by-Step Implementation
The router evaluates four tiers in strict order and stops at the first that resolves. All monetary arithmetic uses decimal, logs are structured key=value, and state transitions are immutable.
Step 1 — Normalize the code
Strip everything that vendors mutate but that carries no meaning: case, whitespace, separators, and version suffixes.
import re
def normalize(raw_code: str) -> str:
return re.sub(r"[^A-Z0-9]", "", raw_code.upper())
assert normalize(" ret_pretax_v2 ") == "RETPRETAXV2"
assert normalize("401(k)") == "401K"
Expected output: the assertions pass. Normalization is idempotent — normalize(normalize(x)) == normalize(x) — so it is safe to re-apply on retries.
Step 2 — Exact then similarity match
Try the canonical map first. On a miss, score the normalized code against the alias registry with difflib.SequenceMatcher and accept only a confidence at or above the threshold.
from decimal import Decimal
from difflib import SequenceMatcher
CONFIDENCE_THRESHOLD = Decimal("0.85")
def best_confidence(normalized: str, registry: list[str]) -> Decimal:
best = 0.0
for alias in registry:
best = max(best, SequenceMatcher(None, normalized, alias).ratio())
return Decimal(str(best))
assert best_confidence("401KMATCH", ["401KMATCH"]) == Decimal("1.0")
assert best_confidence("XYZ", ["401KMATCH"]) < CONFIDENCE_THRESHOLD
Expected output: an exact alias scores 1.0; an unrelated code scores below 0.85 and falls through to the next tier. The 0.85 floor is deliberately conservative — a false match applies a wrong tax treatment, so ambiguity must escalate, not resolve.
Step 3 — Apply threshold guards before any default
Below the confidence floor, a record may still be safely defaulted, but only after it clears the statutory guards. The federal CCPA garnishment cap is the lesser of 25% of disposable earnings or the amount by which disposable earnings exceed 30× the federal minimum wage:
where is disposable earnings and is the federal hourly minimum (, so the protected floor is ).
from decimal import Decimal
FED_MIN_WAGE = Decimal("7.25")
FED_PROTECTED = Decimal("30") * FED_MIN_WAGE # $217.50
def garnishment_cap(disposable: Decimal) -> Decimal:
by_pct = disposable * Decimal("0.25")
by_excess = max(disposable - FED_PROTECTED, Decimal("0"))
return min(by_pct, by_excess)
assert garnishment_cap(Decimal("1000")) == Decimal("250.00")
assert garnishment_cap(Decimal("200")) == Decimal("0") # fully protected
Expected output: at $1000 disposable the cap is $250.00; at $200 the worker is fully protected and the cap is $0. State overrides (for example California child-support allowances) are applied from a jurisdictional table and may only lower the worker’s exposure, never raise it above the federal cap.
Step 4 — Route through the tier hierarchy
Fold the steps into one deterministic route() call. Unknown-but-clean deductions default to post-tax — the safe direction, since post-tax never understates taxable wages — but only after the garnishment and FLSA guards pass; otherwise the record is quarantined.
import logging
from dataclasses import dataclass
from decimal import Decimal
from enum import Enum
logger = logging.getLogger("payroll.deduction_router")
class Tier(Enum):
PRIMARY = "primary_match"
SIMILARITY = "similarity_match"
POST_TAX_DEFAULT = "post_tax_default"
QUARANTINE = "pending_classification"
@dataclass(frozen=True)
class Deduction:
record_id: str
employee_id: str
work_state: str
raw_code: str
amount: Decimal
disposable_earnings: Decimal
hours_worked: Decimal
gross_pay: Decimal
def __post_init__(self) -> None:
if not isinstance(self.amount, Decimal):
raise TypeError(f"amount must be Decimal, got {type(self.amount).__name__}")
def violates_flsa_floor(d: Deduction) -> bool:
if d.hours_worked <= 0:
return False
effective_rate = (d.gross_pay - d.amount) / d.hours_worked
return effective_rate < FED_MIN_WAGE
def route(d: Deduction, canonical: dict[str, str], registry: list[str], version: str) -> Tier:
code = normalize(d.raw_code)
if code in canonical:
tier = Tier.PRIMARY
elif best_confidence(code, registry) >= CONFIDENCE_THRESHOLD:
tier = Tier.SIMILARITY
elif d.amount > garnishment_cap(d.disposable_earnings):
tier = Tier.QUARANTINE
elif violates_flsa_floor(d):
tier = Tier.QUARANTINE
else:
tier = Tier.POST_TAX_DEFAULT
logger.info(
"event=deduction_route record=%s emp=%s state=%s tier=%s registry=%s review=%s",
d.record_id, d.employee_id, d.work_state, tier.value, version,
tier in (Tier.POST_TAX_DEFAULT, Tier.QUARANTINE),
)
return tier
Expected output: a canonical code returns PRIMARY; a close alias returns SIMILARITY; an over-cap garnishment or floor-breaching deduction returns QUARANTINE; a clean unknown returns POST_TAX_DEFAULT. Every call emits one structured log line tying the decision to the registry version.
Verification
Confirm correctness with boundary cases specific to unclassified deductions, run in CI and against a daily reconciliation job:
- Confidence boundary. Assert a code scoring exactly
0.85is accepted asSIMILARITYand0.849...falls through. The threshold comparison must be>=, evaluated inDecimal, neverfloat. - Garnishment cap boundary. With disposable earnings of $1000, assert
amount == Decimal("250.00")routes to a default (at the cap) whileDecimal("250.01")quarantines. Test the protected-wage edge: any disposable at or below $217.50 yields a $0 cap, so any positive garnishment quarantines. - FLSA floor boundary. Construct a record where
(gross_pay - amount) / hours_workedlands exactly on $7.25 (passes) and one cent below (quarantines). - Safe-default direction. Assert an unknown clean code resolves to
POST_TAX_DEFAULT, never pre-tax — a default must never understate taxable wages. - Decimal enforcement. Assert constructing a
Deductionwith afloatamount raisesTypeError, and reconcile a synthetic batch to the cent. - Normalization idempotency. Property-test that
normalizeis stable under re-application across suffix, case, and separator mutations.
Failure Modes
- Suffix drift accepted as a new bucket. A vendor ships
MED_V3and the canonical map silently treats it as unmapped, defaulting a pre-tax medical premium to post-tax. Root cause: normalization that strips digits but not version tokens, or an alias registry that was never updated. Fix: strip non-alphanumerics soMED_V3normalizes towardMEDV3, keepMEDaliases in the versioned registry, and alert when similarity-match volume for a vendor spikes — a spike means the registry, not the queue, needs the update. - Truncated code creating a false high-confidence match. A CSV export cuts
401KCATCHUPto401KCAT, which scores above0.85against401KCATCHUPand is mapped — but it could equally be a different catch-up plan. Root cause: similarity matching on truncated input without a length-overlap guard. Fix: require a minimum normalized-length overlap before accepting a similarity match, and quarantine codes shorter than the registry’s minimum canonical length. - Garnishment evaluated on gross instead of disposable earnings. The cap is computed against
gross_pay, inflating the allowable amount and breaching the CCPA ceiling. Root cause: conflating gross with disposable earnings (gross minus legally required withholdings). Fix: the cap function takesdisposable_earningsas an explicit argument; never passgross_payinto it, and assert the two fields are distinct at the boundary.
Frequently Asked Questions
Why default unknown deductions to post-tax instead of pre-tax?
Post-tax is the conservative direction. A deduction wrongly defaulted to pre-tax reduces reported taxable wages and corrupts the ACA affordability denominator, both of which are statutory understatements that surface at audit. A deduction wrongly defaulted to post-tax merely over-taxes the employee temporarily and is trivially correctable once classification is confirmed. When you must guess the direction of an error, guess toward the one that does not under-report to the IRS.
What confidence threshold should similarity matching use?
Start at 0.85 and treat it as a floor, not a target. The cost of a false positive — applying a wrong tax treatment that reconciles to itself — is far higher than the cost of an extra quarantine review, so the threshold should be conservative. Pair it with a minimum length-overlap guard so truncated codes cannot score artificially high, and review the threshold whenever a vendor’s alias set changes.
How is the garnishment cap affected by state overrides?
The federal CCPA cap under 15 U.S.C. § 1673 is the ceiling on the worker’s exposure. State rules may only reduce it — for example a state that limits general garnishments to 10% of gross — never raise it. Apply the federal cap first, then take the more protective of the federal and state results. Child-support and tax-levy garnishments follow separate, higher statutory ceilings and must be classified before the general cap is applied.
When does an unclassified deduction belong in quarantine rather than a default?
Quarantine whenever resolving the record would require a guess that an auditor could challenge: the amount breaches the garnishment cap, the deduction would push the effective rate below the FLSA floor, the jurisdiction is unresolved, or the code is too short to match safely. A clean unknown that clears every guard can take the post-tax default; anything that touches a statutory limit escalates to human review.
Related
- Fallback Routing Strategies — the general tier hierarchy, jurisdictional defaults, and audit contract this page specializes.
- Data Boundary Definitions — the canonical-record contract every deduction must satisfy before routing.
- FLSA Threshold Mapping — the minimum-wage floor the post-tax default must never breach.
- Deduction mapping rules — the downstream engine that consumes resolved deduction treatments.