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:

DE=WgrossWrequiredDE = W_{\text{gross}} - W_{\text{required}}

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):

Cord=max(0, min(0.25DE,  DE30Wmin))C_{\text{ord}} = \max\!\Big(0,\ \min\big(0.25 \cdot DE,\ \ DE - 30 \cdot W_{\min}\big)\Big)

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:

Csupport=pDE,p{0.50, 0.55, 0.60, 0.65}C_{\text{support}} = p \cdot DE, \qquad p \in \{0.50,\ 0.55,\ 0.60,\ 0.65\}

CCPA ceiling, priority sequence, and GL mapping for competing garnishment orders Disposable earnings feed one CCPA aggregate ceiling: the child-support percentage of disposable earnings when a support order exists, otherwise the ordinary 25 percent / 30-times-minimum-wage rule. Four order types are laid out left to right in statutory priority — child support, federal tax levy, creditor garnishment, voluntary assignment — each showing its own general-ledger liability account, with short arrows passing remaining headroom from higher to lower priority. Beneath each order a segmented bar splits the requested amount into a posted portion and a deferred portion, and every deferred portion converges into one arrears box captioned as carrying forward to the next pay period, still ranked by the same priority. Disposable earnings (DE) gross wages − required withholding CCPA aggregate ceiling child-support % × DE — if a support order exists else min(25% × DE, DE − 30 × min wage) Child support priority 1 GL 2210 · Support Federal tax levy priority 2 GL 2220 · IRS Levy Creditor garnishment priority 3 GL 2230 · Garnishment Voluntary assignment priority 4 GL 2240 · Assignment posted deferred posted deferred posted deferred posted deferred Deferred amounts (arrears) carry to the next pay period — still ranked by the same priority
Disposable earnings feed one CCPA ceiling; orders resolve left to right in statutory priority, each posts to its own GL liability account, and any amount the ceiling cannot cover defers to next period's arrears.

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:

  1. 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.
  2. requested_amount is 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

  1. 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.
  2. Priority respected regardless of input order. Shuffle the orders list before calling allocate_garnishments and assert the allocation for each order_id is unchanged — the sort key is order_type, never list position or arrival timestamp.
  3. Per-order GL correctness. Assert every GarnishmentAllocation.gl_liability_account equals its source order’s gl_liability_account; no allocated amount may post against another order’s account even when two orders request identical amounts.
  4. 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.
  5. Zero-ceiling boundary. Feed a disposable_earnings at or below the 30×-minimum-wage floor with no support order present; assert ceiling == Decimal("0") and every order’s deferred_amount == requested_amount.
  6. Decimal determinism. Run the same input set three times and assert byte-identical Decimal results; passing a float anywhere in gross_wages, required_withholding, or requested_amount must raise before allocation runs, not silently coerce.

Failure Modes

  • Applying the ordinary 25%/30× cap when a support order is present. Root cause: aggregate_ceiling always 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 a CHILD_SUPPORT order 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.