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
completion_statement(£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 addedcompletion_statement(£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 feescompletion_statement(£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.
pub fn completion_statement(price: &Money, deposit_paid: &Money, completion_date: &str, apportionments: &[PeriodCharge], fees: &[FeeLine]) -> CompletionStatement
| price | Money | the purchase price |
| deposit_paid | Money | the deposit already paid on exchange, from zero to the price |
| completion_date | date | the day of actual completion; the seller is treated as owning the property until the end of it |
| apportionments | PeriodCharge[] | outgoings for a period that straddles (or adjoins) completion: ground rent, service charge, rent |
| fees | FeeLine[] | the buyer's own costs to be paid with the balance: legal fees, SDLT, searches, Land Registry |
| returns | CompletionStatement |
The types it declares, generated into your project
/// An outgoing billed for a period, and whether the seller has already paid it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PeriodCharge {
pub label: String,
/// the charge for the whole period, zero or more
pub amount: Money,
/// first day the charge covers
pub period_start: String,
/// last day it covers, inclusive
pub period_end: 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
pub paid_by_seller: bool,
}
/// One cost added to the amount the buyer must provide.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FeeLine {
pub label: String,
/// zero or more
pub amount: Money,
}
/// One outgoing, split at completion.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ApportionmentLine {
pub label: String,
/// days of the period up to and including completion
pub seller_days: i64,
/// days after completion
pub buyer_days: i64,
/// added to the balance: positive when the buyer refunds the seller, negative when the seller allows the buyer
pub adjustment: Money,
}
/// The figures, in the order a completion statement shows them.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CompletionStatement {
pub price: Money,
pub deposit_paid: Money,
pub apportionments: Vec<ApportionmentLine>,
/// price − deposit + every adjustment: the sum sent to the seller's solicitor
pub balance_to_seller: Money,
pub fees: Vec<FeeLine>,
pub fees_total: Money,
/// balance to the seller plus fees: what the buyer must provide
pub total_due: Money,
}
Your code names it in one line, in the file that uses it
fune!(property.completion-statement@^1); // then call completion_statement(…)
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
use super::funejson::Value; ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_days_between::days_between; ← from dates.days-between ^1.0.0 · built alongside by fune
use super::finance_proration::prorate; ← from finance.proration ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_from_value, money_to_value, Money}; ← from money.amount ^1.0.0 · built alongside by fune
/// 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.
///
/// # Panics
/// Panics on mixed currencies, a negative price, charge or fee, a deposit
/// outside 0..=price, a malformed date, or a period that ends before it starts.
pub fn completion_statement(
price: &Money,
deposit_paid: &Money,
completion_date: &str,
apportionments: &[PeriodCharge],
fees: &[FeeLine],
) -> CompletionStatement {
let currency = price.currency.as_str();
let same = |m: &Money, what: &str| {
if m.currency != currency {
panic!("currency mismatch: {} is {}, the price is {}", what, m.currency, currency);
}
};
same(deposit_paid, "depositPaid");
if price.minor < 0 {
panic!("price must not be negative, received {}", price.minor);
}
if deposit_paid.minor < 0 || deposit_paid.minor > price.minor {
panic!("depositPaid must be between 0 and the price, received {}", deposit_paid.minor);
}
let mut balance = price.minor - deposit_paid.minor;
let mut lines: Vec<ApportionmentLine> = Vec::new();
for charge in apportionments {
same(&charge.amount, &format!("\"{}\"", charge.label));
if charge.amount.minor < 0 {
panic!("\"{}\" must not be negative, received {}", charge.label, charge.amount.minor);
}
let total_days = days_between(&charge.period_start, &charge.period_end) + 1;
if total_days < 1 {
panic!(
"\"{}\": periodEnd {} is before periodStart {}",
charge.label, charge.period_end, charge.period_start
);
}
// The seller owns the property to the end of the completion day.
let up_to_completion = days_between(&charge.period_start, completion_date) + 1;
let seller_days = total_days.min(up_to_completion.max(0));
let split = prorate(&charge.amount, total_days, seller_days);
let adjustment = if charge.paid_by_seller { split.unused.minor } else { -split.used.minor };
balance += adjustment;
lines.push(ApportionmentLine {
label: charge.label.clone(),
seller_days,
buyer_days: total_days - seller_days,
adjustment: money(adjustment, currency),
});
}
let mut fees_total = 0i64;
let mut fee_lines: Vec<FeeLine> = Vec::new();
for fee in fees {
same(&fee.amount, &format!("\"{}\"", fee.label));
if fee.amount.minor < 0 {
panic!("\"{}\" must not be negative, received {}", fee.label, fee.amount.minor);
}
fees_total += fee.amount.minor;
fee_lines.push(FeeLine { label: fee.label.clone(), amount: money(fee.amount.minor, currency) });
}
CompletionStatement {
price: money(price.minor, currency),
deposit_paid: money(deposit_paid.minor, currency),
apportionments: lines,
balance_to_seller: money(balance, currency),
fees: fee_lines,
fees_total: money(fees_total, currency),
total_due: money(balance + fees_total, currency),
}
}
pub fn period_charge_from_value(v: &Value) -> PeriodCharge {
PeriodCharge {
label: v.get("label").as_str().to_string(),
amount: money_from_value(v.get("amount")),
period_start: v.get("periodStart").as_str().to_string(),
period_end: v.get("periodEnd").as_str().to_string(),
paid_by_seller: v.get("paidBySeller").as_bool(),
}
}
pub fn fee_line_from_value(v: &Value) -> FeeLine {
FeeLine { label: v.get("label").as_str().to_string(), amount: money_from_value(v.get("amount")) }
}
fn fee_line_to_value(fee: &FeeLine) -> Value {
Value::obj(vec![("label", Value::str(&fee.label)), ("amount", money_to_value(&fee.amount))])
}
pub fn completion_statement_to_value(s: &CompletionStatement) -> Value {
Value::obj(vec![
("price", money_to_value(&s.price)),
("depositPaid", money_to_value(&s.deposit_paid)),
(
"apportionments",
Value::Arr(
s.apportionments
.iter()
.map(|l| {
Value::obj(vec![
("label", Value::str(&l.label)),
("sellerDays", Value::Int(l.seller_days)),
("buyerDays", Value::Int(l.buyer_days)),
("adjustment", money_to_value(&l.adjustment)),
])
})
.collect(),
),
),
("balanceToSeller", money_to_value(&s.balance_to_seller)),
("fees", Value::Arr(s.fees.iter().map(fee_line_to_value).collect())),
("feesTotal", money_to_value(&s.fees_total)),
("totalDue", money_to_value(&s.total_due)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let charges: Vec<PeriodCharge> = args[3].as_arr().iter().map(period_charge_from_value).collect();
let fees: Vec<FeeLine> = args[4].as_arr().iter().map(fee_line_from_value).collect();
completion_statement_to_value(&completion_statement(
&money_from_value(&args[0]),
&money_from_value(&args[1]),
args[2].as_str(),
&charges,
&fees,
))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 3 dependencies, pins them in fune.lock, downloads only the Rust 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. A crate’s build.rs runs it before every compile. Or pin a range in fune.project and build in one step:
fune add property.completion-statement
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./property.completion-statement-1.0.0-rust.fune, or fetch it from a terminal with fune pull property.completion-statement@1.0.0:rust.
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.
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Path | Bytes |
|---|---|
| README.md | 2,486 |
| impl/python.py | 3,218 |
| impl/rust.rs | 5,570 |
| impl/typescript.ts | 3,188 |
| vectors.json | 12,246 |