Manufacturer Rebate Contract Modeling

A manufacturer rebate contract is the pricing agreement that turns an adjudicated claim into money owed back to the plan, and modeling it as data — not as a spreadsheet a rebate analyst reads by eye — is what makes rebate accrual deterministic, replayable, and audit-defensible. Every contract carries a base rebate percentage, optional tiered or market-share bonuses, price-protection (CPI-U) ceilings, formulary-placement conditions, administrative-fee offsets, and hard effective/termination dates, and each of those components changes the per-claim rate the engine attributes at adjudication time. This page specifies the canonical Pydantic v2 model set that encodes those components, the resolution rules that map a contract to a single quantized decimal.Decimal rate for one claim, and the failure-mode taxonomy that keeps the attribution correct when contracts overlap, when a NDC-to-GPI crosswalk gap hides eligibility, or when a manufacturer files a retroactive amendment three months after the claim paid. It sits directly under Rebate Calculation & Accrual Tracking and feeds every downstream accrual, invoice, and reconciliation step.

Problem Framing

Rebate attribution is the point in the claims path where a paid, priced claim becomes a receivable. By the time a claim reaches this step it has already been normalized, resolved to a GPI, priced, and adjudicated to a paid outcome; what remains is to decide how much the manufacturer owes for that specific fill under the contract that governed the drug on the date it dispensed. That decision is entirely contract-driven: the same 407-D7 Product/Service ID (NDC) can earn a 23.5% base rebate under one agreement, an additional market-share bonus under a second clause, and nothing at all if a price-protection ceiling has been breached or the drug slipped out of its contracted formulary tier. Because the rebate is booked as an accrual months before the manufacturer invoices it, the attribution must be reconstructable to the penny during a payer or manufacturer audit — which means the contract cannot be a mutable row that an analyst edits in place. It has to be a versioned, immutable snapshot keyed on effective dates, exactly the discipline the WAC/AMP Spread Calculation step depends on when it computes the spread the rebate sits against. This page defines the prerequisites, the contract-component rule set, a production Pydantic v2 model, and the failure modes that make or break the accrual.

Prerequisites

Contract modeling is a consumer of the taxonomy and pricing layers, never an entry point. Before a contract can resolve a rate, the following must be in place:

  • A priced, GPI-resolved claim. The resolver reasons over therapeutic identity, not the labeler-specific NDC. The 407-D7 NDC is normalized and mapped through NDC-to-GPI Crosswalk Automation so that a contract written against a GPI class matches every therapeutic equivalent, and a contract written against a specific 11-digit NDC still resolves against the exact package. Eligibility keys are matched at both grains.
  • A version-stamped contract snapshot. Each contract is an immutable, dated record (for example contract.snapshot_id = "MFR0442-v2026.04"), never an editable table. The version that governs a claim is the one whose effective window contains the 401-D1 Date of Service, resolved by the rules on Handling Rebate Contract Effective Dating. This versioned-snapshot pattern is what makes an accrual replayable.
  • A resolved formulary placement. Formulary-placement conditions in a contract reference the tier and status the drug held on the service date, which come from Formulary Tier Mapping & Copay Calculation. A contract that pays a bonus only for preferred-tier placement needs that tier as an input, not an assumption.
  • Money as decimal.Decimal, end to end. Every rate, fee, WAC reference, and accrued amount is a decimal.Decimal, quantized explicitly. A rebate rate expressed as a float (0.235) accumulates representation error across millions of claims and will not reconcile against a manufacturer invoice; see the decimal module documentation.
  • An append-only audit sink. Every rate resolution — matched, degraded, price-protection-capped, or ineligible — is serialized with the snapshot_id and the resolved rate before the accrual is booked, satisfying the obligations in Security & Compliance Boundaries for Claims Data.

Contract Components and Their Adjudication Impact

A rebate contract is not one number; it is a stack of components that compose into a single rate. Each component is a first-class field on the model, and each has a precise, testable effect on what the engine attributes for a claim. The table below is the specification the model encodes.

Component Model field Resolves to Adjudication impact
Base rebate base_rate flat Decimal percent of WAC Floor rate applied to every eligible claim, before any bonus
Tiered / market-share rebate tiers: list[RebateTier] stepped Decimal bonus keyed on measured market share Adds a bonus rate once a share breakpoint is met; see Modeling Tiered Rebate Formulas in Python
Price protection (CPI-U) price_protection: PriceProtectionClause rate reduction or claim exclusion Caps rebate when a drug’s WAC rises faster than the CPI-U baseline since the reference date
Formulary-placement condition required_tier, required_status eligibility gate Zeroes the rebate if the drug was not on the contracted tier/status on 401-D1
Administrative fee admin_fee_pct Decimal offset Reduces the net rebate the plan retains; booked separately from gross accrual
Eligibility keys eligible_gpis, eligible_ndcs match set Determines whether the contract governs this 407-D7 at all
Effective dating effective_date, termination_date version selection Selects which contract version governs the service date

Two structural rules wrap the components. First, base and tiered rates compose additively against the WAC basis, but price protection and formulary-placement conditions are gates that can cap or zero the composed rate — never additive. Second, the administrative fee is applied to the gross rebate to yield the net the plan retains, and both figures are carried on the result so reconciliation can reason about gross-versus-net without recomputing. A contract that resolves to a rate must also emit which components fired, because an accrual that cannot explain its own rate is not auditable.

Reference Python Implementation

The model set below encodes a contract as three composed Pydantic v2 structures — RebateContract, RebateTier, and PriceProtectionClause — with decimal.Decimal rates, GPI/NDC eligibility keys, and a snapshot_id for versioned replay. The resolver takes an already-tokenized, GPI-resolved claim view and returns a single quantized per-claim rate plus the component trace. It never accepts or logs raw claim bytes: the caller passes the tokenized 402-D2 Prescription/Service Reference # and the resolved GPI, never 302-C2 Cardholder ID or 310-CA Patient Name.

python
import json
import logging
from decimal import Decimal, ROUND_HALF_UP
from datetime import date
from typing import Optional
from pydantic import BaseModel, ConfigDict, field_validator, model_validator

# Structured JSON logging for SIEM ingestion. PHI GUARDRAIL: only tokenized,
# non-PHI fields (resolved GPI, tokenized 402-D2 ref) reach this logger --
# never 302-C2 Cardholder ID, 310-CA Patient Name, or raw claim bytes.
logging.basicConfig(format="%(message)s", level=logging.INFO)
logger = logging.getLogger("rebate_contract_resolver")

CENT = Decimal("0.01")
RATE_Q = Decimal("0.00001")  # rates carried to 5 dp, quantized on output


class RebateTier(BaseModel):
    """One stepped market-share bonus band. Bonus applies at/above the
    breakpoint; bands must be sorted and non-overlapping on the parent."""
    model_config = ConfigDict(extra="forbid")

    share_floor: Decimal        # market-share breakpoint, e.g. Decimal("0.30")
    bonus_rate: Decimal         # additive bonus percent of WAC, e.g. Decimal("0.02")

    @field_validator("share_floor", "bonus_rate")
    @classmethod
    def _non_negative(cls, v: Decimal) -> Decimal:
        if v < 0:
            raise ValueError("tier rates and breakpoints must be non-negative")
        return v


class PriceProtectionClause(BaseModel):
    """CPI-U price-protection ceiling. If a drug's WAC has grown faster than
    CPI-U since reference_date, the rebate is reduced or the claim excluded."""
    model_config = ConfigDict(extra="forbid")

    reference_date: date
    reference_wac: Decimal          # WAC at reference_date
    cpi_u_reference: Decimal        # CPI-U index at reference_date
    exclude_on_breach: bool = False # True => zero rebate; False => cap at allowed

    def allowed_wac(self, current_cpi_u: Decimal) -> Decimal:
        """WAC ceiling the plan will still rebate against under CPI-U growth."""
        growth = (current_cpi_u / self.cpi_u_reference)
        return (self.reference_wac * growth).quantize(CENT, rounding=ROUND_HALF_UP)


class RebateContract(BaseModel):
    """Immutable, version-stamped manufacturer rebate contract."""
    model_config = ConfigDict(extra="forbid", frozen=True)

    snapshot_id: str                     # e.g. "MFR0442-v2026.04" -- audit key
    manufacturer_id: str
    base_rate: Decimal                   # flat percent of WAC, e.g. Decimal("0.235")
    admin_fee_pct: Decimal = Decimal("0")
    tiers: list[RebateTier] = []
    price_protection: Optional[PriceProtectionClause] = None
    required_tier: Optional[str] = None      # formulary-placement gate
    required_status: Optional[str] = None    # e.g. "preferred"
    eligible_gpis: frozenset[str] = frozenset()
    eligible_ndcs: frozenset[str] = frozenset()   # 407-D7, 11-digit canonical
    effective_date: date
    termination_date: Optional[date] = None

    @model_validator(mode="after")
    def _tiers_sorted_and_dated(self) -> "RebateContract":
        floors = [t.share_floor for t in self.tiers]
        if floors != sorted(floors) or len(set(floors)) != len(floors):
            raise ValueError("tiers must be strictly ascending by share_floor")
        if self.termination_date and self.termination_date < self.effective_date:
            raise ValueError("termination_date precedes effective_date")
        return self

    def governs(self, gpi: str, ndc_407d7: str) -> bool:
        """Eligibility at both grains: GPI class OR exact 11-digit NDC."""
        return gpi in self.eligible_gpis or ndc_407d7 in self.eligible_ndcs


class ClaimView(BaseModel):
    """Tokenized, GPI-resolved claim slice -- no PHI, no raw payload."""
    model_config = ConfigDict(extra="forbid")

    claim_ref: str            # tokenized 402-D2 reference (opaque, non-PHI)
    gpi: str                  # resolved 14-digit GPI
    ndc_407d7: str            # 407-D7, canonical 11-digit
    service_date: date        # 401-D1 Date of Service
    wac_unit: Decimal         # per-unit WAC basis for this claim
    quantity_442e7: Decimal   # 442-E7 Quantity Dispensed -- Decimal, never float
    formulary_tier: str
    formulary_status: str
    market_share: Decimal     # measured share for the tier decision


class RebateResult(BaseModel):
    gross_rate: Decimal
    net_rate: Decimal
    gross_amount: Decimal
    status: str               # matched | capped | ineligible | not_governed
    components_fired: list[str]
    snapshot_id: str


def resolve_tier_bonus(tiers: list[RebateTier], share: Decimal) -> Decimal:
    """Highest bonus whose share_floor <= measured share (stepped, not marginal)."""
    bonus = Decimal("0")
    for tier in tiers:                       # tiers validated ascending
        if share >= tier.share_floor:
            bonus = tier.bonus_rate
        else:
            break
    return bonus


def resolve_rebate(contract: RebateContract, claim: ClaimView,
                   current_cpi_u: Decimal) -> RebateResult:
    fired: list[str] = []

    # Gate 0: does this contract govern the claim's drug at all?
    if not contract.governs(claim.gpi, claim.ndc_407d7):
        return _finish(contract, claim, RebateResult(
            gross_rate=Decimal("0"), net_rate=Decimal("0"),
            gross_amount=Decimal("0.00"), status="not_governed",
            components_fired=fired, snapshot_id=contract.snapshot_id))

    # Gate 1: formulary-placement condition zeroes the rebate if unmet.
    if contract.required_tier and claim.formulary_tier != contract.required_tier:
        return _finish(contract, claim, RebateResult(
            gross_rate=Decimal("0"), net_rate=Decimal("0"),
            gross_amount=Decimal("0.00"), status="ineligible",
            components_fired=["formulary_gate"], snapshot_id=contract.snapshot_id))
    if contract.required_status and claim.formulary_status != contract.required_status:
        return _finish(contract, claim, RebateResult(
            gross_rate=Decimal("0"), net_rate=Decimal("0"),
            gross_amount=Decimal("0.00"), status="ineligible",
            components_fired=["formulary_gate"], snapshot_id=contract.snapshot_id))

    # Compose: base + stepped market-share bonus.
    rate = contract.base_rate
    fired.append("base_rate")
    bonus = resolve_tier_bonus(contract.tiers, claim.market_share)
    if bonus > 0:
        rate += bonus
        fired.append("market_share_bonus")

    wac_basis = claim.wac_unit

    # Gate 2: CPI-U price protection caps the WAC basis, or excludes the claim.
    if contract.price_protection is not None:
        allowed = contract.price_protection.allowed_wac(current_cpi_u)
        if claim.wac_unit > allowed:
            if contract.price_protection.exclude_on_breach:
                return _finish(contract, claim, RebateResult(
                    gross_rate=Decimal("0"), net_rate=Decimal("0"),
                    gross_amount=Decimal("0.00"), status="capped",
                    components_fired=fired + ["price_protection_exclude"],
                    snapshot_id=contract.snapshot_id))
            wac_basis = allowed          # rebate against the capped WAC only
            fired.append("price_protection_cap")

    gross_rate = rate.quantize(RATE_Q, rounding=ROUND_HALF_UP)
    net_rate = (gross_rate * (Decimal("1") - contract.admin_fee_pct)).quantize(
        RATE_Q, rounding=ROUND_HALF_UP)
    gross_amount = (gross_rate * wac_basis * claim.quantity_442e7).quantize(
        CENT, rounding=ROUND_HALF_UP)

    return _finish(contract, claim, RebateResult(
        gross_rate=gross_rate, net_rate=net_rate, gross_amount=gross_amount,
        status="matched", components_fired=fired,
        snapshot_id=contract.snapshot_id))


def _finish(contract: RebateContract, claim: ClaimView,
            result: RebateResult) -> RebateResult:
    # Audit event: tokenized ref + taxonomy identifiers only, no PHI.
    logger.info(json.dumps({
        "event": "rebate_rate_resolution",
        "claim_ref": claim.claim_ref,        # tokenized 402-D2, non-PHI
        "gpi": claim.gpi,
        "snapshot_id": result.snapshot_id,
        "status": result.status,
        "gross_rate": str(result.gross_rate),
        "net_rate": str(result.net_rate),
        "gross_amount": str(result.gross_amount),
        "components_fired": result.components_fired,
    }))
    return result

The resolver is a pure function of (contract, claim, cpi_u), so a booked accrual can be replayed against the exact snapshot_id that governed the claim and reproduce the identical rate to the fifth decimal place. The frozen=True config on RebateContract enforces immutability at the type level — an analyst who needs to change a rate must publish a new snapshot, which is what preserves the audit trail. The gross_amount is quantized to cents with ROUND_HALF_UP once, at the end, so the rate composition never rounds intermediate products.

How a Contract Resolves to a Per-Claim Rate

The decision tree below traces a single claim through the gates and the additive composition. Each terminal state emits exactly one audit event carrying the snapshot_id and the component trace.

Rebate contract resolution decision tree A GPI-resolved claim enters the resolver. First an eligibility gate checks whether the contract governs the claim's GPI class or exact NDC; if not, the result is not-governed with a zero rate. Next a formulary-placement gate checks the required tier and status; an unmet condition yields an ineligible zero rate. The engine then composes the base rate with a stepped market-share bonus. A CPI-U price-protection gate follows: if the claim WAC exceeds the allowed ceiling, the claim is either excluded with a zero rate or the WAC basis is capped. The composed rate, less the administrative fee, produces the net per-claim rebate marked matched. Every terminal state emits one audit event carrying the snapshot id and the fired components. GPI-resolved claim in Contract governs GPI / NDC? Formulary tier / status met? Compose base_rate + stepped market-share bonus WAC over CPI-U ceiling? Net rate = gross × (1 − fee) status matched · book accrual Zero rate · not_governed Zero rate · ineligible Exclude or cap · capped Emit audit event snapshot_id + components_fired no no over yes yes within

Figure: A GPI-resolved claim passes an eligibility gate, a formulary-placement gate, additive base-plus-bonus composition, and a CPI-U price-protection gate — each terminal state emitting one audit event stamped with the contract snapshot and the components that fired.

Engineering Constraints and Known Failure Modes

Contract modeling fails in specific, recurring ways. Each has a deterministic handling rule rather than an ad-hoc branch:

  • Overlapping contract terms. Two contracts — a legacy agreement and its successor — can both claim eligibility for the same GPI on the same service date because their effective windows overlap during a transition. Resolving to both double-counts the rebate; resolving to neither leaks it. The date-bracket resolution on Handling Rebate Contract Effective Dating selects exactly one governing version by service date and amendment sequence, and the resolver here refuses to run against an ambiguous set.
  • GPI eligibility gaps. A contract keyed on GPI class silently misses a claim whose NDC failed to resolve past a package-agnostic crosswalk hit, so the drug never matches eligible_gpis and the rebate is lost. The governs() check matches at both grains — GPI class or exact 407-D7 NDC — and a degraded crosswalk status is carried into the audit event so a rebate analyst can reconcile the gap rather than discover it at invoice time.
  • Retroactive amendments. A manufacturer files an amendment in June that raises the base rate effective the prior January. Every claim that already accrued at the old rate must be re-resolved against the new snapshot and the delta booked as a true-up, never edited in place. Because contracts are frozen and version-stamped, the original accrual and the amended one both survive in the audit trail, and the reconciliation feeds Rebate Accrual Reconciliation.
  • Float contamination of rates. A rate that enters the model as a Python float and is later coerced to Decimal carries the binary-representation error of the float, not the contract’s true 0.235. Every rate field is typed Decimal and constructed from a string; a float at the boundary is a data-quality defect, not a rounding nuisance.
  • Tier ordering drift. An unsorted or overlapping tier list makes the stepped bonus non-deterministic — the resolved bonus would depend on iteration order. The model_validator rejects any contract whose tiers are not strictly ascending by share_floor, so a malformed contract fails at load, not at adjudication.
  • PHI leakage in the accrual path. The rebate step is downstream of pricing and is tempting to debug with a full claim dict. Every log line carries the tokenized 402-D2 reference and taxonomy identifiers only — never 302-C2 Cardholder ID, 310-CA Patient Name, or raw transaction bytes.

Performance and Correctness Tuning

  • Load contracts by snapshot at worker start. Keep the active contract set resident in-process, indexed by (manufacturer_id, snapshot_id) and by an effective-date interval tree, so per-claim resolution is a bounded lookup rather than a database round-trip on the hot path.
  • Quantize once, at the boundary. Compose the rate at full Decimal precision and quantize only the final gross_amount to cents and the rates to five decimals. Quantizing intermediate products introduces drift that will not reconcile against a manufacturer invoice.
  • Idempotent, replayable attribution. Because the resolver is pure over (contract, claim, cpi_u), the tokenized 402-D2 reference doubles as an idempotency key: a re-adjudication after an amendment re-derives the rate deterministically and the true-up is the difference of two audited results.
  • Batch true-ups off the hot path. When an amendment lands, re-resolution of already-accrued claims runs through the concurrency model in Asynchronous Batch Adjudication Workflows, never inline with point-of-sale traffic.
  • Audit before booking. Serialize the resolution event with its snapshot_id and component trace to the append-only store before the accrual is booked, so any receivable can be reconstructed to the penny during a manufacturer or payer examination. NCPDP field semantics are defined in the official NCPDP Standards.

In This Section

  • Modeling Tiered Rebate Formulas in Python — how to represent flat, stepped, marginal, and blended market-share formulas so the resolved bonus rate is deterministic and auditable, using bisect over sorted breakpoints and monotonic-tier validation.
  • Handling Rebate Contract Effective Dating — resolving which contract version governs a claim by 401-D1 service date when effective/termination windows overlap, amendments land retroactively, and successors abut.

← Back to Rebate Calculation & Accrual Tracking