professional.utilisation@1.0.0
impl/typescript.ts
1,931 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 { roundDiv } from "./math_round_div.ts"; ← from math.round-div ^1.0.0 · built alongside by fune
import { type Money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { type UtilisationRates } from "./professional_utilisation_types.ts";
function ratio(numerator: number, denominator: number): number | null {
// A ratio over nothing is not 0% or 100%: null says there was nothing to realise.
return denominator === 0 ? null : roundDiv(numerator * 10000, denominator, "half-up");
}
function checkMinutes(name: string, value: number): void {
if (!Number.isInteger(value) || value < 0) {
throw new RangeError(`${name} must be a non-negative integer, received ${value}`);
}
}
/** Utilisation and billing, collection and overall realisation, in basis points. */
export function utilisationRates(
billableMinutes: number,
availableMinutes: number,
standardValue: Money,
billedValue: Money,
collectedValue: Money,
): UtilisationRates {
checkMinutes("billableMinutes", billableMinutes);
checkMinutes("availableMinutes", availableMinutes);
if (availableMinutes === 0) {
throw new RangeError("availableMinutes must be greater than zero");
}
const amounts: [string, Money][] = [["standardValue", standardValue], ["billedValue", billedValue], ["collectedValue", collectedValue]];
for (const [name, amount] of amounts) {
if (amount.currency !== standardValue.currency) {
throw new RangeError(`currency mismatch: ${standardValue.currency} and ${amount.currency}`);
}
if (amount.minor < 0) {
throw new RangeError(`${name} must not be negative, received ${amount.minor}`);
}
}
return {
utilisationBasisPoints: roundDiv(billableMinutes * 10000, availableMinutes, "half-up"),
billingRealisationBasisPoints: ratio(billedValue.minor, standardValue.minor),
collectionRealisationBasisPoints: ratio(collectedValue.minor, billedValue.minor),
overallRealisationBasisPoints: ratio(collectedValue.minor, standardValue.minor),
};
}