86 lines
2.3 KiB
TypeScript
86 lines
2.3 KiB
TypeScript
import { Err, Ok, Result, SchemaError } from "@aigxion/error";
|
|
import { z } from "zod";
|
|
|
|
export const billingTier = z.object({
|
|
firstUnit: z.number().int().min(1),
|
|
lastUnit: z.number().int().min(1).nullable(),
|
|
/**
|
|
* in cents, e.g. "10.124" = $0.10124
|
|
* set null, to make it free
|
|
*/
|
|
centsPerUnit: z
|
|
.string()
|
|
.regex(/^\d{1,15}(\.\d{1,12})?$/)
|
|
.nullable(),
|
|
});
|
|
|
|
export type BillingTier = z.infer<typeof billingTier>;
|
|
|
|
type TieredPrice = {
|
|
tiers: (BillingTier & { quantity: number })[];
|
|
|
|
/**
|
|
* Here be dragons.
|
|
*
|
|
* DO NOT USE FOR BILLING
|
|
*
|
|
* We're doing floating point operatiuons here, so the result is likely not exact.
|
|
* Use this only for displaying estimates to the user.
|
|
*/
|
|
totalCentsEstimate: number;
|
|
};
|
|
/**
|
|
* calculateTieredPrice calculates the price for a given number of units, based on a tiered pricing model.
|
|
*
|
|
*/
|
|
export function calculateTieredPrices(
|
|
rawTiers: BillingTier[],
|
|
units: number,
|
|
): Result<TieredPrice, SchemaError> {
|
|
/**
|
|
* Validation logic:
|
|
*/
|
|
const parsedTiers = billingTier.array().min(1).safeParse(rawTiers);
|
|
if (!parsedTiers.success) {
|
|
return Err(SchemaError.fromZod(parsedTiers.error, rawTiers));
|
|
}
|
|
const tiers = parsedTiers.data;
|
|
|
|
for (let i = 0; i < tiers.length; i++) {
|
|
if (i > 0) {
|
|
const currentFirstUnit = tiers[i].firstUnit;
|
|
const previousLastUnit = tiers[i - 1].lastUnit;
|
|
if (previousLastUnit === null) {
|
|
return Err(new SchemaError("Every tier except the last one must have a lastUnit"));
|
|
}
|
|
if (currentFirstUnit > previousLastUnit + 1) {
|
|
return Err(new SchemaError("There is a gap between tiers"));
|
|
}
|
|
if (currentFirstUnit < previousLastUnit + 1) {
|
|
return Err(new SchemaError("There is an overlap between tiers"));
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
Calculation logic:
|
|
*/
|
|
let remaining = units; // make a copy, so we don't mutate the original
|
|
const res: TieredPrice = { tiers: [], totalCentsEstimate: 0 };
|
|
for (const tier of tiers) {
|
|
if (units <= 0) {
|
|
break;
|
|
}
|
|
|
|
const quantity =
|
|
tier.lastUnit === null ? remaining : Math.min(tier.lastUnit - tier.firstUnit + 1, remaining);
|
|
remaining -= quantity;
|
|
res.tiers.push({ quantity, ...tier });
|
|
if (tier.centsPerUnit) {
|
|
res.totalCentsEstimate += quantity * parseFloat(tier.centsPerUnit);
|
|
}
|
|
}
|
|
|
|
return Ok(res);
|
|
}
|