Skip to content

Settlement, Deduction & Financial Close

A modeled accrual is a promise, not a fact. In vendor rebate and trade promotion reconciliation, the number that matters to the business is not the one the engine computed upstream — it is the one that survives contact with the general ledger, with the retailer’s remittance advice, and with the auditor at quarter-end. Between the accrual and settled cash sits the least-forgiving stage of the whole system: customer deductions arrive as short-pays with no line-level explanation, accruals must post to a ledger that reverses cleanly on replay, the close calendar imposes a hard cutoff that freezes the numbers, and every incoming vendor payment has to be matched to an open accrual before anyone can call the money earned. When that stage is run on spreadsheets, the failures are expensive and structural: deductions written off because nobody could route them in time, ledgers double-posted by a re-run, accruals stranded open because a partial payment never matched, and a close that cannot prove which version of the numbers was signed.

This page is the settlement reference for that problem. It describes the entities that carry a posted accrual through to cleared cash, the idempotency guarantees that let posting and matching replay safely, the sequenced month-end close that turns a modeled figure into a locked period, the decimal netting math that offsets deductions against accruals to the cent, the exception taxonomy that classifies every short-pay, and the SOX-grade controls that make the whole thing defensible. It is the parent topic for five implementation areas — deduction routing, GL posting, close sequencing, payment reconciliation, and reporting automation — and it sits directly downstream of the core architecture / promotion mapping layer that emits accruals, the data ingestion normalization pipelines that deliver remittance and payment records, and the claim validation rule engine that adjudicated the claims now being settled. The audience is concrete: the Python developer who owns the posting job, the trade finance analyst who defends the close, and the deduction specialist who recovers unauthorized short-pays.

Canonical Data Model Overview

Settlement is only as auditable as the entities it moves money between. Once an accrual is posted, six record types have to resolve to stable, hashable identifiers and stay linked for the life of the liability: the accrual (the recognized liability inherited from upstream), the deduction (a customer or retailer short-pay taken against an invoice), the journal entry (the double-sided GL posting that recognizes or reverses the accrual), the remittance / payment (the vendor’s actual cash movement with its advice detail), the dispute (an open disagreement between modeled and settled amounts), and the close-run (the immutable envelope that stamps a period as frozen). The relationships between these are what the reconciliation traverses at period-end, so they are explicit and version-anchored rather than reconstructed from cash after the fact.

Settlement and financial close reference architecture A left-to-right settlement pipeline. Posted accruals enter from the upstream mapping layer and fan out into three parallel lanes: general-ledger posting, which writes idempotent double-sided journal entries; deduction and short-pay routing, which classifies retailer short-pays by reason code and sends them to recovery queues; and payment matching, which reconciles incoming vendor remittances against open accruals. All three lanes converge into a month-end close run that freezes accruals, applies retroactive tier true-ups, and locks the period. The locked period feeds reconciliation reporting. An append-only, hash-chained, signed audit log spans every stage from posting through reporting. Posted accruals from mapping layer write-once · rule_trace A GL posting idempotent double-entry post · reverse · true-up B Deduction routing short-pay reason codes recovery · SLA queues C Payment matching remittance → open accrual net · partial · unapplied Month-end close freeze · true-up lock · sign period Reconciliation reporting variance · export Append-only, hash-chained, signed audit log · spans posting → close → reporting

The accrual is the inherited anchor; settlement never re-derives it, it only recognizes, offsets, and clears it. Each journal entry references exactly one accrual and one close_run_id, so a posting is always attributable to the period that owns it — a reversal issued in a later period never silently mutates a prior close. Deductions carry a reason_code and a link to the invoice they were taken against, so a short-pay can be traced back to the promotion it claims to settle. Remittances carry advice lines that the matching lane resolves to open accruals; a payment that cannot be fully applied leaves an unapplied_amount on the record rather than being force-fit. The close-run is the immutable envelope: once sealed it stamps every entry, deduction, and match that fell inside its cutoff with a signature, and the numbers under that signature can never move.

The schema fields below are the minimum contract the settlement layer depends on. Treat them as a versioned interface — additive fields are backward-compatible, a retype is a breaking change that re-hashes the affected records. The field-level posting specification and reversal lineage live in accrual posting & GL integration.

Field Type Entity Constraint
accrual_id str (ULID) Accrual immutable, inherited from mapping
open_balance Decimal Accrual >= 0, 4 dp, decremented by matches
deduction_id str (ULID) Deduction immutable, primary key
reason_code str (enum) Deduction maps to routing queue + SLA
invoice_ref str Deduction links short-pay to source invoice
journal_id str (ULID) JournalEntry immutable, write-once
debit / credit Decimal JournalEntry balanced pair, 4 dp, never float
close_run_id str (ULID) CloseRun / JournalEntry period envelope reference
remittance_id str (ULID) Remittance immutable, carries advice lines
unapplied_amount Decimal Remittance >= 0; residual after matching
dispute_id str (ULID) Dispute opened on modeled-vs-settled delta
posting_key str (SHA-256) JournalEntry idempotency key, deterministic

Modeling the journal entry as a write-once record keyed by a deterministic posting_key — rather than a mutable ledger row — is what makes settlement replayable. A re-run of the posting job does not double-post; it recognizes that the key already exists and no-ops. A correction is a new reversing entry plus a fresh posting, never an in-place edit, so the ledger’s history of how a liability was recognized, offset, and cleared survives every close indefinitely.

Pipeline Design Principles

The settlement layer runs as a set of stages between accrual generation and cleared cash, and four properties make its output trustworthy: idempotent posting, deterministic keys, period isolation, and monetary exactness.

Idempotent posting. Re-running the posting job over the same accruals must leave the ledger byte-identical. This is the single hardest guarantee in settlement, because a naive retry after a partial failure is how hand-rolled systems double a liability. Every journal entry is keyed on a deterministic posting_key derived from the accrual, the entry type, and the close run; the writer performs an insert-if-absent against that key, so a replayed batch is a no-op rather than a duplication. Matching obeys the same discipline: applying a remittance to an accrual keys on (remittance_id, accrual_id) so replaying a payment file never over-applies cash.

python
import hashlib
from decimal import Decimal

def posting_key(accrual_id: str, entry_type: str, close_run_id: str) -> str:
    """Deterministic idempotency key for a journal entry. Same inputs always
    hash to the same key, so a replayed posting job inserts-if-absent and no-ops."""
    canonical = f"{accrual_id}|{entry_type}|{close_run_id}"
    return hashlib.sha256(canonical.encode("utf-8")).hexdigest()

Deterministic keys. Every fact that can change a posting is folded into a content hash — the accrual’s agreement_version, the entry type (accrue, reverse, true_up), and the owning close run. Persisting the key alongside the entry lets an auditor prove a given ledger line was produced by a specific accrual under a specific period, with no ambiguity. The same discipline deduplicates inbound remittances before they reach the matcher: a composite key over (payer, remittance_doc, advice_line) drops a re-delivered payment file so it cannot inflate cash applied.

Period isolation. A close run is a hard boundary. Once a period is frozen, no stage may write into it — a correction discovered after the freeze posts into the open period as a reversing-plus-new pair that references the original entry, never as a backdated edit. This is what lets a signed close survive audit: the numbers under the signature are immutable by construction, and the audit trail carries the full lineage of any later correction. Enforcing the freeze boundary is the responsibility of the month-end close sequencing stage; the posting layer treats any write targeting a sealed close_run_id as a hard reject.

Monetary exactness. Every figure the settlement layer touches — accrual balance, deduction amount, netted offset, journal debit and credit — is a decimal.Decimal with an explicit context and rounding mode. Float arithmetic introduces drift that is invisible on a single short-pay and material across a period’s netting run, and a ledger that does not balance to the cent will not pass. ASC 606 / IFRS 15 revenue-recognition treatment depends on the recognized number being reproducible exactly, so float is never permitted anywhere in the posting or netting path.

Rule Evaluation Architecture

The close is not a single job; it is an ordered sequence of dependent stages, and running them in the wrong order silently corrupts the period. Retroactive tier recalculation before the accrual freeze re-rates volume that is still moving; a GL post before the true-up locks in a figure the true-up was about to correct. The close is therefore modeled as a directed acyclic graph, where each stage is a gate on the next and the edges encode the only legal order. A DAG — rather than a runbook a human executes by hand — is what lets the controller prove the sequence ran completely, in order, and exactly once before the period is signed.

Month-end close sequencing DAG A left-to-right directed acyclic graph of the month-end close. Stage one freezes accruals at the period cutoff so no further movement is admitted. Stage two applies retroactive tier recalculation against the frozen volume. Stage three posts the resulting true-up delta as a balanced journal entry. Stage four writes the idempotent GL post that recognizes all accruals for the period. Stage five seals the close run, signs it, and produces the audit export. Each stage passes forward only on success; any failure halts the sequence and routes to the exception queue with the period left open rather than partially closed. advance on success halt — period stays open 1 Accrual freeze cutoff · no new movement snapshot balances 2 Retro re-rate retroactive tiers on frozen volume 3 True-up post delta journal entry balanced pair 4 GL post idempotent recognize posting_key insert 5 Seal & export sign close run audit export Exception queue · period held open any stage failure halts the DAG — no partial close is ever sealed

The sequence runs in five stages, each a gate on the next:

  1. Accrual freeze. At the period cutoff the layer snapshots every open accrual balance and admits no further movement into the period. A late-arriving transaction after the freeze belongs to the next period, not this one. Freezing first is what makes every downstream figure stable — nothing the close computes can shift underneath it.
  2. Retroactive tier recalculation. With volume frozen, any promotion whose breakpoint was crossed during the period is re-rated across its full earned volume. This runs against the snapshot, never against live data, so the re-rate is deterministic and reproducible. The full treatment of freeze-then-recalculate ordering lives in sequencing accrual freeze and retroactive recalculation.
  3. True-up post. The delta between the frozen accrual and the re-rated figure posts as a balanced journal entry — a positive true-up credits additional liability, a negative one reverses over-accrual. The entry is idempotent on its posting_key like every other posting.
  4. GL post. All recognized accruals for the period write to the ledger through the insert-if-absent path, so a re-run of the close cannot double-post.
  5. Seal and export. The close run is sealed, cryptographically signed, and its audit export generated. After this stage the period is immutable.

Precedence is explicit and total: a failure in any stage halts the DAG and leaves the period open rather than partially closed — there is no state in which some accruals are frozen and others are still moving under the same signature. Encoding the close as a DAG makes the order a property of the configuration, not of an analyst’s runbook, so changing the sequence is a reviewed edit rather than a tribal-knowledge risk.

python
from enum import Enum

class Stage(str, Enum):
    FREEZE = "accrual_freeze"
    RETRO = "retro_recalc"
    TRUE_UP = "true_up_post"
    GL_POST = "gl_post"
    SEAL = "seal_export"

CLOSE_DAG = [Stage.FREEZE, Stage.RETRO, Stage.TRUE_UP, Stage.GL_POST, Stage.SEAL]

def run_close(close_run, ctx) -> list[tuple[Stage, str]]:
    trace: list[tuple[Stage, str]] = []
    for stage in CLOSE_DAG:                 # strict topological order
        result = ctx.execute(stage, close_run)
        trace.append((stage, result.status))
        if result.status != "ok":
            ctx.hold_period_open(close_run)  # never seal a partial close
            break
    return trace

Financial Modeling & Accrual Generation

The settlement layer’s arithmetic is netting, not accrual — it takes the modeled liability inherited from payout structure modeling and offsets it against the deductions the retailer already took and the payments the vendor actually sent. Getting this wrong is the most common source of stranded balances: a deduction netted against the wrong accrual leaves one liability understated and another open forever. Netting is a matter of which offsets apply to which accrual, in what order, and to the cent.

All monetary arithmetic uses decimal.Decimal with an explicit context and rounding mode. A single netting run may offset dozens of partial deductions and payments against one accrual, and float drift across those additions is exactly what makes a ledger fail to balance. ASC 606 / IFRS 15 treatment requires the settled figure to be reproducible exactly, so Decimal with a fixed context and explicit ROUND_HALF_UP is used everywhere in the settlement path.

python
from dataclasses import dataclass
from decimal import Decimal, ROUND_HALF_UP, getcontext

getcontext().prec = 28
CENT = Decimal("0.0001")

@dataclass(frozen=True)
class Offset:
    kind: str            # "deduction" | "payment"
    amount: Decimal      # positive magnitude taken against the accrual

def net_settlement(accrual_balance: Decimal, offsets: list[Offset]) -> dict:
    """Net deductions and payments against an open accrual. Offsets apply in the
    order received; the residual is what remains open (or unapplied if negative)."""
    applied = Decimal("0")
    for o in offsets:
        applied += o.amount
    applied = applied.quantize(CENT, rounding=ROUND_HALF_UP)
    residual = (accrual_balance - applied).quantize(CENT, rounding=ROUND_HALF_UP)
    return {
        "applied": applied,
        "open_balance": max(Decimal("0"), residual),
        "unapplied": max(Decimal("0"), -residual),  # over-payment / over-deduction
    }

The figure the settlement layer clears is the modeled accrual net of validated offsets, not whatever a remittance happens to total. When a payment or deduction exceeds the open accrual, the excess becomes an unapplied residual routed for investigation — never silently absorbed. Each settlement record carries the accrual balance, every applied offset, and the resulting residual, so a finance analyst can reconstruct the netting line-by-line during the close. When an incoming vendor payment must be matched back to the open accruals it settles, the matching precedence and partial-application rules live in matching vendor payments to open accruals.

Exception Taxonomy & Routing

A retailer short-pay never arrives explained. A deduction shows up as a number missing from a payment, and the settlement layer’s job is to classify why — because a trade-promotion deduction that matches an open accrual is a settlement, while an unauthorized deduction is a recovery case worth chasing. Deductions and settlement mismatches are therefore a first-class output, classified into a stable taxonomy so that routing, priority, and SLA are driven by reason code rather than by an analyst’s guess. Each short-pay or mismatch exits with an explicit reason code, and the routing layer maps that code to a queue and a deadline.

Reason code Category Routing priority SLA target
TRADE_PROMO_DEDUCTION authorized settlement low — auto-net to accrual 5 business days
UNAUTHORIZED_DEDUCTION recovery high — chargeback candidate 2 business days
SHORTAGE_CLAIM logistics medium — needs proof of delivery 3 business days
PRICING_DISCREPANCY pricing medium — contract price mismatch 3 business days
DUPLICATE_DEDUCTION recovery high — same claim taken twice 2 business days
UNMATCHED_REMITTANCE cash application medium — payment, no open accrual 3 business days

When a deduction cannot be matched to an open accrual or a payment lands with no accrual to apply against, the layer does not strand the cash: it tags the record with the reason code, parks the residual as unapplied, and routes it to the queue its priority demands, so the batch keeps moving while the exception is worked. The recovery mechanics for short-pays that should never have been taken — chargeback packet assembly, proof-of-delivery attachment, and escalation — are owned by the deduction & short-pay routing cluster, with the specific unauthorized-deduction workflow detailed in routing unauthorized deductions for recovery. The settlement layer’s job is to emit the correct reason code and never to write off a recoverable deduction by default.

Classification itself is deterministic and testable — a reason code is a pure function of the deduction, the invoice it references, and the open accruals it could match, not a manual judgment:

python
from decimal import Decimal

def classify_deduction(ded, matched_accrual, open_accruals) -> str:
    if matched_accrual is not None:
        return "TRADE_PROMO_DEDUCTION"          # nets cleanly to a known accrual
    if ded.invoice_ref in {d.invoice_ref for d in open_accruals if d.settled}:
        return "DUPLICATE_DEDUCTION"            # invoice already fully settled
    if ded.claimed_qty is not None and ded.claimed_qty < ded.shipped_qty:
        return "SHORTAGE_CLAIM"                 # quantity short-pay
    if ded.unit_price is not None and ded.unit_price != ded.contract_price:
        return "PRICING_DISCREPANCY"
    return "UNAUTHORIZED_DEDUCTION"             # no basis found — pursue recovery

Governance, Audit Trail & Access Control

A settlement and close system is the system of financial record, so its governance posture has to satisfy SOX and survive external audit. Three controls are load-bearing.

Immutable, signed audit trail. Every posting, netting, deduction routing decision, and close-run seal writes an append-only log entry: input hashes, posting_key, close_run_id, the balanced debit/credit pair, and the resulting route. Entries are never updated. Each journal entry is cryptographically signed and the sequence within a close run is hash-chained, so any retroactive edit breaks the chain and surfaces immediately in reconciliation. This is what lets a signed period survive audit — the numbers under the signature are tamper-evident by construction, and a correction is always a new, chained entry rather than a rewrite.

python
import hmac, hashlib

def sign_entry(entry_bytes: bytes, prev_hash: str, key: bytes) -> str:
    """Hash-chain each journal entry to its predecessor, then sign. A retroactive
    edit breaks the chain because prev_hash no longer matches."""
    chained = prev_hash.encode("utf-8") + entry_bytes
    return hmac.new(key, chained, hashlib.sha256).hexdigest()

Segregation of duties and role-based access control. The person who configures a rule cannot post the accrual it produces, and the person who posts cannot seal the close — SOX requires these to be different roles, and the system enforces it structurally rather than by policy. ETL developers deploy posting and netting logic through the promotion pipeline but cannot mutate a posted journal entry; deduction specialists adjudicate and route short-pays but cannot alter the ledger; the close controller can seal a period but cannot edit the entries inside it. RBAC tags travel with every entity so authorization is enforced at the field level, and no single role can both create and approve the same financial movement.

Configuration as code with deterministic close controls. Posting rules, netting policy, and the close DAG live in Git with pull-request approval, automated linting, and environment promotion. A synthetic settlement corpus covering the hard cases — partial payments, over-deductions, negative true-ups, replayed remittance files, and a re-run close — executes on every change so a policy edit cannot regress a known boundary or break idempotency. The combination turns the close from a spreadsheet fire-drill into a proactive control: fewer stranded accruals, faster deduction recovery, and a period that closes on schedule with an audit trail that reconstructs every dollar of settled trade spend.

Frequently Asked Questions

Why key journal entries on a deterministic posting_key instead of an auto-increment id? A settlement job that retries after a partial failure will replay accruals it already posted. If entries were keyed on a database sequence, each replay would insert a fresh duplicate and double the liability. A posting_key derived from the accrual, the entry type, and the close run lets the writer insert-if-absent: a replayed batch recognizes the key already exists and no-ops, so the ledger is byte-identical no matter how many times the job runs.

How does the close stop a correction from mutating a period that was already signed? A sealed close run is a hard boundary. Any correction discovered after the freeze posts into the currently open period as a reversing entry plus a new posting that both reference the original, never as a backdated edit to the sealed period. Because each entry is hash-chained and signed within its close run, an in-place edit would break the chain and surface in reconciliation, so the numbers under a signature are immutable by construction.

Why must a deduction be classified before it is netted against an accrual? A short-pay that matches an open accrual is an authorized settlement, but an unauthorized or duplicate deduction is a recovery case worth chasing as a chargeback. If every short-pay were netted by default, recoverable money would be written off silently. Classifying by reason code first routes authorized deductions to auto-netting and everything else to the correct recovery queue with its own SLA, so no recoverable deduction is absorbed into an accrual.

What ordering does the month-end close require and why does it matter? The close runs as a fixed DAG: freeze accruals at the cutoff, apply retroactive tier recalculation against the frozen volume, post the true-up delta, write the idempotent GL post, then seal and sign the period. Running retroactive recalculation before the freeze would re-rate volume that is still moving, and posting before the true-up would lock in a figure the true-up was about to correct. Any stage failure halts the sequence and leaves the period open, so a partial close is never sealed.

Up one level: Home