Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { roundDiv, type RoundingMode } from "./math_round_div.ts"; ← from math.round-div ^1.0.0 · built alongside by fune
import { type Money, money } from "./money_amount.ts"; ← from money.amount ^1.0.0 · built alongside by fune
import { roundToIncrement } from "./time_round_to_increment.ts"; ← from time.round-to-increment ^1.0.0 · built alongside by fune
import { type FeeEarnerRate, type FeeEarnerTotal, type TimeBill, type TimeBillLine, type TimeEntry } from "./legal_time_billing_types.ts";
/** Price time entries in whole billing units at each fee earner's hourly rate. */
export function timeBilling(entries: readonly TimeEntry[], rates: readonly FeeEarnerRate[], unitMinutes: number, mode: RoundingMode): TimeBill {
if (rates.length === 0) {
throw new RangeError("rates must not be empty");
}
const currency = rates[0].hourlyRate.currency;
const card = new Map<string, Money>();
for (const rate of rates) {
if (card.has(rate.feeEarner)) {
throw new RangeError(`duplicate rate for fee earner "${rate.feeEarner}"`);
}
if (rate.hourlyRate.currency !== currency) {
throw new RangeError(`currency mismatch: ${currency} and ${rate.hourlyRate.currency}`);
}
if (rate.hourlyRate.minor < 0) {
throw new RangeError(`hourly rates must not be negative, received ${rate.hourlyRate.minor}`);
}
card.set(rate.feeEarner, rate.hourlyRate);
}
const lines: TimeBillLine[] = [];
const order: string[] = [];
const totals = new Map<string, { units: number; fee: number }>();
let units = 0;
let billedMinutes = 0;
let total = 0;
for (const entry of entries) {
const hourlyRate = card.get(entry.feeEarner);
if (hourlyRate === undefined) {
throw new RangeError(`no rate for fee earner "${entry.feeEarner}"`);
}
// Rounded per entry: seven 1-minute emails are seven units, not two.
const billed = roundToIncrement(entry.minutes, unitMinutes, mode);
const lineUnits = billed / unitMinutes;
const fee = roundDiv(billed * hourlyRate.minor, 60, "half-up");
lines.push({ feeEarner: entry.feeEarner, minutes: entry.minutes, billedMinutes: billed, units: lineUnits, hourlyRate, fee: money(fee, currency) });
let t = totals.get(entry.feeEarner);
if (t === undefined) {
t = { units: 0, fee: 0 };
totals.set(entry.feeEarner, t);
order.push(entry.feeEarner);
}
t.units += lineUnits;
t.fee += fee;
units += lineUnits;
billedMinutes += billed;
total += fee;
}
const byFeeEarner: FeeEarnerTotal[] = order.map((code) => {
const t = totals.get(code) as { units: number; fee: number };
return { feeEarner: code, units: t.units, fee: money(t.fee, currency) };
});
return { lines, byFeeEarner, units, billedMinutes, total: money(total, currency) };
}