Functional Weave
Code in Python

subscriptions.usage-tiered@1.0.0

impl/typescript.ts

2,809 bytes · the TypeScript implementation · view raw

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

import { assertSameCurrency, money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { sumMoney } from "./money_sum.ts";  ← from money.sum ^1.0.0 · built alongside by fune
import { type TierCharge, type TierMode, type UsageCharge, type UsageTier } from "./subscriptions_usage_tiered_types.ts";

function charge(tier: number, quantity: number, t: UsageTier): TierCharge {
  const amount = money(quantity * t.unitPrice.minor + t.flatFee.minor, t.unitPrice.currency);
  return { tier, quantity, unitPrice: t.unitPrice, flatFee: t.flatFee, amount };
}

/**
 * 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.
 */
export function priceUsage(quantity: number, tiers: readonly UsageTier[], mode: TierMode): UsageCharge {
  if (!Number.isInteger(quantity) || quantity < 0) {
    throw new RangeError(`quantity must be a whole number of 0 or more, received ${quantity}`);
  }
  if (mode !== "graduated" && mode !== "volume") {
    throw new RangeError(`unknown tier mode "${mode}": expected graduated or volume`);
  }
  if (tiers.length === 0) {
    throw new RangeError("tiered pricing needs at least one tier");
  }
  const currency = tiers[0].unitPrice.currency;
  let previous = 0;
  tiers.forEach((t, i) => {
    assertSameCurrency(tiers[0].unitPrice, t.unitPrice);
    assertSameCurrency(tiers[0].unitPrice, t.flatFee);
    if (t.upTo === null) {
      if (i !== tiers.length - 1) throw new RangeError("only the last tier may be open-ended (upTo null)");
    } else {
      if (!Number.isInteger(t.upTo) || t.upTo <= previous) {
        throw new RangeError(`tier upTo values must be positive and strictly increasing, received ${t.upTo} after ${previous}`);
      }
      previous = t.upTo;
    }
  });
  const last = tiers[tiers.length - 1];
  if (last.upTo !== null && quantity > last.upTo) {
    throw new RangeError(`quantity ${quantity} exceeds the last tier, which ends at ${last.upTo}`);
  }

  const lines: TierCharge[] = [];
  if (mode === "volume") {
    const index = tiers.findIndex((t) => t.upTo === null || quantity <= t.upTo);
    lines.push(charge(index + 1, quantity, tiers[index]));
  } else {
    let floor = 0;
    for (let i = 0; i < tiers.length; i++) {
      // Tier 1 is always reached; a later tier only once usage passes the
      // previous tier's last unit.
      if (i > 0 && quantity <= floor) break;
      const t = tiers[i];
      const top = t.upTo === null ? quantity : Math.min(quantity, t.upTo);
      lines.push(charge(i + 1, top - floor, t));
      if (t.upTo === null) break;
      floor = t.upTo;
    }
  }
  return { lines, total: sumMoney(lines.map((l) => l.amount), currency) };
}