Functional Weave
Code in TypeScript

finance.accruals-prepayments

Split an invoice across accounting periods by days of service, for accruals, prepayments and deferred income.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 17 tests, run in TypeScript, Python and Rust.

What it does

The matching principle, as arithmetic: an invoice for a service that runs over several accounting periods belongs to those periods in proportion to the days of service in each. A year's insurance paid in January is mostly a prepayment at the end of March; a quarter's rent billed in arrears is an accrual until the invoice arrives. This works out each period's share; which of those shares is the prepayment, accrual or deferred income at a balance-sheet date depends on when the invoice was booked, and is the caller's to read off.

Days are calendar days, both ends inclusive, and the split is by money.allocate with each period's days as its weight, so the shares always add back to the invoice to the minor unit. The odd units go to the largest remainders, ties to the earlier period. A year's 1,200.00 over four quarters of 90, 91, 92 and 92 days is 295.89, 299.18, 302.47 and 302.46, not four lots of 300.00 (which is what splitting by months would give).

For example

  • allocateToPeriods(£1,200.00, 2026-01-01, 2026-12-31, periods ×4) → ×4 a year's insurance by quarter: by days, not by months, and not a penny lost
  • allocateToPeriods(-£1,200.00, 2026-01-01, 2026-12-31, periods ×4) → ×4 a credit note mirrors its invoice period by period
  • allocateToPeriods(£3,100.00, 2026-12-15, 2027-01-14, periods ×2) → ×2 a month's service across a year end

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

export function allocateToPeriods(amount: Money, serviceStart: string, serviceEnd: string, periods: readonly AccountingPeriod[]): readonly PeriodAllocation[]
amountMoneythe invoice's net amount; negative for a credit note
serviceStartdatefirst day of the service the invoice covers
serviceEnddatelast day of the service, inclusive
periodsAccountingPeriod[]the accounting periods, in date order, together covering the whole service
returnsPeriodAllocation[]

The types it declares, generated into your project

/** One accounting period, by its first and last day. */
export interface AccountingPeriod {
  /** e.g. "2026-03" or "Q1" */
  readonly label: string;
  readonly start: string;
  /** inclusive */
  readonly end: string;
}

/** The part of the invoice that belongs to one period. */
export interface PeriodAllocation {
  readonly label: string;
  /** days of service falling in the period */
  readonly days: number;
  readonly amount: Money;
}

Your code names it in one line, in the file that uses it

import { allocateToPeriods } from "#fune/finance.accruals-prepayments@^1";
impl/typescript.ts · 49 lines · open · 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],
  }));
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 4 dependencies, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:

fune add finance.accruals-prepayments
Download for TypeScript finance.accruals-prepayments-1.0.0-typescript.fune · 15,633 bytes sha256 eefae201c0f7d8593e46e035a6eea1e3c93d64cc50ff3eab9908aa2098996948

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./finance.accruals-prepayments-1.0.0-typescript.fune, or fetch it from a terminal with fune pull finance.accruals-prepayments@1.0.0:typescript.

The whole function, every language, is one file too: finance.accruals-prepayments-1.0.0.fune, 21,194 bytes, sha256 98cd07e7a8663095d0e0f03e602e352103e186343e428b13014ec27501b3bad7. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before finance.accruals-prepayments

after — your function gets the result and the arguments, and returns the final result.

// fune: after finance.accruals-prepayments

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

// fune: replace dates.days-between in finance.accruals-prepayments
// fune: replace dates.overlap in finance.accruals-prepayments
// fune: replace money.allocate in finance.accruals-prepayments
// fune: replace money.amount in finance.accruals-prepayments

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show finance.accruals-prepayments --steps.

// fune: step finance.accruals-prepayments after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
a year's insurance by quarter: by days, not by months, and not a penny lost £1,200.00, 2026-01-01, 2026-12-31, periods ×4 → ×4
a credit note mirrors its invoice period by period -£1,200.00, 2026-01-01, 2026-12-31, periods ×4 → ×4
a month's service across a year end £3,100.00, 2026-12-15, 2027-01-14, periods ×2 → ×2
periods outside the service are listed with nothing in them £3,100.00, 2026-12-15, 2027-01-14, periods ×4 → ×4
leap-year February counts 29 days £600.00, 2024-02-01, 2024-03-31, periods ×2 → ×2
a single day of service £9.99, 2026-03-31, 2026-03-31, periods ×2 → ×2
odd pennies go to the largest remainders £1.00, 2026-01-01, 2026-01-03, periods ×3 → ×3
a 4-4-5 quarter £13,000.00, 2026-01-04, 2026-04-04, periods ×3 → ×3
periods with a gap outside the service are fine £10.00, 2026-03-10, 2026-03-19, periods ×2 → ×2
zero amount £0.00, 2026-01-01, 2026-12-31, periods ×4 → ×4
Show the other 7 tests
CaseArgumentsExpected
a service that ends before it starts is refused £1.00, 2026-03-10, 2026-03-09, periods ×4 → error: service ends 2026-03-09, before it starts 2026-03-10
no periods is refused £1.00, 2026-03-10, 2026-03-12, → error: at least one accounting period is needed
a period that ends before it starts is refused £1.00, 2026-03-10, 2026-03-12, periods ×1 → error: period bad ends before it starts
overlapping periods are refused £1.00, 2026-03-10, 2026-03-12, periods ×2 → error: period b overlaps or precedes the period before it
periods out of order are refused £1.00, 2026-03-10, 2026-03-12, periods ×2 → error: period Mar overlaps or precedes the period before it
days of service outside every period are refused £1.00, 2026-03-25, 2026-04-05, periods ×1 → error: the accounting periods cover 7 of the 12 days of service
an impossible date is refused £1.00, 2026-02-29, 2026-03-05, periods ×4 → error: not a real calendar date

More from the author

A credit note (a negative amount) is split exactly as the invoice it reverses and negated, so the two cancel period by period.

The periods are given, not assumed to be calendar months, because financial years and 4-4-5 calendars do not follow the calendar. They must be in date order, must not overlap, and together must cover every day of service; periods outside the service are allowed and get zero days and a zero amount, so a whole year of periods can be passed for any invoice. Each period appears in the result, in the order given.

Files

PathBytes
README.md1,535
impl/python.py2,069
impl/rust.rs3,299
impl/typescript.ts2,080
vectors.json7,762