Files
cfw-autumn/shared/utils/billingUtils/invoicingUtils/lineItemUtils/graduatedTiersToLineAmount.ts
2026-02-23 13:52:26 +00:00

85 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Decimal } from "decimal.js";
import type { UsageTier } from "../../../../models/productModels/priceModels/priceConfig/usagePriceConfig";
import { Infinite } from "../../../../models/productModels/productEnums";
import { nullish } from "../../../utils";
import { roundUsageToNearestBillingUnit } from "../../usageUtils/roundUsageToNearestBillingUnit";
/**
* Core graduated tiered pricing calculation used across all billing contexts:
* included usage (free allowances), prepaid purchased quantities, and paid overage.
*
* Graduated pricing splits usage across tier bands — each band is charged at
* its own rate. For example, if tier 1 covers 0100 units at $1 and tier 2
* covers 100+ at $0.50, then 150 units costs (100 × $1) + (50 × $0.50) = $125.
*
* - **Included usage** (free allowance): pass `usage = includedQuantity`. The
* result represents the monetary value of the free bucket — used to compute
* how much of the prepaid charge is "used up" vs remaining.
* - **Prepaid quantity** (usage_in_advance): pass `usage = quantityPurchased`.
* The result is what the customer is charged upfront for the units they bought.
* - **Paid overage** (usage_in_arrear / pay-per-use): pass `usage = overageUnits`
* (raw usage minus any included or prepaid allowance). The result is the
* end-of-period charge for units consumed beyond the free/prepaid bucket.
*
* @param tiers - Ordered array of tier bands from the price config (`usage_tiers`).
* @param usage - The quantity to price. Meaning depends on context: purchased
* quantity for prepaid, overage units for arrear billing, or free-bucket size
* for included-usage valuation. Must be non-negative unless `allowNegative` is true.
* @param billingUnits - Divisor applied before multiplying by tier rate (e.g. 1000
* for "per 1k tokens"). Defaults to 1.
* @param allowNegative - When true, a negative `usage` is priced on its absolute
* value and the result is negated. Used for downgrade credits / proration refunds.
* Defaults to false.
* @returns The total dollar amount as a number rounded to 10 decimal places.
*/
export const graduatedTiersToLineAmount = ({
tiers,
usage,
billingUnits = 1,
allowNegative = false,
}: {
tiers: UsageTier[];
usage: number;
billingUnits?: number;
allowNegative?: boolean;
}): number => {
if (nullish(tiers)) {
throw new Error(
"[graduatedTiersToLineAmount] usage_tiers required for usage-based prices",
);
}
const isNegative = allowNegative && usage < 0;
const absoluteUsage = allowNegative ? Math.abs(usage) : usage;
const roundedUsage = roundUsageToNearestBillingUnit({
usage: absoluteUsage,
billingUnits,
});
let amount = new Decimal(0);
let remaining = new Decimal(roundedUsage);
let lastTierTo = 0;
for (const tier of tiers) {
if (remaining.lte(0)) break;
const isFinalTier = tier.to === Infinite || tier.to === -1;
const tierSize = isFinalTier
? remaining
: Decimal.min(remaining, new Decimal(tier.to).minus(lastTierTo));
const rate = new Decimal(tier.amount).div(billingUnits);
amount = amount.plus(rate.mul(tierSize));
remaining = remaining.minus(tierSize);
if (!isFinalTier) {
lastTierTo = tier.to as number;
}
}
const finalAmount = amount.toDecimalPlaces(10).toNumber();
return isNegative ? -finalAmount : finalAmount;
};