Modeling Tiered Rebate Formulas in Python

The decision this page settles is how to represent a tiered or market-share rebate formula in Python so that the bonus rate a claim resolves to is deterministic, monotonic, and reproducible during an audit — not an artifact of iteration order or floating-point drift. A manufacturer contract rarely pays a single flat percent; it pays a base rate plus a bonus that steps up as the plan drives more market share toward the drug, and the shape of that step function is a contract term with real money attached. Choose the wrong representation and two things break: the same measured share resolves to different bonuses on different runs, and a reconciliation against the manufacturer’s own calculation fails by fractions of a cent that compound across millions of claims. This guide compares the four formula shapes that appear in production contracts, then implements the resolver with decimal.Decimal, a bisect over sorted breakpoints, and Pydantic v2 validation of tier monotonicity, as the tiered-rate component of Manufacturer Rebate Contract Modeling.

The Decision: Which Formula Shape

Market-share rebate formulas come in four shapes, and a contract states exactly one. The difference is not cosmetic — a cliff tier pays one rate on the entire volume once a breakpoint is crossed, while a marginal tier pays each rate only on the volume inside its band, and confusing the two misstates the rebate by the whole lower-band amount. The table below is the decision matrix.

Formula shape Resolved bonus Applies to Determinism risk Typical contract wording
Flat Single base_rate, no tiers All eligible volume None “23.5% of WAC on all units”
Stepped (cliff) One tier’s rate, chosen by highest breakpoint met All eligible volume Order-dependent if tiers unsorted “23.5%, rising to 25.5% at 30% share”
Marginal Sum of each band’s rate on the volume inside that band Volume per band Off-by-one at band edges “extra 2% on units above the 30% share threshold”
Blended Effective single rate = marginal total ÷ total volume All eligible volume Rounding drift if quantized early “blended effective rate across share bands”

The stepped-cliff shape is the most common and the default this implementation targets: it resolves to exactly one bonus rate, chosen as the rate of the highest tier whose share_floor the measured share meets or exceeds. Marginal and blended are variations on the same sorted-breakpoint structure — marginal integrates across bands, blended divides the marginal total back into an effective rate — so a single sorted, validated tier list serves all three, and only the reducer that walks it changes. Everything below keys on a strictly ascending list of (share_floor, bonus_rate) tiers, because that ordering is what makes bisect resolution O(log n) and, more importantly, deterministic.

Cliff versus marginal tier curves Two step functions plotted against market share on the horizontal axis and resolved bonus rate on the vertical axis, with breakpoints at thirty and fifty percent share. The cliff curve jumps the entire volume to the higher rate at each breakpoint, drawn as a rising staircase in cyan. The marginal curve pays each band's rate only on the volume inside that band, drawn in amber, so its effective resolved rate rises more gradually. The divergence between the two curves at any share level is the amount by which confusing cliff and marginal misstates the rebate. Market share Resolved bonus rate 30% 50% Cliff · whole-volume rate Marginal · per-band rate base +2% +4%

Figure: A cliff formula jumps the whole eligible volume to the higher rate at each breakpoint; a marginal formula pays each rate only on the volume inside its band, so its effective resolved rate rises more gradually — confusing the two misstates the rebate by the lower-band amount.

Step-by-Step Implementation

The resolver builds in four moves: validate the tiers are monotonic at load, sort the breakpoints for bisect, resolve the cliff bonus by breakpoint position, and expose marginal and blended reducers over the same structure. Money and rates are decimal.Decimal throughout.

1. Model the tiers and enforce strict monotonicity at load. A tier list that is unsorted or has duplicate breakpoints makes resolution non-deterministic. A Pydantic v2 model_validator rejects it before any claim is priced, so a malformed contract fails at ingestion, not at the pharmacy counter.

python
from decimal import Decimal, ROUND_HALF_UP
from bisect import bisect_right
from pydantic import BaseModel, ConfigDict, field_validator, model_validator

RATE_Q = Decimal("0.00001")   # rates carried to 5 dp
CENT = Decimal("0.01")


class RebateTier(BaseModel):
    model_config = ConfigDict(extra="forbid")
    share_floor: Decimal      # 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("share_floor and bonus_rate must be non-negative")
        return v


class TierSchedule(BaseModel):
    model_config = ConfigDict(extra="forbid", frozen=True)
    base_rate: Decimal
    tiers: tuple[RebateTier, ...] = ()

    @model_validator(mode="after")
    def _strictly_ascending(self) -> "TierSchedule":
        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")
        rates = [t.bonus_rate for t in self.tiers]
        if rates != sorted(rates):
            raise ValueError("bonus_rate must be non-decreasing across tiers")
        return self

2. Resolve the cliff bonus with bisect over sorted breakpoints. Precompute the breakpoint list once; bisect_right finds the index of the highest tier whose floor the share meets, in O(log n). The rate at that index is the whole-volume bonus.

python
def resolve_cliff_bonus(schedule: TierSchedule, market_share: Decimal) -> Decimal:
    """Highest tier whose share_floor <= measured share; whole-volume rate."""
    floors = [t.share_floor for t in schedule.tiers]     # ascending, validated
    idx = bisect_right(floors, market_share)             # count of floors <= share
    if idx == 0:
        return Decimal("0")                              # below the first breakpoint
    return schedule.tiers[idx - 1].bonus_rate

3. Add the marginal reducer over the same structure. Marginal integration pays each band’s rate only on the share volume inside that band. It walks the sorted tiers once, accumulating band_width * band_rate, and is the correct shape when a contract says “extra 2% on units above the threshold.”

python
def resolve_marginal_bonus(schedule: TierSchedule, market_share: Decimal) -> Decimal:
    """Sum of each band's rate over the share width inside that band."""
    total = Decimal("0")
    tiers = schedule.tiers
    for i, tier in enumerate(tiers):
        if market_share <= tier.share_floor:
            break
        upper = tiers[i + 1].share_floor if i + 1 < len(tiers) else market_share
        band_top = min(market_share, upper)
        band_width = band_top - tier.share_floor
        total += band_width * tier.bonus_rate
    return total.quantize(RATE_Q, rounding=ROUND_HALF_UP)

4. Derive the blended effective rate and compute the gross rebate. The blended rate divides the marginal total back into a single effective rate over the total volume; the gross rebate applies the composed rate to the WAC basis and 442-E7 Quantity Dispensed, quantized to cents once at the end.

python
def gross_rebate(schedule: TierSchedule, market_share: Decimal,
                 wac_unit: Decimal, quantity_442e7: Decimal) -> Decimal:
    # 442-E7 Quantity Dispensed -- Decimal, never float.
    rate = schedule.base_rate + resolve_cliff_bonus(schedule, market_share)
    rate = rate.quantize(RATE_Q, rounding=ROUND_HALF_UP)
    return (rate * wac_unit * quantity_442e7).quantize(CENT, rounding=ROUND_HALF_UP)

The gross rebate then flows into the contract resolver in Manufacturer Rebate Contract Modeling, which applies the formulary-placement and CPI-U price-protection gates before booking the accrual. The tier resolver never touches a claim payload directly — it receives the measured share and the WAC basis, so no 302-C2 Cardholder ID or raw NDC bytes are in scope here, and the resolved rate carries into the audit event alongside the tokenized 402-D2 reference the parent resolver logs.

Verification and Testing Pattern

Correctness means the resolver returns the same bonus for the same share regardless of tier input order, and that cliff and marginal shapes diverge exactly as the contract intends. Drive it with fixtures pinned to known breakpoints.

python
import pytest


@pytest.fixture
def schedule():
    return TierSchedule(
        base_rate=Decimal("0.235"),
        tiers=(
            RebateTier(share_floor=Decimal("0.30"), bonus_rate=Decimal("0.02")),
            RebateTier(share_floor=Decimal("0.50"), bonus_rate=Decimal("0.04")),
        ),
    )


def test_below_first_breakpoint_no_bonus(schedule):
    assert resolve_cliff_bonus(schedule, Decimal("0.10")) == Decimal("0")


def test_cliff_picks_highest_met_tier(schedule):
    assert resolve_cliff_bonus(schedule, Decimal("0.55")) == Decimal("0.04")


def test_cliff_at_exact_breakpoint_is_inclusive(schedule):
    # share == floor must meet the tier (>=), not miss it.
    assert resolve_cliff_bonus(schedule, Decimal("0.30")) == Decimal("0.02")


def test_marginal_diverges_from_cliff(schedule):
    share = Decimal("0.55")
    # marginal integrates: 0.20*0.02 + 0.05*0.04 = 0.006, far below cliff 0.04.
    assert resolve_marginal_bonus(schedule, share) == Decimal("0.00600")


def test_unsorted_tiers_rejected_at_load():
    with pytest.raises(ValueError):
        TierSchedule(
            base_rate=Decimal("0.20"),
            tiers=(
                RebateTier(share_floor=Decimal("0.50"), bonus_rate=Decimal("0.04")),
                RebateTier(share_floor=Decimal("0.30"), bonus_rate=Decimal("0.02")),
            ),
        )


def test_order_independence_of_gross(schedule):
    # Same inputs, deterministic gross to the cent.
    g1 = gross_rebate(schedule, Decimal("0.55"), Decimal("400.00"), Decimal("30"))
    g2 = gross_rebate(schedule, Decimal("0.55"), Decimal("400.00"), Decimal("30"))
    assert g1 == g2 == Decimal("3300.00")

The test_cliff_at_exact_breakpoint_is_inclusive case is the load-bearing one: bisect_right(floors, share) treats a share equal to a floor as meeting that tier, which is the contract-standard interpretation. Swapping to bisect_left would silently under-pay every claim that lands exactly on a breakpoint.

Gotchas and PHI Guardrails

  • Marginal versus cliff confusion. The single most expensive error is applying a cliff rate to a marginal contract or vice versa. Read the contract wording into the schedule type explicitly; a “rising to X% at Y share” clause is cliff, an “extra X% above Y” clause is marginal. Encode which reducer a contract uses in its snapshot_id so a reconciliation can prove which shape was applied.
  • Float drift in rate arithmetic. A bonus of 0.02 as a float is 0.0200000000000000004; multiplied by WAC and quantity across millions of claims it will not tie out to a manufacturer invoice. Every rate and money value is constructed from a string Decimal and quantized once, at the boundary — never mid-composition. See the decimal module documentation for ROUND_HALF_UP semantics.
  • Retroactive tier reclassification. Measured market share is often restated after the period closes, which can move a claim into a different tier. Never edit the booked bonus; re-resolve against the restated share and book the delta as a true-up, replayed against the same immutable TierSchedule snapshot the way Handling Rebate Contract Effective Dating replays contract versions.
  • Non-monotonic bonus rates. A contract whose higher tier pays a lower bonus is almost always a data-entry error; the model_validator rejects a non-decreasing rate sequence so the defect surfaces at load rather than as an unexplained accrual dip.
  • PHI stays upstream. This resolver operates on a measured share and a WAC basis, never a claim payload. Keep it that way: no 302-C2 Cardholder ID, 310-CA Patient Name, or raw NDC bytes enter the tier math, and only the tokenized 402-D2 reference travels with the resolved rate into the audit event, per Security & Compliance Boundaries for Claims Data.

← Back to Manufacturer Rebate Contract Modeling