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’s 401-D1 Date 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-E7 Quantity Dispensed and every dollar figure enter the builder as decimal.Decimal. A float unit 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.6 for strict line contracts, structlog>=24.1 or the standard-library logging module for JSON telemetry, and the standard-library decimal for 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.

Rebate invoice generation data flow Finalized, accrual-booked adjudicated claims flow left to right through four stages: deterministic aggregation by contract, GPI and period; application of the resolved rebate rate as a Decimal; construction of an auditable invoice line carrying summed units and quantized rebate amount; and a PHI-stripped utilization-file export. A guard band below the export stage marks where the 302-C2 Cardholder ID and 310-CA Patient Name are removed so only tokenized aggregate detail leaves the boundary. Prerequisite: finalized accrual + versioned contract snapshot Adjudicated claims accrual finalized Aggregate by contract · GPI · period Σ 442-E7 units Apply rebate rate (Decimal) from snapshot Invoice line + amount quantize 0.01 Utilization file export PHI-stripped PHI strip before external disclosure remove 302-C2 Cardholder ID · 310-CA Patient Name export carries tokenized aggregate detail only Idempotent invoice numbering number = f(manufacturer, period, snapshot_version) a re-run reproduces the identical invoice Every line and header is written to the append-only audit sink before the file is released.

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.

python
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_token before 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-E7 quantities as float and rounding at the end produces different totals on different platforms. Units are summed in Decimal and quantized once, per group, with an explicit ROUND_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 internal claim_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_version on 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

← Back to Rebate Calculation & Accrual Tracking