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-D7NDC 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 the401-D1Date 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 adecimal.Decimal, quantized explicitly. A rebate rate expressed as afloat(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_idand 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.
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 resultThe 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.
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_gpisand the rebate is lost. Thegoverns()check matches at both grains — GPI class or exact407-D7NDC — 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
frozenand 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
floatand is later coerced toDecimalcarries the binary-representation error of the float, not the contract’s true0.235. Every rate field is typedDecimaland constructed from a string; afloatat 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_validatorrejects any contract whose tiers are not strictly ascending byshare_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-D2reference and taxonomy identifiers only — never302-C2Cardholder ID,310-CAPatient 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
Decimalprecision and quantize only the finalgross_amountto 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 tokenized402-D2reference 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_idand 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
bisectover sorted breakpoints and monotonic-tier validation. - Handling Rebate Contract Effective Dating — resolving which contract version governs a claim by
401-D1service date when effective/termination windows overlap, amendments land retroactively, and successors abut.
Related
- Rebate Calculation & Accrual Tracking — the parent area whose accrual and invoice steps consume the per-claim rate this model resolves.
- WAC/AMP Spread Calculation — computes the WAC/AMP basis the rebate rate is applied against, with the same
decimal.Decimaldiscipline. - Rebate Accrual Reconciliation — reconciles the accruals these contracts generate against manufacturer invoices and books true-ups.
- Formulary Tier Mapping & Copay Calculation — supplies the formulary tier and status that placement conditions gate on.
- NDC-to-GPI Crosswalk Automation — resolves the GPI eligibility key every contract match depends on.
← Back to Rebate Calculation & Accrual Tracking