Functional Weave
Code in Rust

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],
  }));
}