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 lostallocateToPeriods(-£1,200.00, 2026-01-01, 2026-12-31, periods ×4)→ ×4 a credit note mirrors its invoice period by periodallocateToPeriods(£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[]
| amount | Money | the invoice's net amount; negative for a credit note |
| serviceStart | date | first day of the service the invoice covers |
| serviceEnd | date | last day of the service, inclusive |
| periods | AccountingPeriod[] | the accounting periods, in date order, together covering the whole service |
| returns | PeriodAllocation[] |
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";
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
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.
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Path | Bytes |
|---|---|
| README.md | 1,535 |
| impl/python.py | 2,069 |
| impl/rust.rs | 3,299 |
| impl/typescript.ts | 2,080 |
| vectors.json | 7,762 |