construction.valuation
Interim valuation: gross value to date, retention, previous payments, amount due, VAT or reverse charge, and CIS.
1.0.0 · published 2026-10-03 by charlie · Anterra
Pinned by 16 tests, run in TypeScript, Python and Rust.
What it does
An interim valuation (application for payment, payment certificate) from the quantity surveyor's figures to the amount the payer actually transfers.
## The order of operations
For example
interim_valuation(work done £100,000.00, materials on site £5,000.00, retention basis points 5%, stage interim, previously certified £70,000.00, cis status —, cis materials £0.00, vat category STAN…)→ gross valuation £105,000.00, retention …, net valuation £99,750.00, previously certified £70,000.00, amount due £29,750.00, vat £5,950.00, vat basis points 20%, reverse charge vat… 100,000 of work and 5,000 of materials on site, 5% retention, 70,000 paid before, standard-rated VAT, no CISinterim_valuation(work done £100,000.00, materials on site £5,000.00, retention basis points 5%, stage interim, previously certified £70,000.00, cis status standard, cis materials £9,750.00, vat ca…)→ gross valuation £105,000.00, retention …, net valuation £99,750.00, previously certified £70,000.00, amount due £29,750.00, vat £0.00, vat basis points 20%, reverse charge vat £5,… the same valuation under the reverse charge and CIS: no VAT charged, 20% withheld from the labour onlyinterim_valuation(work done £10,000.00, materials on site £0.00, retention basis points 3%, stage interim, previously certified £5,000.00, cis status higher, cis materials £0.00, vat category —, re…)→ gross valuation £10,000.00, retention …, net valuation £9,700.00, previously certified £5,000.00, amount due £4,700.00, vat £0.00, vat basis points 0%, reverse charge vat £0.00, c… a subcontractor not registered for VAT or CIS: 30% of the whole amount due
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 interim_valuation(input: &ValuationInput) -> Valuation
| input | ValuationInput | |
| returns | Valuation |
The types it declares, generated into your project
/// One valuation, cumulative to date, as a quantity surveyor prepares it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValuationInput {
/// value of work properly executed to date, variations included
pub work_done: Money,
/// value of unfixed materials included in the valuation
pub materials_on_site: Money,
/// the retention percentage, 500 = 5%
pub retention_basis_points: i64,
/// interim, practical-completion or final
pub stage: String,
/// total of the amounts due on earlier valuations
pub previously_certified: Money,
/// null when the payment is not made under CIS
pub cis_status: Option<String>,
/// materials, plant hire and the like within this amount due, excluded from the CIS deduction
pub cis_materials: Money,
/// GB VAT category of the supply (STANDARD, REDUCED, ZERO); null when the payee is not VAT registered
pub vat_category: Option<String>,
/// true when the domestic reverse charge applies (see construction.vat-reverse-charge)
pub reverse_charge: bool,
/// the tax point and payment date that decide the VAT and CIS rates
pub valuation_date: String,
}
/// The payment certificate, line by line.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Valuation {
/// work done plus materials on site
pub gross_valuation: Money,
pub retention: Retention,
/// gross less retention held
pub net_valuation: Money,
pub previously_certified: Money,
/// net valuation less previously certified, before VAT and CIS
pub amount_due: Money,
/// VAT charged on the invoice; zero under the reverse charge
pub vat: Money,
pub vat_basis_points: i64,
/// VAT the customer accounts for under the reverse charge; zero otherwise
pub reverse_charge_vat: Money,
/// the CIS deduction, null when not under CIS
pub cis: Option<CisDeduction>,
/// amount due plus VAT charged less the CIS deduction
pub amount_payable: Money,
}
Your code names it in one line, in the file that uses it
fune!(construction.valuation@^1); // then call interim_valuation(…)
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::construction_cis_deduction::{cis_deduction, cis_deduction_to_value, CisDeduction}; ← from construction.cis-deduction ^1.0.0 · built alongside by fune
use super::construction_retention::{retention, retention_to_value}; ← from construction.retention ^1.0.0 · built alongside by fune
use super::finance_tax_add_vat::add_vat; ← from finance.tax.add-vat ^1.0.0 · built alongside by fune
use super::money_add::{add_money, subtract_money}; ← from money.add ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_from_value, money_to_value}; ← from money.amount ^1.0.0 · built alongside by fune
/// An interim valuation, from the cumulative value of the work to what is paid.
///
/// The order is the whole point: retention is taken on the cumulative gross,
/// previous certificates come off the net, VAT goes on the amount due, and the
/// CIS deduction comes off the labour in the amount due, never off the VAT.
/// Each step is its own capability; this one only fixes the order.
///
/// # Panics
/// Panics on mixed currencies, a reverse charge without a standard or reduced
/// VAT category, a negative amount due under CIS, and whatever the retention,
/// VAT and CIS capabilities refuse.
pub fn interim_valuation(input: &ValuationInput) -> Valuation {
let gross_valuation = add_money(&input.work_done, &input.materials_on_site);
let held = retention(&gross_valuation, input.retention_basis_points, &input.stage);
let net_valuation = subtract_money(&gross_valuation, &held.held);
let amount_due = subtract_money(&net_valuation, &input.previously_certified);
let currency = amount_due.currency.clone();
let mut vat = money(0, ¤cy);
let mut reverse_charge_vat = money(0, ¤cy);
let mut vat_basis_points = 0;
match &input.vat_category {
None => {
if input.reverse_charge {
panic!("the reverse charge needs a VAT category: the supplier must be VAT registered");
}
}
Some(category) => {
if input.reverse_charge && category != "STANDARD" && category != "REDUCED" {
panic!(
"the reverse charge applies only to STANDARD or REDUCED supplies, received {}",
category
);
}
let breakdown = add_vat(&amount_due, "GB", category, &input.valuation_date);
vat_basis_points = breakdown.basis_points;
if input.reverse_charge {
reverse_charge_vat = breakdown.tax;
} else {
vat = breakdown.tax;
}
}
}
let cis: Option<CisDeduction> = match &input.cis_status {
None => None,
Some(status) => {
if amount_due.minor < 0 {
panic!(
"the amount due is negative ({}), so there is no payment to make a CIS deduction from",
amount_due.minor
);
}
Some(cis_deduction(
&amount_due,
&input.cis_materials,
status,
&input.valuation_date,
"down",
))
}
};
let payable = add_money(&amount_due, &vat);
let amount_payable = match &cis {
None => payable,
Some(c) => subtract_money(&payable, &c.deduction),
};
Valuation {
gross_valuation,
retention: held,
net_valuation,
previously_certified: input.previously_certified.clone(),
amount_due,
vat,
vat_basis_points,
reverse_charge_vat,
cis,
amount_payable,
}
}
fn optional_string(v: &Value) -> Option<String> {
if v.is_null() {
None
} else {
Some(v.as_str().to_string())
}
}
pub fn valuation_input_from_value(v: &Value) -> ValuationInput {
ValuationInput {
work_done: money_from_value(v.get("workDone")),
materials_on_site: money_from_value(v.get("materialsOnSite")),
retention_basis_points: v.get("retentionBasisPoints").as_i64(),
stage: v.get("stage").as_str().to_string(),
previously_certified: money_from_value(v.get("previouslyCertified")),
cis_status: optional_string(v.get("cisStatus")),
cis_materials: money_from_value(v.get("cisMaterials")),
vat_category: optional_string(v.get("vatCategory")),
reverse_charge: v.get("reverseCharge").as_bool(),
valuation_date: v.get("valuationDate").as_str().to_string(),
}
}
pub fn valuation_to_value(v: &Valuation) -> Value {
Value::obj(vec![
("grossValuation", money_to_value(&v.gross_valuation)),
("retention", retention_to_value(&v.retention)),
("netValuation", money_to_value(&v.net_valuation)),
("previouslyCertified", money_to_value(&v.previously_certified)),
("amountDue", money_to_value(&v.amount_due)),
("vat", money_to_value(&v.vat)),
("vatBasisPoints", Value::Int(v.vat_basis_points)),
("reverseChargeVat", money_to_value(&v.reverse_charge_vat)),
(
"cis",
match &v.cis {
None => Value::Null,
Some(c) => cis_deduction_to_value(c),
},
),
("amountPayable", money_to_value(&v.amount_payable)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
valuation_to_value(&interim_valuation(&valuation_input_from_value(&args[0])))
}Install
fune build
With that line in your source, in a Rust project (language rust in fune.project), fune build resolves it and its 5 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 construction.valuation
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./construction.valuation-1.0.0-rust.fune, or fetch it from a terminal with fune pull construction.valuation@1.0.0:rust.
The whole function, every language, is one file too: construction.valuation-1.0.0.fune, 44,079 bytes, sha256 1974a3927d6147e478ea118428c408484aed5f276db955298896d5162f0078a8. 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 construction.valuation
after — your function gets the result and the arguments, and returns the final result.
// fune: after construction.valuation
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 construction.cis-deduction in construction.valuation
// fune: replace construction.retention in construction.valuation
// fune: replace finance.tax.add-vat in construction.valuation
// fune: replace money.add in construction.valuation
// fune: replace money.amount in construction.valuation
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 construction.valuation --steps.
// fune: step construction.valuation 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 | |
|---|---|---|---|
| 100,000 of work and 5,000 of materials on site, 5% retention, 70,000 paid before, standard-rated VAT, no CIS | work done £100,000.00, materials on site £5,000.00, retention basis points 5%, stage interim, previously certified £70,000.00, cis status —, cis materials £0.00, vat category STAN… | → | gross valuation £105,000.00, retention …, net valuation £99,750.00, previously certified £70,000.00, amount due £29,750.00, vat £5,950.00, vat basis points 20%, reverse charge vat… |
| the same valuation under the reverse charge and CIS: no VAT charged, 20% withheld from the labour only | work done £100,000.00, materials on site £5,000.00, retention basis points 5%, stage interim, previously certified £70,000.00, cis status standard, cis materials £9,750.00, vat ca… | → | gross valuation £105,000.00, retention …, net valuation £99,750.00, previously certified £70,000.00, amount due £29,750.00, vat £0.00, vat basis points 20%, reverse charge vat £5,… |
| a subcontractor not registered for VAT or CIS: 30% of the whole amount due | work done £10,000.00, materials on site £0.00, retention basis points 3%, stage interim, previously certified £5,000.00, cis status higher, cis materials £0.00, vat category —, re… | → | gross valuation £10,000.00, retention …, net valuation £9,700.00, previously certified £5,000.00, amount due £4,700.00, vat £0.00, vat basis points 0%, reverse charge vat £0.00, c… |
| practical completion releases half the retention: the amount due is the released half | work done £100,000.00, materials on site £0.00, retention basis points 5%, stage practical-completion, previously certified £95,000.00, cis status —, cis materials £0.00, vat cate… | → | gross valuation £100,000.00, retention …, net valuation £97,500.00, previously certified £95,000.00, amount due £2,500.00, vat £500.00, vat basis points 20%, reverse charge vat £0… |
| the final valuation releases the rest; gross payment status withholds nothing | work done £100,000.00, materials on site £0.00, retention basis points 5%, stage final, previously certified £97,500.00, cis status gross, cis materials £0.00, vat category STANDA… | → | gross valuation £100,000.00, retention …, net valuation £100,000.00, previously certified £97,500.00, amount due £2,500.00, vat £500.00, vat basis points 20%, reverse charge vat £… |
| over-certified before: a negative amount due, with negative VAT, when there is no CIS | work done £10,000.00, materials on site £0.00, retention basis points 5%, stage interim, previously certified £10,000.00, cis status —, cis materials £0.00, vat category STANDARD,… | → | gross valuation £10,000.00, retention …, net valuation £9,500.00, previously certified £10,000.00, amount due -£500.00, vat -£100.00, vat basis points 20%, reverse charge vat … |
| reduced-rated work: 5% of 11,975.30 is 598.765, VAT rounds half up to 598.77; CIS on the 10,000.00 labour | work done £12,345.67, materials on site £0.00, retention basis points 3%, stage interim, previously certified £0.00, cis status standard, cis materials £1,975.30, vat category RED… | → | gross valuation £12,345.67, retention …, net valuation £11,975.30, previously certified £0.00, amount due £11,975.30, vat £598.77, vat basis points 5%, reverse charge vat £0.00, c… |
| the CIS deduction rounds down: 20% of 123.48 is 24.696, withheld 24.69 | work done £123.48, materials on site £0.00, retention basis points 0%, stage interim, previously certified £0.00, cis status standard, cis materials £0.00, vat category —, reverse… | → | gross valuation £123.48, retention …, net valuation £123.48, previously certified £0.00, amount due £123.48, vat £0.00, vat basis points 0%, reverse charge vat £0.00, cis …, amoun… |
| zero-rated work: VAT at 0% | work done £5,000.00, materials on site £0.00, retention basis points 5%, stage interim, previously certified £0.00, cis status —, cis materials £0.00, vat category ZERO, reverse c… | → | gross valuation £5,000.00, retention …, net valuation £4,750.00, previously certified £0.00, amount due £4,750.00, vat £0.00, vat basis points 0%, reverse charge vat £0.00, cis —,… |
| the rate on the valuation date: 17.5% in 2010 | work done £1,000.00, materials on site £0.00, retention basis points 0%, stage interim, previously certified £0.00, cis status —, cis materials £0.00, vat category STANDARD, rever… | → | gross valuation £1,000.00, retention …, net valuation £1,000.00, previously certified £0.00, amount due £1,000.00, vat £175.00, vat basis points 17.5%, reverse charge vat £0.00, c… |
Show the other 6 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| the reverse charge without a VAT category | work done £1,000.00, materials on site £0.00, retention basis points 0%, stage interim, previously certified £0.00, cis status —, cis materials £0.00, vat category —, reverse char… | → | error: the reverse charge needs a VAT category |
| the reverse charge on zero-rated work | work done £1,000.00, materials on site £0.00, retention basis points 0%, stage interim, previously certified £0.00, cis status —, cis materials £0.00, vat category ZERO, reverse c… | → | error: the reverse charge applies only to STANDARD or REDUCED supplies |
| a negative amount due under CIS | work done £10,000.00, materials on site £0.00, retention basis points 5%, stage interim, previously certified £10,000.00, cis status standard, cis materials £0.00, vat category —,… | → | error: the amount due is negative (-50000) |
| CIS materials above the amount due | work done £1,000.00, materials on site £0.00, retention basis points 0%, stage interim, previously certified £0.00, cis status standard, cis materials £1,000.01, vat category —, r… | → | error: cannot exceed the payment |
| materials on site in another currency | work done £1,000.00, materials on site €1.00, retention basis points 0%, stage interim, previously certified £0.00, cis status —, cis materials £0.00, vat category —, reverse char… | → | error: currency mismatch |
| an unknown retention stage | work done £1,000.00, materials on site £0.00, retention basis points 5%, stage handover, previously certified £0.00, cis status —, cis materials £0.00, vat category —, reverse cha… | → | error: unknown retention stage "handover" |
More from the author
1. **Gross valuation** = work done to date + materials on site. Both are cumulative, as valuations always are. 2. **Retention** on the cumulative gross, with `construction.retention`: the full percentage before practical completion, half after it, none after the defects are made good. 3. **Net valuation** = gross - retention held. 4. **Amount due** = net - previously certified. Because everything above is cumulative, retention released at practical completion simply shows up here as money due. 5. **VAT** on the amount due, with `finance.tax.add-vat` (GB, the category and the valuation date). Under the domestic reverse charge nothing is charged: `vat` is zero and `reverseChargeVat` shows what the customer accounts for to HMRC. Whether the reverse charge applies is a separate question for `construction.vat-reverse-charge`; pass its answer as `reverseCharge`. 6. **CIS** with `construction.cis-deduction`: the deduction is taken from the amount due less `cisMaterials`, never from the VAT, at the payee's status and the rate on the valuation date. The deduction rounds **down** to the penny (HMRC publishes no rounding rule; down never over-deducts). Call `construction.cis-deduction` yourself if you need another mode. 7. **Amount payable** = amount due + VAT charged - CIS deduction.
## Edges
- A valuation below what was previously certified gives a negative amount due, with negative VAT: the payee owes the difference. Under CIS that is an error, because there is no payment to deduct from; correct it in the next valuation. - `vatCategory` null means the payee is not VAT registered; the reverse charge then cannot apply and is refused, as is a reverse charge on zero-rated work. - `cisMaterials` is ignored when `cisStatus` is null. It must not exceed the amount due. - All amounts must share one currency.
Payment timing (due date, payment and pay-less notices) is `construction.payment-notice-dates`.
Files
| Path | Bytes |
|---|---|
| README.md | 2,175 |
| impl/python.py | 2,781 |
| impl/rust.rs | 5,191 |
| impl/typescript.ts | 2,616 |
| vectors.json | 22,929 |