Functional Weave
Code in TypeScript

property.completion-statement

A buyer's completion statement: price less deposit, ground rent and service charge apportioned to completion, plus fees.

1.0.0 (not the latest) · published 2026-10-03 by charlie · Anterra

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

Not professional advice. This capability calculates property figures from published rules. It is a software component for developers, not legal or financial advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed in its README, and have a conveyancer or tax adviser review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.

What it does

The figures on a buyer's completion statement for a property purchase: what has to be sent to the seller on the completion day, and what the buyer has to provide in total once their own costs are added.

price − deposit paid on exchange ± ground rent, service charge and other outgoings apportioned to completion = balance to the seller + the buyer's fees (legal fees, SDLT, searches, registration) = total due from the buyer

For example

  • completionStatement(£250,000.00, £25,000.00, 2025-06-30, apportionments ×2, fees ×2) → price £250,000.00, deposit paid £25,000.00, apportionments ×2, balance to seller £225,479.31, fees ×2, fees total £3,700.00, total due £229,179.31 a leasehold flat: service charge paid by the seller refunded, ground rent unpaid allowed, fees added
  • completionStatement(£300,000.00, £30,000.00, 2025-06-30, , ) → price £300,000.00, deposit paid £30,000.00, apportionments , balance to seller £270,000.00, fees , fees total £0.00, total due £270,000.00 a freehold with nothing to apportion and no fees
  • completionStatement(£150,000.00, £0.00, 2025-06-30, , fees ×1) → price £150,000.00, deposit paid £0.00, apportionments , balance to seller £150,000.00, fees ×1, fees total £350.00, total due £150,350.00 no deposit paid

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 completionStatement(price: Money, depositPaid: Money, completionDate: string, apportionments: readonly PeriodCharge[], fees: readonly FeeLine[]): CompletionStatement
priceMoneythe purchase price
depositPaidMoneythe deposit already paid on exchange, from zero to the price
completionDatedatethe day of actual completion; the seller is treated as owning the property until the end of it
apportionmentsPeriodCharge[]outgoings for a period that straddles (or adjoins) completion: ground rent, service charge, rent
feesFeeLine[]the buyer's own costs to be paid with the balance: legal fees, SDLT, searches, Land Registry
returnsCompletionStatement

The types it declares, generated into your project

/** An outgoing billed for a period, and whether the seller has already paid it. */
export interface PeriodCharge {
  readonly label: string;
  /** the charge for the whole period, zero or more */
  readonly amount: Money;
  /** first day the charge covers */
  readonly periodStart: string;
  /** last day it covers, inclusive */
  readonly periodEnd: string;
  /** true: the seller paid it, the buyer refunds the days after completion; false: the buyer will pay it, the seller allows the days up to completion */
  readonly paidBySeller: boolean;
}

/** One cost added to the amount the buyer must provide. */
export interface FeeLine {
  readonly label: string;
  /** zero or more */
  readonly amount: Money;
}

/** One outgoing, split at completion. */
export interface ApportionmentLine {
  readonly label: string;
  /** days of the period up to and including completion */
  readonly sellerDays: number;
  /** days after completion */
  readonly buyerDays: number;
  /** added to the balance: positive when the buyer refunds the seller, negative when the seller allows the buyer */
  readonly adjustment: Money;
}

/** The figures, in the order a completion statement shows them. */
export interface CompletionStatement {
  readonly price: Money;
  readonly depositPaid: Money;
  readonly apportionments: readonly ApportionmentLine[];
  /** price − deposit + every adjustment: the sum sent to the seller's solicitor */
  readonly balanceToSeller: Money;
  readonly fees: readonly FeeLine[];
  readonly feesTotal: Money;
  /** balance to the seller plus fees: what the buyer must provide */
  readonly totalDue: Money;
}

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

import { completionStatement } from "#fune/property.completion-statement@^1";
impl/typescript.ts · 69 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 { prorate } from "./finance_proration.ts";  ← from finance.proration ^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 ApportionmentLine, type CompletionStatement, type FeeLine, type PeriodCharge } from "./property_completion_statement_types.ts";

/**
 * A buyer's completion statement. Each outgoing is split at the end of the
 * completion day (the seller owns the property until then) by
 * finance.proration, so the two shares always add back to the charge.
 */
export function completionStatement(
  price: Money,
  depositPaid: Money,
  completionDate: string,
  apportionments: readonly PeriodCharge[],
  fees: readonly FeeLine[],
): CompletionStatement {
  const currency = price.currency;
  const same = (m: Money, what: string) => {
    if (m.currency !== currency) {
      throw new RangeError(`currency mismatch: ${what} is ${m.currency}, the price is ${currency}`);
    }
  };
  same(depositPaid, "depositPaid");
  if (!Number.isInteger(price.minor) || price.minor < 0) {
    throw new RangeError(`price must not be negative, received ${price.minor}`);
  }
  if (!Number.isInteger(depositPaid.minor) || depositPaid.minor < 0 || depositPaid.minor > price.minor) {
    throw new RangeError(`depositPaid must be between 0 and the price, received ${depositPaid.minor}`);
  }
  let balance = price.minor - depositPaid.minor;
  const lines: ApportionmentLine[] = [];
  for (const charge of apportionments) {
    same(charge.amount, `"${charge.label}"`);
    if (!Number.isInteger(charge.amount.minor) || charge.amount.minor < 0) {
      throw new RangeError(`"${charge.label}" must not be negative, received ${charge.amount.minor}`);
    }
    const totalDays = daysBetween(charge.periodStart, charge.periodEnd) + 1;
    if (totalDays < 1) {
      throw new RangeError(`"${charge.label}": periodEnd ${charge.periodEnd} is before periodStart ${charge.periodStart}`);
    }
    // The seller owns the property to the end of the completion day.
    const upToCompletion = daysBetween(charge.periodStart, completionDate) + 1;
    const sellerDays = Math.min(totalDays, Math.max(0, upToCompletion));
    const split = prorate(charge.amount, totalDays, sellerDays);
    const adjustment = charge.paidBySeller ? split.unused.minor : -split.used.minor;
    balance += adjustment;
    lines.push({ label: charge.label, sellerDays, buyerDays: totalDays - sellerDays, adjustment: money(adjustment, currency) });
  }
  let feesTotal = 0;
  const feeLines: FeeLine[] = [];
  for (const fee of fees) {
    same(fee.amount, `"${fee.label}"`);
    if (!Number.isInteger(fee.amount.minor) || fee.amount.minor < 0) {
      throw new RangeError(`"${fee.label}" must not be negative, received ${fee.amount.minor}`);
    }
    feesTotal += fee.amount.minor;
    feeLines.push({ label: fee.label, amount: money(fee.amount.minor, currency) });
  }
  return {
    price: money(price.minor, currency),
    depositPaid: money(depositPaid.minor, currency),
    apportionments: lines,
    balanceToSeller: money(balance, currency),
    fees: feeLines,
    feesTotal: money(feesTotal, currency),
    totalDue: money(balance + feesTotal, currency),
  };
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 3 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 property.completion-statement
Download for TypeScript property.completion-statement-1.0.0-typescript.fune · 25,208 bytes sha256 26a34e69e3a3cac1500946aa086a2c04cb8b1d6a8d498d1ab3eb4d5e3af974ce

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

The whole function, every language, is one file too: property.completion-statement-1.0.0.fune, 34,327 bytes, sha256 1f9cfa17eae6a2cd7677174082df16b88414d40d7064502744dcc963f5e46977. 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 property.completion-statement

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

// fune: after property.completion-statement

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 property.completion-statement
// fune: replace finance.proration in property.completion-statement
// fune: replace money.amount in property.completion-statement

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 property.completion-statement --steps.

// fune: step property.completion-statement 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 leasehold flat: service charge paid by the seller refunded, ground rent unpaid allowed, fees added £250,000.00, £25,000.00, 2025-06-30, apportionments ×2, fees ×2 → price £250,000.00, deposit paid £25,000.00, apportionments ×2, balance to seller £225,479.31, fees ×2, fees total £3,700.00, total due £229,179.31
a freehold with nothing to apportion and no fees £300,000.00, £30,000.00, 2025-06-30, , → price £300,000.00, deposit paid £30,000.00, apportionments , balance to seller £270,000.00, fees , fees total £0.00, total due £270,000.00
no deposit paid £150,000.00, £0.00, 2025-06-30, , fees ×1 → price £150,000.00, deposit paid £0.00, apportionments , balance to seller £150,000.00, fees ×1, fees total £350.00, total due £150,350.00
completion on the last day of the period: nothing to refund £200,000.00, £20,000.00, 2025-09-30, apportionments ×1, → price £200,000.00, deposit paid £20,000.00, apportionments ×1, balance to seller £180,000.00, fees , fees total £0.00, total due £180,000.00
completion on the first day: the seller keeps just that day £200,000.00, £20,000.00, 2025-01-01, apportionments ×1, → price £200,000.00, deposit paid £20,000.00, apportionments ×1, balance to seller £180,364.00, fees , fees total £0.00, total due £180,364.00
the next quarter paid in advance by the seller is refunded in full £200,000.00, £20,000.00, 2025-06-30, apportionments ×1, → price £200,000.00, deposit paid £20,000.00, apportionments ×1, balance to seller £180,920.00, fees , fees total £0.00, total due £180,920.00
unpaid arrears for a past quarter are allowed in full £200,000.00, £20,000.00, 2025-06-30, apportionments ×1, → price £200,000.00, deposit paid £20,000.00, apportionments ×1, balance to seller £179,500.00, fees , fees total £0.00, total due £179,500.00
a leap year: completion on 29 February is 60 of 366 days £200,000.00, £20,000.00, 2024-02-29, apportionments ×1, → price £200,000.00, deposit paid £20,000.00, apportionments ×1, balance to seller £179,940.00, fees , fees total £0.00, total due £179,940.00
£9.99 over two days splits 500p/499p, never 500p both ways £200,000.00, £20,000.00, 2025-06-30, apportionments ×1, → price £200,000.00, deposit paid £20,000.00, apportionments ×1, balance to seller £180,004.99, fees , fees total £0.00, total due £180,004.99
the whole price as deposit leaves only apportionments and fees £100,000.00, £100,000.00, 2025-06-30, , fees ×1 → price £100,000.00, deposit paid £100,000.00, apportionments , balance to seller £0.00, fees ×1, fees total £150.00, total due £150.00
Show the other 5 tests
CaseArgumentsExpected
a deposit above the price is refused £100,000.00, £100,000.01, 2025-06-30, , → error: depositPaid must be between 0 and the price
a fee in another currency is refused £100,000.00, £10,000.00, 2025-06-30, , fees ×1 → error: currency mismatch: "Legal fees" is EUR
a period that ends before it starts is refused £100,000.00, £10,000.00, 2025-06-30, apportionments ×1, → error: "Ground rent": periodEnd 2025-01-01 is before periodStart 2025-12-31
a negative fee is refused £100,000.00, £10,000.00, 2025-06-30, , fees ×1 → error: "Refund" must not be negative
a malformed completion date is refused £100,000.00, £10,000.00, 30/06/2025, apportionments ×1, → error: is not an ISO date

More from the author

## Apportionment

Outgoings such as ground rent and service charge are billed for a period that usually straddles the completion date. Each is split by days, following the Standard Conditions of Sale (fifth edition), condition 6.3: apportionment is made with effect from the date of actual completion, and the seller is treated as owning the property **until the end of the completion day**, with the sum accruing evenly from day to day across the period. So:

- seller's days = the period start up to and including the completion date; - buyer's days = the rest of the period.

If the seller has **already paid** the charge (`paidBySeller: true`), the buyer refunds the seller the buyer's days, which is added to the balance. If it is **not yet paid** (the buyer will receive and pay the bill), the seller allows the buyer the seller's days, which is taken off.

The split is made by `finance.proration` (with `money.allocate`), so the seller's and buyer's shares always add up to the charge exactly: a £9.99 charge over two days is 500p and 499p, never 500p twice.

A period that ends before completion is all the seller's (arrears not paid are allowed in full); a period that starts after completion is all the buyer's (an advance payment by the seller is refunded in full).

## What it does not do

- It uses a daily rate of the charge ÷ days in the period. Some contracts apportion annual sums on a 365-day year regardless of leap years, or treat the completion day as the buyer's: adjust `completionDate` or the period to match the contract. - Estimated service charges that are later balanced (SCS 6.3.5), rent arrears clauses, late completion interest and retentions are not calculated. - The mortgage advance is not netted off: the total due is what the buyer needs, from a lender and their own funds together. - Every amount must be in the price's currency.

## Source

Law Society, Standard Conditions of Sale (fifth edition, 2018 revision), condition 6.3 (apportionments).

Files

PathBytes
README.md2,486
impl/python.py3,218
impl/rust.rs5,570
impl/typescript.ts3,188
vectors.json12,246