Automating Rebate True-Up Adjustments
A true-up is the closing move of reconciliation: once a manufacturer has invoiced and paid, the estimated rebate receivable that was accrued from adjudicated utilization has to be adjusted to the amount actually settled, and that adjustment has to post exactly once no matter how many times the period close runs. The exact decision this page settles is how to post that difference — as a reversing entry that unwinds the whole accrual and re-books the settled figure, as a signed delta that nudges the ledger by only the gap, or as a full restatement of a closed period — and how to guarantee the same true-up is never counted twice when a batch retries or a contract is amended retroactively. This is the terminal step of Rebate Accrual Reconciliation, consuming the variances classified by Reconciling Accrued vs Invoiced Rebates.
True-Up Posting Strategy Decision Matrix
The three posting strategies are not interchangeable; the right one depends on whether the affected period is still open, whether the GL permits back-dated entries, and how auditors expect the trail to read.
| Strategy | Mechanism | When to use | Double-count guard | Audit trail shape |
|---|---|---|---|---|
| Reversing entry | Reverse the full accrual, re-book the settled amount | Period still open; clean restate preferred | Idempotency key on (grain, close_run) | Two entries: reversal + rebook |
| Delta adjustment | Post only settled − already_posted |
Default; period open or soft-closed | Track cumulative posted; delta = target − posted | One signed entry per run |
| Full restatement | Reopen a closed period, restate the balance | Retroactive contract amendment materially changes a closed period | New restatement id; original left immutable | Restatement entry referencing the closed period |
The default is the delta adjustment: it posts only the difference between the settled target and what has already been booked, so re-running the close over unchanged inputs yields a delta of zero and posts nothing. The reversing entry is cleaner to read but writes two lines per true-up and is best reserved for open periods where a full unwind is acceptable. Full restatement is the heavyweight option — it reopens a closed period and must leave the original entries immutable, appending a restatement rather than editing history, because a closed GL period is an audit artifact you never mutate in place.
Figure: True-up posting timeline — each period trues the receivable toward the latest settled figure with a signed delta, while a retroactive amendment appends a restatement to the earlier closed period rather than editing it.
Step-by-Step Implementation
The poster below computes the signed decimal.Decimal delta between the settled target and what has already been booked for a reconciliation grain, posts only that delta, and records an audit event. It is period-safe and idempotent: a re-run over unchanged inputs computes a zero delta and posts nothing.
1. Establish the target and the already-posted total. The target is the settled amount (collected if present, else invoiced). The already-posted total is the running sum of prior true-up postings for this grain, read from the ledger — never assumed to be just the original accrual.
2. Compute the signed delta. delta = target − already_posted, quantized to cents. This is the only number that posts. A zero delta is a legitimate, common outcome and must post nothing.
3. Guard the period. If the target period is closed and the delta is non-zero, do not back-date into it; emit a restatement referencing the closed period instead, leaving original entries immutable.
4. Post idempotently and audit. Derive a deterministic posting key from the grain, the close run, and the delta so a retry dedupes on it. Emit a PHI-safe audit event carrying only tokenized and financial identifiers.
import json
import logging
import hashlib
from decimal import Decimal, ROUND_HALF_UP
from datetime import datetime, timezone
from typing import Optional
logging.basicConfig(format="%(message)s", level=logging.INFO)
logger = logging.getLogger("rebate_trueup") # PHI-safe: no 302-C2 / 310-CA / raw bytes
CENT = Decimal("0.01")
def _posting_key(contract_id: str, gpi_family: str, gl_period: str,
close_run: str, delta: Decimal) -> str:
basis = f"{contract_id}|{gpi_family}|{gl_period}|{close_run}|{delta}"
return hashlib.sha256(basis.encode("utf-8")).hexdigest()[:24]
def post_true_up(
*,
contract_id: str,
gpi_family: str,
gl_period: str,
close_run: str, # id of this period-close execution
settled_amount: Decimal, # collected if present, else invoiced
already_posted: Decimal, # cumulative prior true-ups for this grain
period_closed: bool,
) -> Optional[dict]:
target = settled_amount.quantize(CENT, ROUND_HALF_UP)
posted = already_posted.quantize(CENT, ROUND_HALF_UP)
delta = (target - posted).quantize(CENT, ROUND_HALF_UP)
if delta == 0:
# Nothing to true up -- idempotent no-op on re-run.
return None
if period_closed:
entry_type = "restatement" # append to closed period, never mutate
else:
entry_type = "delta"
key = _posting_key(contract_id, gpi_family, gl_period, close_run, delta)
entry = {
"posting_key": key,
"entry_type": entry_type,
"contract_id": contract_id,
"gpi_family": gpi_family,
"gl_period": gl_period,
"delta": str(delta),
"new_posted_total": str((posted + delta).quantize(CENT, ROUND_HALF_UP)),
"posted_at": datetime.now(timezone.utc).isoformat(),
}
logger.info(json.dumps({"event": "rebate_true_up", **entry}))
return entryBecause the delta is computed against the cumulative already-posted total rather than the original accrual, a second true-up in a later period stacks correctly: the running total moves toward the newest settled figure, and no prior posting is disturbed. The posting_key folds the delta into the hash, so a genuine second adjustment (a different delta) gets its own key while an accidental replay of the same delta collides and is deduped by the ledger writer.
Verification and Testing Pattern
The property that must hold above all others: re-running the true-up over unchanged inputs posts nothing. The tests also pin correct stacking and the closed-period restatement branch.
import pytest
def test_no_double_count_on_rerun():
kw = dict(contract_id="C1", gpi_family="27100010", gl_period="2026-06",
close_run="run-1", settled_amount=Decimal("800.00"),
already_posted=Decimal("1000.00"), period_closed=False)
first = post_true_up(**kw)
assert first["delta"] == "-200.00" # accrued 1000 -> settled 800
# Re-run once the delta is reflected in already_posted: no new posting.
kw2 = {**kw, "already_posted": Decimal("800.00")}
assert post_true_up(**kw2) is None # zero delta -> idempotent no-op
def test_second_period_stacks_toward_new_target():
entry = post_true_up(
contract_id="C1", gpi_family="27100010", gl_period="2026-06",
close_run="run-2", settled_amount=Decimal("760.00"),
already_posted=Decimal("800.00"), period_closed=False)
assert entry["delta"] == "-40.00"
assert entry["new_posted_total"] == "760.00"
def test_closed_period_posts_restatement():
entry = post_true_up(
contract_id="C1", gpi_family="27100010", gl_period="2026-03",
close_run="run-9", settled_amount=Decimal("910.00"),
already_posted=Decimal("1000.00"), period_closed=True)
assert entry["entry_type"] == "restatement"
assert entry["delta"] == "-90.00"The first test is load-bearing: it proves that once the delta is reflected in the posted total, the next run computes zero and returns None, so a retried or re-triggered period close cannot post the same adjustment twice.
Gotchas and PHI Guardrails
- Retroactive contract amendments. An amendment to Manufacturer Rebate Contract Modeling that changes a closed period must post as a restatement against a new snapshot version, not a silent re-price of the original accrual. Reconcile on the contract version stamped at accrual time and append the change.
- Closed-period posting. Never back-date a delta into a closed GL period. The
period_closedguard routes those to a restatement that references — but does not mutate — the original entries, preserving the audit trail. - Rounding residue. Repeated deltas can leave sub-cent residue if each is rounded independently. Quantize the target and posted totals with
ROUND_HALF_UPand take their difference, rather than accumulating rounded deltas, per the Python decimal documentation. - Delta against the wrong base. Computing the delta from the original accrual instead of the cumulative posted total double-counts every prior true-up. Always read the running posted total from the ledger.
- PHI in the audit event. A true-up entry carries only the contract id, GPI family, period, and amounts — never
302-C2Cardholder ID,310-CAPatient Name, or the tokenized407-D7-level claim payload. The reconciliation grain is already aggregated above the claim.
Trued-up receivables feed the settled figures that reconcile the ledger; for standard field definitions consult the official NCPDP Standards, and route batch true-up runs through the idempotency posture of Asynchronous Batch Adjudication Workflows.
Related
- Rebate Accrual Reconciliation — the reconciliation workflow whose closing step this true-up posting is.
- Reconciling Accrued vs Invoiced Rebates — the sibling step that classifies the variances this poster resolves.
- Manufacturer Rebate Contract Modeling — the versioned contract terms whose amendments trigger restatements.
- WAC/AMP Spread Calculation — the spread basis a restated true-up must reconcile against after a WAC/AMP revision.
- Rebate Invoice Generation — the invoiced figures that set the true-up target before cash is collected.
← Back to Rebate Accrual Reconciliation