Rebate Invoice Generation
Rebate invoice generation is the step that converts finalized rebate accruals into a manufacturer-facing bill: a deterministic roll-up of adjudicated utilization into invoice lines that a pharmaceutical manufacturer’s contracts team can validate, dispute at the line level, and pay. Where accrual estimates the money a plan expects to collect, invoicing asserts the claim in a form the counterparty can audit — each line tied to a signed contract, a therapeutic identifier, a summed dispensed quantity, a resolved rebate rate, and a period, with the member-identifying detail that produced it stripped before the file ever leaves the boundary. This page specifies how to build that subsystem in Python as an idempotent, versioned-snapshot process so the same adjudicated population always produces the same invoice, and so a line can be defended in a rebate audit two years after it was cut.
Operational Context
Invoicing sits at the tail of the rebate lifecycle described in Rebate Calculation & Accrual Tracking. Upstream, a claim is adjudicated, its 407-D7 Product/Service ID is resolved to a therapeutic class through the NDC-to-GPI crosswalk, the applicable manufacturer terms are attached by Manufacturer Rebate Contract Modeling, and a per-claim accrual is booked. Invoicing is the transition from accrued to invoiced: it selects the claims eligible for a billing period, aggregates them to the grain a manufacturer contract is written at, applies the rate that was resolved for each group, and emits both a human-readable invoice and a machine-readable utilization file — the industry data-file exchange a manufacturer’s rebate administrator loads to validate the bill.
The invoice is not an estimate and it is not reversible-in-place. Once a period is invoiced, corrections flow through Rebate Accrual Reconciliation as explicit true-up lines in the next cycle, never as silent edits to a bill already sent. That constraint — append-only, replayable, line-addressable — drives every design decision below. A rebate invoice that cannot be reproduced byte-for-byte from a versioned snapshot is not auditable, and a rebate program that is not auditable leaks money on both sides of the contract.
Problem Framing
The engineering problem is aggregation under three simultaneous constraints. First, determinism: given the same eligible-claim set, the same contract snapshot, and the same rate table, the builder must produce identical invoice lines and identical totals every run — sort order, dict iteration, and float drift are all disqualifying. Second, correctness of money: units summed across thousands of claims and multiplied by a per-unit rebate rate must use decimal.Decimal with explicit quantization, because a half-cent rounding error replicated across a large utilization population is a real reconciliation break. Third, PHI safety: the utilization file that ships to a manufacturer is an external disclosure, so it must carry only de-identified aggregate detail — never 302-C2 Cardholder ID or 310-CA Patient Name — under the same disclosure boundary enforced by Security & Compliance Boundaries for Claims Data.
Two of those constraints pull against each other in a way worth naming up front. Determinism wants a single, total ordering over the output; PHI safety wants the output to carry as little detail as possible. The resolution is the grain choice: aggregate to exactly the level the manufacturer contract is validated at, sort that level totally, and keep everything finer-grained behind the disclosure boundary. A manufacturer’s rebate administrator loads the utilization file, re-derives the units and amounts from its own copy of the contract, and accepts or disputes each line — so the file must expose enough to be re-footed independently and nothing more. That is why claim_count ships (it lets a manufacturer sanity-check line density against its own utilization estimates) while the claims themselves do not.
Prerequisites
Invoice generation is a consumer of finalized state, not a computation from raw claims. Before the builder runs, the following must hold:
- Finalized accruals for the period. Every claim in scope must have a booked, non-provisional accrual. A claim still in a pending or estimated accrual state is excluded from the invoice, not invoiced at a guessed rate — mixing provisional accruals into a manufacturer bill is the single fastest way to lose a rebate dispute. Accrual finalization is owned by Rebate Accrual Reconciliation.
- A versioned contract snapshot. The rebate terms — eligible GPIs, per-unit or percentage rates, tier breakpoints, effective dating — must be pinned to an immutable snapshot (for example
contract.version = "MFR-4471.v2026-Q2"). The builder resolves rates against the snapshot that was effective at each claim’s401-D1Date of Service, exactly as modeled in Manufacturer Rebate Contract Modeling. A live contract table read on the hot path makes an invoice unreproducible. - A resolved eligible-claim set. Eligibility is decided before aggregation: reversals netted out, non-covered products dropped, out-of-period dispensings excluded, and each surviving claim tagged with its resolved
contract_id, GPI, and rebate rate. This selection is the load-bearing input; the child guide, Generating Rebate Invoices from Adjudicated Claims, specifies it in full. - Decimal-native quantities and money.
442-E7Quantity Dispensed and every dollar figure enter the builder asdecimal.Decimal. Afloatunit or rate anywhere upstream poisons the summed total; see the Python decimal module for the quantization contract used throughout. - An append-only audit sink. Every invoice, and every line within it, is serialized to an immutable store keyed by invoice number and snapshot version before the file is released, so the bill can be reconstructed during a manufacturer examination.
- Library baseline. Python 3.11+,
pydantic>=2.6for strict line contracts,structlog>=24.1or the standard-libraryloggingmodule for JSON telemetry, and the standard-librarydecimalfor all money and unit math.
Invoice Line Specification
A rebate invoice is a set of lines plus a header. The header carries the manufacturer, the billing period, the contract snapshot version, and the deterministic invoice number. Each line is the atomic unit a manufacturer can accept or dispute, and it is written at the grain the contract is negotiated at — typically (contract_id, GPI, period), with the summed dispensed units and the resolved rate producing the line amount. The line schema is fixed:
| Field | Source | Type | Rule |
|---|---|---|---|
contract_id |
resolved contract snapshot | str |
Identifies the manufacturer agreement; part of the group-by key. |
gpi |
407-D7 → GPI crosswalk |
str (14-digit) |
Therapeutic identifier the rebate is negotiated on; part of the key. |
period |
billing cycle | str (YYYY-Qn / YYYY-MM) |
Closed billing period; part of the key. Never spans a contract-version boundary. |
units |
442-E7 Quantity Dispensed |
Decimal |
Deterministic sum of eligible dispensed units for the group. |
rebate_rate |
contract snapshot | Decimal |
Per-unit rate or resolved percentage; pinned to the effective snapshot. |
rebate_amount |
units × rebate_rate |
Decimal |
Quantized to 0.01, ROUND_HALF_UP; the billed figure. |
claim_count |
eligible-claim set | int |
Number of claims rolled into the line; a dispute-ready density signal. |
snapshot_version |
contract snapshot | str |
Pins the line to the exact terms; enables audit replay. |
The line intentionally carries claim_count but not the claims themselves in the manufacturer-facing artifact. Line-level claim detail lives in a separate, access-controlled dispute file that stays inside the PHI boundary; the manufacturer receives aggregate density, not member-level rows. This split is what lets a line be “dispute-ready” — a challenged line can be expanded to its constituent claims internally — without disclosing PHI in the external utilization file.
Figure: Adjudicated, accrual-finalized claims aggregate deterministically to invoice lines, receive a snapshot-pinned rebate rate, and export to a PHI-stripped utilization file under an idempotent invoice number.
Reference Python Implementation
The builder below aggregates an eligible-claim set into invoice lines deterministically, applies the snapshot rate in Decimal, assigns an idempotent invoice number, and emits a PHI-safe utilization export. It never accepts raw claim bytes; the caller passes already-selected, already-tokenized eligible claims. 302-C2 and 310-CA are structurally absent from the export model — the type system, not a runtime filter, is the primary PHI guard.
import json
import hashlib
import logging
from decimal import Decimal, ROUND_HALF_UP
from collections import defaultdict
from datetime import datetime, timezone
from pydantic import BaseModel, ConfigDict, field_validator
logging.basicConfig(format="%(message)s", level=logging.INFO)
logger = logging.getLogger("rebate_invoice")
CENTS = Decimal("0.01")
UNIT_Q = Decimal("0.001") # quantize summed 442-E7 units to 3 places
class EligibleClaim(BaseModel):
"""One accrual-finalized claim selected for invoicing.
PHI (302-C2 Cardholder ID, 310-CA Patient Name) is NOT a field here.
claim_token is an opaque, non-PHI reference for internal dispute lookup.
"""
model_config = ConfigDict(str_strip_whitespace=True, extra="forbid")
claim_token: str # tokenized 402-D2 ref (opaque, non-PHI)
contract_id: str # resolved manufacturer agreement
gpi: str # 407-D7 NDC resolved to 14-digit GPI
period: str # closed billing period, e.g. "2026-Q2"
units_442e7: Decimal # 442-E7 Quantity Dispensed -- Decimal, never float
rebate_rate: Decimal # per-unit rate from the contract snapshot
snapshot_version: str # pins the terms for audit replay
@field_validator("gpi")
@classmethod
def _gpi_shape(cls, v: str) -> str:
if not (v.isdigit() and len(v) == 14):
raise ValueError("gpi must be a 14-digit string")
return v
class InvoiceLine(BaseModel):
contract_id: str
gpi: str
period: str
units: Decimal
rebate_rate: Decimal
rebate_amount: Decimal
claim_count: int
snapshot_version: str
class RebateInvoice(BaseModel):
invoice_number: str
manufacturer_id: str
period: str
snapshot_version: str
lines: list[InvoiceLine]
total_units: Decimal
total_rebate: Decimal
generated_at: str
def _invoice_number(manufacturer_id: str, period: str, snapshot_version: str) -> str:
"""Deterministic, idempotent invoice number.
A re-run over the same population and snapshot yields the identical
number, so a retried job never double-bills a manufacturer.
"""
key = f"{manufacturer_id}|{period}|{snapshot_version}".encode("utf-8")
digest = hashlib.sha256(key).hexdigest()[:12].upper()
return f"RBT-{manufacturer_id}-{period}-{digest}"
class InvoiceBuilder:
"""Deterministic, snapshot-pinned rebate invoice builder."""
def build(
self,
manufacturer_id: str,
period: str,
snapshot_version: str,
claims: list[EligibleClaim],
) -> RebateInvoice:
# Deterministic aggregation: sum units per (contract_id, gpi, period).
# rate and version are captured per group and asserted invariant.
units: dict[tuple[str, str, str], Decimal] = defaultdict(lambda: Decimal("0"))
rates: dict[tuple[str, str, str], Decimal] = {}
counts: dict[tuple[str, str, str], int] = defaultdict(int)
for c in claims:
if c.snapshot_version != snapshot_version:
# A claim priced on a different snapshot cannot share this bill.
raise ValueError("snapshot version skew in eligible-claim set")
key = (c.contract_id, c.gpi, c.period)
units[key] += c.units_442e7
counts[key] += 1
prior = rates.setdefault(key, c.rebate_rate)
if prior != c.rebate_rate:
# Two rates for one group is a contract-resolution defect.
raise ValueError(f"rate conflict for group {key}")
# Sort keys so line order (and thus the file) is fully deterministic.
lines: list[InvoiceLine] = []
total_units = Decimal("0")
total_rebate = Decimal("0")
for key in sorted(units):
contract_id, gpi, grp_period = key
summed = units[key].quantize(UNIT_Q, rounding=ROUND_HALF_UP)
rate = rates[key]
amount = (summed * rate).quantize(CENTS, rounding=ROUND_HALF_UP)
lines.append(InvoiceLine(
contract_id=contract_id, gpi=gpi, period=grp_period,
units=summed, rebate_rate=rate, rebate_amount=amount,
claim_count=counts[key], snapshot_version=snapshot_version,
))
total_units += summed
total_rebate += amount
invoice = RebateInvoice(
invoice_number=_invoice_number(manufacturer_id, period, snapshot_version),
manufacturer_id=manufacturer_id,
period=period,
snapshot_version=snapshot_version,
lines=lines,
total_units=total_units.quantize(UNIT_Q, rounding=ROUND_HALF_UP),
total_rebate=total_rebate.quantize(CENTS, rounding=ROUND_HALF_UP),
generated_at=datetime.now(timezone.utc).isoformat(),
)
self._audit(invoice)
return invoice
def to_utilization_export(self, invoice: RebateInvoice) -> list[dict]:
"""PHI-safe utilization file rows for the manufacturer.
No 302-C2, no 310-CA, no claim_token -- aggregate detail only.
"""
return [{
"invoice_number": invoice.invoice_number,
"contract_id": ln.contract_id,
"gpi": ln.gpi,
"period": ln.period,
"units": str(ln.units), # Decimal crosses the wire as a string
"rebate_rate": str(ln.rebate_rate),
"rebate_amount": str(ln.rebate_amount),
"claim_count": ln.claim_count,
"snapshot_version": ln.snapshot_version,
} for ln in invoice.lines]
def _audit(self, invoice: RebateInvoice) -> None:
# Audit event: invoice identity + totals only, no PHI, no member rows.
logger.info(json.dumps({
"event": "rebate_invoice_generated",
"invoice_number": invoice.invoice_number,
"manufacturer_id": invoice.manufacturer_id,
"period": invoice.period,
"snapshot_version": invoice.snapshot_version,
"line_count": len(invoice.lines),
"total_rebate": str(invoice.total_rebate),
"timestamp": invoice.generated_at,
}))Three properties make this builder safe to run in production. It is idempotent: the invoice number is a pure function of manufacturer, period, and snapshot version, so a retried or replayed job reproduces the identical bill rather than a duplicate. It is deterministic: aggregation sorts its keys before emitting lines, so file byte-order never depends on dict insertion order or claim arrival sequence. And it is PHI-safe by construction: the to_utilization_export rows are assembled from a model that has no member fields to leak, so the export cannot carry 302-C2 or 310-CA even if a caller is careless.
The two guards inside the aggregation loop deserve emphasis because they convert silent financial errors into loud, run-time failures. The snapshot_version check refuses to fold a claim priced on different contract terms into the bill — without it, a late-arriving claim carrying last quarter’s rate would be summed into this quarter’s units and mispriced invisibly. The rate-conflict check refuses to accept two different rebate rates for one (contract_id, gpi, period) group. In a correct pipeline every claim in a group resolves to the same rate because the rate is a property of the contract snapshot, not the claim; a divergence therefore means the upstream rate resolution is wrong, almost always an effective-dating boundary error from handling rebate contract effective dating. Raising here surfaces that defect during the invoicing run, where it is cheap to fix, instead of in a manufacturer dispute months later, where it is not.
Note also that the builder does not price at all until the very last multiply. Units accumulate in Decimal at full precision, are quantized once per group to three decimal places, and only then multiply the rate to produce a cents-quantized amount. Quantizing units early, or carrying a running dollar total that is quantized on every claim, is the classic way a header total ends up a penny off the sum of its own lines — a footing break a manufacturer’s own reconciliation will catch and bounce.
Engineering Constraints & Known Failure Modes
Each failure below has a deterministic rule rather than a best-effort patch:
- Claim reversals after invoicing. A pharmacy reverses a fill after the period was invoiced. The invoiced line is not edited — the reversal is netted in the next cycle as a negative true-up through automating rebate true-up adjustments. Editing a sent bill in place destroys the audit trail and desynchronizes the manufacturer’s copy.
- Duplicate lines / duplicate claim inclusion. Re-running selection without netting reversals, or unioning two overlapping eligible sets, double-counts units. The eligible-claim set must be deduplicated on
claim_tokenbefore it reaches the builder, and the builder’s group-by is the last line of defense — it sums, it does not append. - Unit rounding drift. Summing raw
442-E7quantities asfloatand rounding at the end produces different totals on different platforms. Units are summed inDecimaland quantized once, per group, with an explicitROUND_HALF_UP— the rounding mode is part of the contract, not a default. - Rate conflict within a group. Two claims mapped to the same
(contract_id, gpi, period)but carrying different rebate rates signal a contract-resolution defect (usually an effective-dating error). The builder raises rather than silently picking one, so the defect surfaces in the run, not in a manufacturer dispute months later. - Snapshot skew. A claim priced against a different contract snapshot than the invoice is being cut on cannot share the bill; mixing versions makes the invoice unreproducible. The builder rejects version skew explicitly.
- PHI in the utilization export. The export is an external disclosure to the manufacturer. It carries aggregate units and amounts only — never
302-C2,310-CA, or even the internalclaim_token. Line-level claim detail for disputes stays behind the access boundary defined in Security & Compliance Boundaries for Claims Data.
Performance & Correctness Tuning
- Idempotency keys everywhere. The invoice number is the natural idempotency key; persist it uniquely so a re-submitted job is a no-op that returns the existing invoice rather than a second bill. The append-only audit event is keyed the same way.
- Decimal quantization discipline. Quantize units and amounts exactly once, at line construction, and let totals accumulate already-quantized line values — quantizing twice, or quantizing the total independently of its lines, is how a header total drifts a penny off the sum of its lines and fails a manufacturer’s own footing check.
- Versioned snapshots for replay. Persist the
snapshot_versionon every line so any invoice can be regenerated from the exact contract terms and rate table that were live at generation time; this is the same audit-replay discipline the WAC/AMP Spread Calculation subsystem relies on for its own numbers. - Batch aggregation, not streaming aggregation. Invoicing runs on a closed period, so it is a bounded batch job — read the whole eligible set, aggregate in memory, sort, and emit. There is no hot-path latency budget here; correctness and reproducibility dominate throughput.
- Audit before release. Serialize the invoice and its lines to the immutable store before the utilization file is transmitted, so a bill can always be reconstructed during a manufacturer examination. Consult the NCPDP standards for the utilization-file field conventions your manufacturer administrators expect.
In This Section
- Generating Rebate Invoices from Adjudicated Claims — the exact aggregation grain,
442-E7unit summation, rate application, and PHI-stripping decision for turning an adjudicated population into deterministic, dispute-ready invoice lines.
Related
- Rebate Accrual Reconciliation — finalizes the accruals this invoice bills and absorbs post-invoice reversals as true-up lines.
- Manufacturer Rebate Contract Modeling — supplies the versioned contract snapshot and resolved rebate rates each line is priced on.
- WAC/AMP Spread Calculation — the sibling money subsystem that shares this page’s Decimal-quantization and snapshot-replay discipline.
- Security & Compliance Boundaries for Claims Data — the PHI and disclosure boundary every utilization export must satisfy.
- NDC-to-GPI Crosswalk Automation — resolves the
407-D7NDC to the GPI each invoice line is keyed on.
← Back to Rebate Calculation & Accrual Tracking