Files
cfw-autumn/shared/utils/productUtils/priceToInvoiceAmount.ts
2026-02-25 19:39:10 +00:00

158 lines
4.5 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 { UsagePriceConfig } from "../../models/productModels/priceModels/priceConfig/usagePriceConfig.js";
import { BillingType } from "../../models/productModels/priceModels/priceEnums.js";
import type { Price } from "../../models/productModels/priceModels/priceModels.js";
import { Infinite } from "../../models/productModels/productEnums.js";
import {
type ProductItem,
UsageModel,
} from "../../models/productV2Models/productItemModels/productItemModels.js";
import { tiersToLineAmount } from "../billingUtils/invoicingUtils/lineItemUtils/tiersToLineAmount.js";
import { isPriceItem } from "../productV2Utils/productItemUtils/getItemType.js";
import {
calculateProrationAmount,
type Proration,
} from "../productV2Utils/productItemUtils/getProductItemRes.js";
import { nullish } from "../utils.js";
import { isFixedPrice } from "./priceUtils/classifyPriceUtils.js";
import { getBillingType } from "./priceUtils.js";
/**
* Prices an arbitrary quantity against a usage price's tier schedule.
* This is the single entry point for converting a raw unit count into dollars —
* it does NOT know whether that count represents included allowance, prepaid
* purchased units, or paid overage. The caller is responsible for passing the
* correct quantity for the billing context:
*
* - **Included usage** (free allowance): pass the size of the free bucket to
* find out its monetary value (used internally for proration math).
* - **Prepaid** (`UsageInAdvance`): pass the quantity the customer is buying
* upfront. The result is the immediate charge.
* - **Pay-per-use overage** (`UsageInArrear`): pass only the units consumed
* *above* any included free allowance — i.e. `totalUsage includedFree`.
* Do NOT pass raw total usage here; the caller must subtract the free tier first.
*
* @param price - The usage price whose config contains `usage_tiers` and
* optionally `billing_units`.
* @param quantity - The unit count to price. Must already be net of any free
* included allowance when called in an overage context.
* @returns Dollar amount as a number rounded to 10 decimal places.
*/
export const getAmountForQuantity = ({
price,
quantity,
}: {
price: Price;
quantity: number;
}) => {
const config = price.config as UsagePriceConfig;
const billingUnits = config.billing_units || 1;
return tiersToLineAmount({
price,
overage: quantity,
billingUnits,
});
};
export const itemToInvoiceAmount = ({
item,
quantity,
overage,
}: {
item: ProductItem;
quantity?: number;
overage?: number;
}) => {
let amount = 0;
if (isPriceItem(item)) {
amount = item.price!;
}
if (!nullish(quantity) && !nullish(overage)) {
throw new Error(
`itemToInvoiceAmount: quantity or overage is required, autumn item: ${item.feature_id}`,
);
}
const price = {
tier_behavior: item.tier_behavior,
config: {
usage_tiers: item.tiers || [
{
to: Infinite,
amount: item.price!,
},
],
billing_units: item.billing_units || 1,
},
} as unknown as Price;
if (item.usage_model === UsageModel.Prepaid) {
amount = getAmountForQuantity({ price, quantity: quantity! });
} else {
amount = getAmountForQuantity({ price, quantity: overage! });
}
return amount;
};
export const priceToInvoiceAmount = ({
price,
item,
quantity,
productQuantity,
overage,
proration,
now,
}: {
price?: Price;
item?: ProductItem;
quantity?: number; // quantity should be multiplied by billing units
productQuantity?: number;
overage?: number;
proration?: Proration;
now?: number;
}) => {
// 1. If fixed price, just return amount
let amount = 0;
if (price) {
if (isFixedPrice(price)) {
amount = new Decimal(price.config.amount)
.mul(productQuantity ?? 1)
.toNumber();
} else {
const config = price.config as UsagePriceConfig;
const billingType = getBillingType(config);
if (!nullish(quantity) && !nullish(overage)) {
throw new Error(
`getAmountForPrice: quantity or overage is required, autumn price: ${price.id}`,
);
}
if (billingType === BillingType.UsageInAdvance) {
amount = getAmountForQuantity({ price, quantity: quantity! });
} else {
amount = getAmountForQuantity({ price, quantity: overage! });
}
}
} else {
amount = itemToInvoiceAmount({ item: item!, quantity, overage });
}
if (proration) {
return calculateProrationAmount({
periodEnd: proration.end,
periodStart: proration.start,
now: now || Date.now(),
amount,
allowNegative: true,
});
}
return amount;
};