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.
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.
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.
The sequence runs in five stages, each a gate on the next:
- 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.
- 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.
- 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_keylike every other posting. - 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.
- 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.
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.
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:
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.
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.
Related
- Deduction & Short-Pay Routing — classify retailer short-pays by reason code and route unauthorized deductions to recovery.
- Accrual Posting & GL Integration — write idempotent, balanced journal entries that reverse and replay cleanly.
- Month-End Close Sequencing — order accrual freeze, retroactive re-rate, true-up, and GL post as a controlled DAG.
- Payment & Settlement Reconciliation — match incoming vendor remittances to open accruals with partial and unapplied handling.
- Reconciliation Reporting Automation — generate period-end variance and audit-export reports from the sealed close.
- Core Architecture & Promotion Mapping — the upstream layer that emits the write-once accruals this stage settles.
- Data Ingestion & Normalization Pipelines — the upstream stage that delivers typed remittance and payment records.
- Claim Validation & Rule Engine Configuration — the engine that adjudicated the claims now flowing into settlement.
Up one level: Home