Functional Weave
Code in Rust

subscriptions.usage-tiered

Price metered usage against tiers, graduated or volume, with an optional flat fee per tier.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 25 tests, run in TypeScript, Python and Rust.

What it does

Tiered pricing for metered usage, in the two modes Stripe calls `graduated` and `volume`, with Stripe's `up_to` and `flat_amount` semantics. With tiers of 1-5 at 7.00, 6-10 at 6.50 and 11+ at 6.00 (Stripe's own worked example):

- **graduated**: each unit is priced in the tier it falls in. 6 units are 5 x 7.00 + 1 x 6.50 = 41.50; 20 units are 127.50. - **volume**: every unit is priced at the tier the whole quantity falls in. 6 units are 6 x 6.50 = 39.00, which is less than 5 units at 35.00 would suggest by extrapolation: volume totals can fall as usage rises across a boundary, and that is correct.

For example

  • price_usage(1, tiers ×3, volume) → lines ×1, total $7.00 Stripe volume example: 1 font at 7.00
  • price_usage(5, tiers ×3, volume) → lines ×1, total $35.00 Stripe volume example: 5 fonts, the top of the first tier
  • price_usage(6, tiers ×3, volume) → lines ×1, total $39.00 Stripe volume example: 6 fonts, all at the second tier's 6.50

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

pub fn price_usage(quantity: i64, tiers: &[UsageTier], mode: &str) -> UsageCharge
quantityintunits used in the period, 0 or more
tiersUsageTier[]in ascending order; only the last may be open-ended
modeTierModegraduated: each unit at its own tier's price; volume: every unit at the price of the tier the total falls in
returnsUsageCharge

The types it declares, generated into your project

// TierMode is a string in Rust, one of: "graduated", "volume".
// Parameters take it as &str and results hold it as String.

/// One band of a tiered price, as Stripe's tiers spell it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UsageTier {
    /// the last unit in this tier, inclusive; null for an open-ended top tier
    pub up_to: Option<i64>,
    /// the price of each unit priced in this tier
    pub unit_price: Money,
    /// charged once when usage reaches this tier; zero for none
    pub flat_fee: Money,
}

/// What one tier contributed to the charge.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TierCharge {
    /// 1-based position in the tiers list
    pub tier: i64,
    /// units priced in this tier
    pub quantity: i64,
    pub unit_price: Money,
    pub flat_fee: Money,
    /// quantity x unitPrice + flatFee
    pub amount: Money,
}

/// The charge and the tiers it came from, ready to print as invoice lines.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UsageCharge {
    pub lines: Vec<TierCharge>,
    pub total: Money,
}

Your code names it in one line, in the file that uses it

fune!(subscriptions.usage-tiered@^1);  // then call price_usage(…)
impl/rust.rs · 123 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::money_amount::{assert_same_currency, money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune
use super::money_sum::sum_money;  ← from money.sum ^1.0.0 · built alongside by fune

fn charge(tier: i64, quantity: i64, t: &UsageTier) -> TierCharge {
    TierCharge {
        tier,
        quantity,
        unit_price: t.unit_price.clone(),
        flat_fee: t.flat_fee.clone(),
        amount: money(quantity * t.unit_price.minor + t.flat_fee.minor, &t.unit_price.currency),
    }
}

/// Price `quantity` units against ascending tiers. Graduated prices each unit
/// in its own tier and charges the flat fee of every tier reached; volume
/// prices every unit at the tier the quantity falls in, plus that tier's flat
/// fee. The first tier is always reached, so its flat fee applies at zero.
///
/// # Panics
/// Panics on a negative quantity, an unknown mode, no tiers, badly ordered
/// tiers, usage beyond a closed last tier, or mixed currencies.
pub fn price_usage(quantity: i64, tiers: &[UsageTier], mode: &str) -> UsageCharge {
    if quantity < 0 {
        panic!("quantity must be a whole number of 0 or more, received {}", quantity);
    }
    if mode != "graduated" && mode != "volume" {
        panic!("unknown tier mode \"{}\": expected graduated or volume", mode);
    }
    if tiers.is_empty() {
        panic!("tiered pricing needs at least one tier");
    }
    let currency = tiers[0].unit_price.currency.clone();
    let mut previous = 0;
    for (i, t) in tiers.iter().enumerate() {
        assert_same_currency(&tiers[0].unit_price, &t.unit_price);
        assert_same_currency(&tiers[0].unit_price, &t.flat_fee);
        match t.up_to {
            None => {
                if i != tiers.len() - 1 {
                    panic!("only the last tier may be open-ended (upTo null)");
                }
            }
            Some(up_to) => {
                if up_to <= previous {
                    panic!(
                        "tier upTo values must be positive and strictly increasing, received {} after {}",
                        up_to, previous
                    );
                }
                previous = up_to;
            }
        }
    }
    if let Some(end) = tiers[tiers.len() - 1].up_to {
        if quantity > end {
            panic!("quantity {} exceeds the last tier, which ends at {}", quantity, end);
        }
    }

    let mut lines: Vec<TierCharge> = Vec::new();
    if mode == "volume" {
        let index = tiers
            .iter()
            .position(|t| t.up_to.map_or(true, |end| quantity <= end))
            .unwrap();
        lines.push(charge(index as i64 + 1, quantity, &tiers[index]));
    } else {
        let mut floor = 0;
        for (i, t) in tiers.iter().enumerate() {
            // Tier 1 is always reached; a later tier only once usage passes
            // the previous tier's last unit.
            if i > 0 && quantity <= floor {
                break;
            }
            let top = t.up_to.map_or(quantity, |end| quantity.min(end));
            lines.push(charge(i as i64 + 1, top - floor, t));
            match t.up_to {
                None => break,
                Some(end) => floor = end,
            }
        }
    }
    let amounts: Vec<Money> = lines.iter().map(|l| l.amount.clone()).collect();
    let total = sum_money(&amounts, &currency);
    UsageCharge { lines, total }
}

pub fn usage_tier_from_value(v: &Value) -> UsageTier {
    UsageTier {
        up_to: if v.get("upTo").is_null() { None } else { Some(v.get("upTo").as_i64()) },
        unit_price: money_from_value(v.get("unitPrice")),
        flat_fee: money_from_value(v.get("flatFee")),
    }
}

pub fn usage_charge_to_value(c: &UsageCharge) -> Value {
    Value::obj(vec![
        (
            "lines",
            Value::Arr(
                c.lines
                    .iter()
                    .map(|l| {
                        Value::obj(vec![
                            ("tier", Value::Int(l.tier)),
                            ("quantity", Value::Int(l.quantity)),
                            ("unitPrice", money_to_value(&l.unit_price)),
                            ("flatFee", money_to_value(&l.flat_fee)),
                            ("amount", money_to_value(&l.amount)),
                        ])
                    })
                    .collect(),
            ),
        ),
        ("total", money_to_value(&c.total)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let tiers: Vec<UsageTier> = args[1].as_arr().iter().map(usage_tier_from_value).collect();
    usage_charge_to_value(&price_usage(args[0].as_i64(), &tiers, args[2].as_str()))
}

Install

fune build

With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 2 dependencies, pins them in fune.lock, downloads only the Rust package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:

fune add subscriptions.usage-tiered
Download for Rust subscriptions.usage-tiered-1.0.0-rust.fune · 30,397 bytes sha256 4066522a5d8ec60df6b8924cc0b3ef622926336614dd2c3a148a44861f0ab360

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./subscriptions.usage-tiered-1.0.0-rust.fune, or fetch it from a terminal with fune pull subscriptions.usage-tiered@1.0.0:rust.

The whole function, every language, is one file too: subscriptions.usage-tiered-1.0.0.fune, 36,389 bytes, sha256 a95d14505f62a99c7864d1763d9cb2cef7fb5c178ad87c07060a7fe269a41a97. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before subscriptions.usage-tiered

after — your function gets the result and the arguments, and returns the final result.

// fune: after subscriptions.usage-tiered

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

// fune: replace money.amount in subscriptions.usage-tiered
// fune: replace money.sum in subscriptions.usage-tiered

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show subscriptions.usage-tiered --steps.

// fune: step subscriptions.usage-tiered after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
Stripe volume example: 1 font at 7.00 1, tiers ×3, volume → lines ×1, total $7.00
Stripe volume example: 5 fonts, the top of the first tier 5, tiers ×3, volume → lines ×1, total $35.00
Stripe volume example: 6 fonts, all at the second tier's 6.50 6, tiers ×3, volume → lines ×1, total $39.00
Stripe volume example: 20 fonts, all at 6.00 20, tiers ×3, volume → lines ×1, total $120.00
Stripe volume example: 25 fonts 25, tiers ×3, volume → lines ×1, total $150.00
Stripe graduated example: 5 fonts 5, tiers ×3, graduated → lines ×1, total $35.00
Stripe graduated example: 6 fonts is 35.00 plus one at 6.50 6, tiers ×3, graduated → lines ×2, total $41.50
Stripe graduated example: 20 fonts 20, tiers ×3, graduated → lines ×3, total $127.50
Stripe graduated example: 25 fonts 25, tiers ×3, graduated → lines ×3, total $157.50
Stripe flat-rate example, volume: 12 x 3.00 + 30.00 12, tiers ×5, volume → lines ×1, total $66.00
Show the other 15 tests
CaseArgumentsExpected
Stripe flat-rate example, graduated: three tiers and three flat fees 12, tiers ×5, graduated → lines ×3, total $111.00
no usage still bills the first tier's flat fee, graduated 0, tiers ×5, graduated → lines ×1, total $10.00
no usage still bills the first tier's flat fee, volume 0, tiers ×5, volume → lines ×1, total $10.00
usage of exactly a tier's upTo does not reach the next tier or its flat fee 10, tiers ×5, graduated → lines ×2, total $75.00
one unit past a boundary reaches the next tier and its flat fee 11, tiers ×5, graduated → lines ×3, total $108.00
a free allowance, then 2p a unit, then 1p a unit 25,000, tiers ×3, graduated → lines ×3, total £330.00
a single open-ended tier is plain per-unit pricing 42, tiers ×1, graduated → lines ×1, total £10.50
a closed last tier prices usage right up to its end 200, tiers ×2, volume → lines ×1, total £10.00
usage past a closed last tier is an error 201, tiers ×2, graduated → error: exceeds the last tier
a negative quantity is an error -1, tiers ×3, graduated → error: quantity must be a whole number of 0 or more
no tiers is an error 5, , volume → error: at least one tier
tiers out of order are an error 5, tiers ×3, graduated → error: strictly increasing
an open-ended tier before the last is an error 5, tiers ×2, graduated → error: only the last tier may be open-ended
tiers in two currencies are an error 5, tiers ×2, graduated → error: currency mismatch
an unknown mode is an error 5, tiers ×3, stairstep → error: unknown tier mode

More from the author

A tier's `upTo` is the last unit in it, inclusive, so usage of exactly 10 against tiers ending at 5 and 10 is wholly inside the first two tiers and does not reach the third, flat fee included. The last tier may be open-ended (`upTo` null); if it is not, usage beyond it is an error rather than being silently priced at the last rate.

Flat fees. In graduated mode a tier's flat fee is charged once when usage reaches the tier; in volume mode only the flat fee of the tier the quantity falls in is charged. The first tier is always reached: usage of 0 still charges the first tier's flat fee, in both modes, which is what Stripe does ("Stripe always bills the first flat rate tier when quantity=0"). To charge nothing for no usage, make the first tier `upTo` 1 with a unit price instead of a flat fee.

The result lists one line per tier that was charged, in tier order, with the units priced in it, so an invoice can show the breakdown; `total` is their sum. Everything is exact integer arithmetic in minor units - no rounding happens anywhere. A per-unit price smaller than one minor unit (0.1p per API call) cannot be written as Money; meter such usage in packages (per 1,000 calls) instead.

Errors: no tiers, a negative quantity, `upTo` values that are not positive and strictly increasing, an open-ended tier before the last, usage beyond a closed last tier, and prices in more than one currency.

Source: Stripe, "Set up tiered pricing", https://docs.stripe.com/subscriptions/pricing-models/tiered-pricing (the vectors reproduce its volume, graduated and flat-rate examples).

Files

PathBytes
README.md2,226
impl/python.py2,984
impl/rust.rs4,621
impl/typescript.ts2,809
vectors.json17,155