finance.accruals-prepayments@1.0.0
impl/typescript.ts
2,080 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 { daysBetween } from "./dates_days_between.ts"; ← from dates.days-between ^1.0.0 · built alongside by fune
import { overlapDays } from "./dates_overlap.ts"; ← from dates.overlap ^1.0.0 · built alongside by fune
import { allocate } from "./money_allocate.ts"; ← from money.allocate ^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 { type AccountingPeriod, type PeriodAllocation } from "./finance_accruals_prepayments_types.ts";
/**
* Split an invoice across accounting periods by days of service.
*
* money.allocate does the split with each period's days as the weight, so the
* shares add back to the invoice exactly. A negative amount is split as its
* positive and negated, so a credit note mirrors its invoice period by period.
*/
export function allocateToPeriods(
amount: Money,
serviceStart: string,
serviceEnd: string,
periods: readonly AccountingPeriod[],
): readonly PeriodAllocation[] {
const serviceDays = daysBetween(serviceStart, serviceEnd) + 1;
if (serviceDays < 1) {
throw new RangeError(`service ends ${serviceEnd}, before it starts ${serviceStart}`);
}
if (periods.length === 0) {
throw new RangeError("at least one accounting period is needed");
}
const days: number[] = [];
periods.forEach((period, index) => {
if (daysBetween(period.start, period.end) < 0) {
throw new RangeError(`period ${period.label} ends before it starts`);
}
if (index > 0 && daysBetween(periods[index - 1].end, period.start) < 1) {
throw new RangeError(`period ${period.label} overlaps or precedes the period before it`);
}
days.push(overlapDays(serviceStart, serviceEnd, period.start, period.end, true));
});
const covered = days.reduce((sum, d) => sum + d, 0);
if (covered !== serviceDays) {
throw new RangeError(`the accounting periods cover ${covered} of the ${serviceDays} days of service`);
}
const negative = amount.minor < 0;
const shares = allocate(money(negative ? -amount.minor : amount.minor, amount.currency), days);
return periods.map((period, index) => ({
label: period.label,
days: days[index],
amount: negative ? money(-shares[index].minor, amount.currency) : shares[index],
}));
}