Mapping Garnishments to General-Ledger Accounts
When two or more post-tax garnishment orders compete for the same paycheck, an engine that withholds each order independently instead of testing the combined total against one disposable-earnings ceiling silently withholds more than 15 U.S.C. § 1673 allows and posts the overage to the wrong general-ledger account. This pattern is part of the Deduction Mapping Rules topic within the Payroll Calculation Engines & Validation Rules framework, and it isolates post-tax garnishment routing — child support, federal tax levies, other creditor garnishments, and voluntary wage assignments — as a distinct allocation stage that runs after every pre-tax deduction has already reduced taxable wages.
Problem Framing
Garnishment routing breaks naive implementations for three reasons that only surface once a second or third order attaches to the same employee:
- Wrong earnings base. These are post-tax withholdings, evaluated against disposable earnings — gross wages less amounts required by law to be withheld — after the Ordering Pre-Tax 401(k) and Section 125 Deductions stage has already reduced taxable wages. An engine that reuses the pre-tax ordering logic, or that further subtracts voluntary deductions before computing disposable earnings, shrinks the base and under-remits every attached order.
- One ceiling, many orders. The Consumer Credit Protection Act (CCPA) Title III caps the total amount an employer may withhold for garnishment in a pay period — it is not a per-order allowance. An engine that checks each order’s own limit in isolation, without a shared running ceiling, can authorize a combined withholding that exceeds 15 U.S.C. § 1673 the moment a second order attaches.
- Priority collapse under contention. When the combined requested amount exceeds the ceiling, the order that resolves first matters. Child support outranks a federal tax levy, which outranks an ordinary creditor garnishment, which outranks a voluntary wage assignment. An engine that allocates by arrival order instead of statutory precedence can starve a support order to make room for a garnishment that attached to the file first.
Disposable earnings is defined precisely — not “net pay” — by 15 U.S.C. § 1672(a): earnings remaining after amounts required by law to be withheld, meaning federal and state income tax and FICA, and nothing else:
The ordinary CCPA ceiling that governs a ordinary creditor garnishment (no support order in play) is the lesser of 25% of disposable earnings or the amount by which disposable earnings exceed thirty times the federal minimum hourly wage, per 15 U.S.C. § 1673(a):
When a child-support order is present, § 1673(b) supersedes the ordinary ceiling with its own percentage of disposable earnings — 50% or 60% depending on whether the individual is also supporting another spouse or child, each raised five points (to 55% or 65%) when the arrears exceed twelve weeks:
Prerequisites & Data Requirements
Every monetary field is Decimal, never float, for the same reason the platform enforces Decimal precision everywhere a statutory cap is a hard step function rather than a rounded estimate.
| Field | Type | Precondition |
|---|---|---|
order_id |
str |
Unique per attached order; stable across pay periods for arrears tracking. |
order_type |
Enum |
One of child_support, federal_tax_levy, creditor_garnishment, voluntary_assignment. |
priority |
int (derived) |
Fixed by order_type per statutory precedence — never set from ingestion order or timestamp. |
requested_amount |
Decimal |
Already computed upstream from the order’s own statutory basis for this pay period. |
gl_liability_account |
str |
Must resolve against the active ERP chart of accounts before activation. |
support_ceiling_pct |
Decimal, optional |
Required only when order_type = child_support; the § 1673(b) percentage (0.50 / 0.55 / 0.60 / 0.65). |
Two preconditions sit outside the function signature:
- Disposable earnings is computed strictly per 15 U.S.C. § 1672(a). Only amounts required by law to be withheld — federal and state income tax, FICA — reduce gross wages to disposable earnings. Voluntary deductions, including anything already resolved by pre-tax Deduction Mapping Rules, never enter this subtraction.
requested_amountis resolved outside this stage. A federal tax levy’s per-period amount comes from the IRS Publication 1494 exempt-amount tables under 26 U.S.C. § 6334; a support order’s amount comes from the issuing agency; an ordinary garnishment’s amount comes from the writ. This engine’s only job is prioritizing and capping what competing orders already request — it does not derive any order’s own statutory amount.
Step-by-Step Implementation
Step 1 — Pin constants and enumerate order types. Fix the federal minimum wage, the per-period multiples used to operationalize the “30 times minimum wage” prong of § 1673(a) across pay frequencies (per DOL Wage and Hour Division Fact Sheet #30), and the priority each order type resolves at.
from dataclasses import dataclass
from decimal import Decimal, ROUND_HALF_UP
from enum import Enum
from typing import Dict, List, Optional
import logging
logger = logging.getLogger("payroll.garnishment.gl")
CENTS = Decimal("0.01")
FEDERAL_MIN_WAGE = Decimal("7.25") # 29 U.S.C. § 206(a)(1)
# Per-period multiples of the federal minimum wage (DOL Fact Sheet #30),
# operationalizing the "30 times" prong of 15 U.S.C. § 1673(a)(2) by pay frequency.
PERIOD_MULTIPLES: Dict[str, Decimal] = {
"weekly": Decimal("30"),
"biweekly": Decimal("60"),
"semimonthly": Decimal("65"),
"monthly": Decimal("130"),
}
class OrderType(str, Enum):
CHILD_SUPPORT = "child_support"
FEDERAL_TAX_LEVY = "federal_tax_levy"
CREDITOR_GARNISHMENT = "creditor_garnishment"
VOLUNTARY_ASSIGNMENT = "voluntary_assignment"
# Statutory precedence — lower integer resolves first.
PRIORITY_BY_TYPE: Dict[OrderType, int] = {
OrderType.CHILD_SUPPORT: 1,
OrderType.FEDERAL_TAX_LEVY: 2,
OrderType.CREDITOR_GARNISHMENT: 3,
OrderType.VOLUNTARY_ASSIGNMENT: 4,
}
Step 2 — Model orders and allocations as typed, validating dataclasses. Reject a support order that arrives without its ceiling percentage rather than defaulting to a guess.
@dataclass(frozen=True)
class GarnishmentOrder:
order_id: str
order_type: OrderType
requested_amount: Decimal # pre-computed per the order's own statutory basis
gl_liability_account: str
support_ceiling_pct: Optional[Decimal] = None
def __post_init__(self) -> None:
if self.requested_amount < Decimal("0"):
raise ValueError(f"order={self.order_id} requested_amount must be >= 0")
if self.order_type == OrderType.CHILD_SUPPORT and self.support_ceiling_pct is None:
raise ValueError(f"order={self.order_id} child support requires support_ceiling_pct")
@dataclass
class GarnishmentAllocation:
order_id: str
order_type: OrderType
gl_liability_account: str
allocated_amount: Decimal
deferred_amount: Decimal
Step 3 — Compute disposable earnings and the single aggregate ceiling. A support order’s own percentage governs the whole ceiling whenever one is present; otherwise the ordinary 25% / 30×-minimum-wage rule applies. Expected output for gross=2000.00, required=400.00, biweekly, no support order: ceiling == Decimal("400.00") (the 25% branch, since 1600 − 435.00 = 1165.00 is larger).
def compute_disposable_earnings(gross_wages: Decimal, required_withholding: Decimal) -> Decimal:
"""Disposable earnings per 15 U.S.C. § 1672(a): gross wages less amounts required
by law to be withheld. Voluntary deductions never enter this subtraction."""
disposable = gross_wages - required_withholding
if disposable < Decimal("0"):
raise ValueError("disposable earnings cannot be negative")
return disposable
def aggregate_ceiling(
orders: List[GarnishmentOrder], disposable_earnings: Decimal, pay_frequency: str,
) -> Decimal:
"""Resolve the single CCPA ceiling that governs combined withholding this period."""
support_orders = [o for o in orders if o.order_type == OrderType.CHILD_SUPPORT]
if support_orders:
pct = max(o.support_ceiling_pct for o in support_orders) # § 1673(b)
ceiling = disposable_earnings * pct
else:
multiple = PERIOD_MULTIPLES[pay_frequency]
pct_based = disposable_earnings * Decimal("0.25")
floor_based = disposable_earnings - multiple * FEDERAL_MIN_WAGE
ceiling = max(Decimal("0"), min(pct_based, floor_based))
return ceiling.quantize(CENTS, rounding=ROUND_HALF_UP)
Step 4 — Allocate by statutory priority, never by input order. Sort on PRIORITY_BY_TYPE, walk the list once, and let each order consume only what remains of the ceiling.
def allocate_garnishments(
orders: List[GarnishmentOrder],
gross_wages: Decimal,
required_withholding: Decimal,
pay_frequency: str,
employee_id: str = "unknown",
) -> List[GarnishmentAllocation]:
disposable = compute_disposable_earnings(gross_wages, required_withholding)
ceiling = aggregate_ceiling(orders, disposable, pay_frequency)
remaining = ceiling
allocations: List[GarnishmentAllocation] = []
for order in sorted(orders, key=lambda o: PRIORITY_BY_TYPE[o.order_type]):
allocated = min(order.requested_amount, remaining).quantize(CENTS, rounding=ROUND_HALF_UP)
deferred = order.requested_amount - allocated
remaining -= allocated
allocations.append(
GarnishmentAllocation(
order.order_id, order.order_type, order.gl_liability_account,
allocated, deferred,
)
)
logger.info(
"garnishment_allocate emp=%s order=%s type=%s requested=%s allocated=%s "
"deferred=%s remaining_ceiling=%s",
employee_id, order.order_id, order.order_type.value,
order.requested_amount, allocated, deferred, remaining,
)
logger.info(
"garnishment_ceiling emp=%s disposable_earnings=%s ceiling=%s",
employee_id, disposable, ceiling,
)
return allocations
Step 5 — Run a multi-order case that hits the ceiling. Three orders compete: a support order with a 60% ceiling percentage, a federal tax levy, and a creditor garnishment, requesting a combined 1500.00 against a 960.00 ceiling.
orders = [
GarnishmentOrder("ORD-1", OrderType.CHILD_SUPPORT, Decimal("700.00"),
"2210-Support-Liability", support_ceiling_pct=Decimal("0.60")),
GarnishmentOrder("ORD-2", OrderType.FEDERAL_TAX_LEVY, Decimal("500.00"),
"2220-IRS-Levy-Liability"),
GarnishmentOrder("ORD-3", OrderType.CREDITOR_GARNISHMENT, Decimal("300.00"),
"2230-Garnishment-Liability"),
]
result = allocate_garnishments(
orders,
gross_wages=Decimal("2000.00"),
required_withholding=Decimal("400.00"),
pay_frequency="biweekly",
employee_id="E-77201",
)
# ceiling = Decimal("960.00") — the 60% support ceiling governs, not 25%/30x
# ORD-1 child_support: allocated 700.00, deferred 0.00
# ORD-2 federal_tax_levy: allocated 260.00, deferred 240.00
# ORD-3 creditor_garnishment: allocated 0.00, deferred 300.00
Verification
- Ceiling never exceeded. For any order set,
sum(a.allocated_amount for a in result) <= ceiling, and the two are exactly equal whenever total requests meet or exceed the ceiling — the engine never under-allocates headroom that priority entitles a lower-ranked order to consume. - Priority respected regardless of input order. Shuffle the
orderslist before callingallocate_garnishmentsand assert the allocation for eachorder_idis unchanged — the sort key isorder_type, never list position or arrival timestamp. - Per-order GL correctness. Assert every
GarnishmentAllocation.gl_liability_accountequals its source order’sgl_liability_account; no allocated amount may post against another order’s account even when two orders request identical amounts. - Remainder is deferred exactly, never dropped. For every allocation,
allocated_amount + deferred_amount == requested_amount. Sum this identity across the full order set and confirm it accounts for every cent of every request. - Zero-ceiling boundary. Feed a
disposable_earningsat or below the 30×-minimum-wage floor with no support order present; assertceiling == Decimal("0")and every order’sdeferred_amount == requested_amount. - Decimal determinism. Run the same input set three times and assert byte-identical
Decimalresults; passing afloatanywhere ingross_wages,required_withholding, orrequested_amountmust raise before allocation runs, not silently coerce.
Failure Modes
- Applying the ordinary 25%/30× cap when a support order is present. Root cause:
aggregate_ceilingalways evaluates the ordinary formula regardless of which order types are attached, so a child-support order’s higher § 1673(b) ceiling — 50% to 65% of disposable earnings — is silently clamped to the lower ordinary ceiling, systematically under-remitting court-ordered support. Remediation: branch on the presence of aCHILD_SUPPORTorder before selecting the ceiling formula, and add a regression test that asserts the support ceiling governs whenever one exists, independent of how many other orders are attached. - Sorting by ingestion order instead of statutory priority. Root cause: orders are processed in database insertion or arrival-timestamp order, so a creditor garnishment ingested before a later-served child-support order consumes headroom the support order needed first. Remediation: sort exclusively by
PRIORITY_BY_TYPE[order.order_type], and cover it with the shuffled-input regression test from Verification step 2. - Disposable earnings computed net of voluntary deductions. Root cause: the earnings-base calculation subtracts a 401(k) contribution or other voluntary election in addition to required withholding, shrinking disposable earnings below what 15 U.S.C. § 1672(a) defines and understating the ceiling for every attached order. Remediation: restrict the subtraction to amounts required by law to be withheld, and unit-test that adding a voluntary pre-tax deduction to a fixture never changes the computed
disposable_earnings.
When an order arrives with an unrecognized order_type, a gl_liability_account that fails to resolve, or a child-support order missing its support_ceiling_pct, the engine must not guess a priority or a ceiling — it routes the order exactly as described in Fallback Routing for Unclassified Deductions: quarantined pending manual classification, never posted at a default rate.
Frequently Asked Questions
Is a federal tax levy ever capped by the ordinary 25% garnishment limit?
No. A federal tax levy’s per-period withholding amount is computed independently, from the exempt-amount tables in IRS Publication 1494 under 26 U.S.C. § 6334, not from the CCPA’s 25%/30×-minimum-wage formula. This engine still sequences the levy at priority 2 and enforces one aggregate ceiling across every attached order — support percentage when a support order exists, otherwise the ordinary rule — because an employer still cannot disburse more in combined garnishments than disposable earnings and statute allow in a single pay period.
What happens to an order that gets partially or fully deferred this period?
It is not written off. The undisbursed amount becomes arrears against that specific order and must be carried into its requested_amount for the next pay period, so the obligee or creditor is eventually made whole once ceiling headroom exists — still subject to the same priority sequence, so a deferred support order continues to resolve ahead of a deferred creditor garnishment in every subsequent run.
Can two child-support orders on the same employee have different ceiling percentages?
Yes — 15 U.S.C. § 1673(b) sets the percentage per order based on that order’s own dependent-support and arrears facts (50%, 55%, 60%, or 65%). Because the CCPA ceiling is a per-employee cap rather than a per-order allowance, aggregate_ceiling takes the highest applicable percentage across all attached support orders as the single governing ceiling for that pay period, then allocates within it by priority as usual.
Why is requested_amount computed outside this engine instead of inside it?
Each order type has its own statutory formula for its per-period amount — a support-order percentage set by the issuing agency, the IRS Publication 1494 exempt-amount table for a federal levy, or the amount stated on a creditor’s writ — and folding those calculations into the allocator would blur ownership of each formula’s compliance surface. This stage’s only responsibility is prioritizing and capping already-known requested amounts across competing orders and mapping each to its general-ledger account.
Related
- Ordering Pre-Tax 401(k) and Section 125 Deductions — the earlier, pre-tax stage that must resolve before disposable earnings is computed for this post-tax allocation.
- Deduction Mapping Rules — the parent topic defining the canonical schema, jurisdiction resolution, and fallback chain this page’s engine plugs into.
- Fallback Routing for Unclassified Deductions — the quarantine path for any order with an unresolved type, account, or ceiling percentage.
- Enforcing Decimal Precision Across Payroll Fields — the platform-wide Decimal discipline this ceiling and allocation math depends on.