Functional Weave
Code in Rust

construction.day-rate-quote

A trade quote from labour days at a day rate, materials with a markup, and VAT.

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

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

What it does

The price of a small job the way trades quote it: so many days at a day rate, materials at cost plus a markup, and VAT on top.

- **Labour** is days x day rate, rounded half up to the penny once. Days are hundredths of a day (`250` is 2.5 days, `50` a half day), so no float ever touches the money. - **Materials** are the cost plus a markup, priced by `finance.markup-price` on the `markup` basis (profit over cost: 15% on 100.00 is 115.00), rounded half up. The markup applies to materials only; the day rate is already the price of labour. For a margin target instead, price the materials with `finance.markup-price` yourself. - **VAT** is `finance.tax.add-vat` on the net total, GB rates, at the rate in force on `onDate`. Pass `null` when you are not VAT registered. Under the domestic reverse charge (see `construction.vat-reverse-charge`) quote with `null` too and say on the quote that the customer accounts for the VAT.

For example

  • day_rate_quote(300, £250.00, £400.00, 20%, STANDARD, 2026-09-23) → labour £750.00, materials £480.00, net £1,230.00, vat £246.00, gross £1,476.00, vat basis points 20% 3 days at 250.00 and 400.00 of materials marked up 20%, standard-rated: the markup is on materials only
  • day_rate_quote(250, £180.00, £0.00, 15%, —, 2026-09-23) → labour £450.00, materials £0.00, net £450.00, vat £0.00, gross £450.00, vat basis points 0% 2.5 days at 180.00, no materials, not VAT registered
  • day_rate_quote(33, £199.99, £12.34, 15%, STANDARD, 2026-09-23) → labour £66.00, materials £14.19, net £80.19, vat £16.04, gross £96.23, vat basis points 20% a third of a day at 199.99 is 65.9967, rounded half up to 66.00; 12.34 of materials at 15% is 14.191, 14.19; VAT 20% of 80.19 is 16.038, 16.04

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 day_rate_quote(labour_days_hundredths: i64, day_rate: &Money, materials_cost: &Money, materials_markup_basis_points: i64, vat_category: Option<&str>, on_date: &str) -> DayRateQuote
labour_days_hundredthsintlabour in hundredths of a day: 250 = 2.5 days, 50 = half a day
day_rateMoneythe charge for one day's labour
materials_costMoneywhat the materials cost you, excluding VAT
materials_markup_basis_pointsintmarkup on the materials cost, 1500 = 15%
vat_categorystring?GB VAT category (STANDARD, REDUCED, ZERO); null when you are not VAT registered
on_datedatethe date of the quote's supply, which decides the VAT rate
returnsDayRateQuote

The type it declares, generated into your project

/// The quote as the customer sees it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DayRateQuote {
    /// days x day rate
    pub labour: Money,
    /// materials cost with the markup
    pub materials: Money,
    /// labour plus materials
    pub net: Money,
    pub vat: Money,
    /// net plus VAT: the price quoted
    pub gross: Money,
    /// the VAT rate applied, 0 when none
    pub vat_basis_points: i64,
}

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

fune!(construction.day-rate-quote@^1);  // then call day_rate_quote(…)
impl/rust.rs · 97 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.

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::finance_markup_price::markup_price;  ← from finance.markup-price ^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::math_round_div::round_div;  ← from math.round-div ^1.0.0 · built alongside by fune
use super::money_add::add_money;  ← from money.add ^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

const MAX_SAFE: i128 = 9_007_199_254_740_991;

/// A quote from labour days, a day rate, marked-up materials and VAT.
///
/// The markup goes on the materials only: the day rate already is the price of
/// labour. Labour is rounded once, to the penny, from days x rate; VAT is one
/// rounding on the net total, as on the invoice that follows.
///
/// # Panics
/// Panics on negative days or rates, mixed currencies, an unknown VAT
/// category, or a product beyond 2^53 - 1.
pub fn day_rate_quote(
    labour_days_hundredths: i64,
    day_rate: &Money,
    materials_cost: &Money,
    materials_markup_basis_points: i64,
    vat_category: Option<&str>,
    on_date: &str,
) -> DayRateQuote {
    if labour_days_hundredths < 0 {
        panic!(
            "labourDaysHundredths must be a whole number of hundredths, not negative, received {}",
            labour_days_hundredths
        );
    }
    if day_rate.minor < 0 {
        panic!("dayRate must not be negative, received {}", day_rate.minor);
    }
    let product = day_rate.minor as i128 * labour_days_hundredths as i128;
    // i64 could go further, but TypeScript cannot, and all three must agree.
    if product > MAX_SAFE {
        panic!("the labour calculation exceeds 2^53 - 1");
    }
    let labour = money(round_div(product as i64, 100, "half-up"), &day_rate.currency);
    let materials = markup_price(materials_cost, materials_markup_basis_points, "markup", "half-up");
    let net = add_money(&labour, &materials);
    match vat_category {
        None => DayRateQuote {
            vat: money(0, &net.currency),
            gross: net.clone(),
            labour,
            materials,
            net,
            vat_basis_points: 0,
        },
        Some(category) => {
            let vat = add_vat(&net, "GB", category, on_date);
            DayRateQuote {
                labour,
                materials,
                net,
                vat: vat.tax,
                gross: vat.gross,
                vat_basis_points: vat.basis_points,
            }
        }
    }
}

pub fn day_rate_quote_to_value(q: &DayRateQuote) -> Value {
    Value::obj(vec![
        ("labour", money_to_value(&q.labour)),
        ("materials", money_to_value(&q.materials)),
        ("net", money_to_value(&q.net)),
        ("vat", money_to_value(&q.vat)),
        ("gross", money_to_value(&q.gross)),
        ("vatBasisPoints", Value::Int(q.vat_basis_points)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    if let Value::Float(f) = &args[0] {
        panic!(
            "labourDaysHundredths must be a whole number of hundredths, not negative, received {}",
            f
        );
    }
    if let Value::Float(f) = &args[3] {
        panic!("basisPoints must be an integer, received {}", f);
    }
    let category = if args[4].is_null() { None } else { Some(args[4].as_str()) };
    day_rate_quote_to_value(&day_rate_quote(
        args[0].as_i64(),
        &money_from_value(&args[1]),
        &money_from_value(&args[2]),
        args[3].as_i64(),
        category,
        args[5].as_str(),
    ))
}

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.day-rate-quote
Download for Rust construction.day-rate-quote-1.0.0-rust.fune · 19,133 bytes sha256 2f28ec2aca83ec841f7ace7c90cde380094e8df5e633a47215b3148dca255bd1

The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./construction.day-rate-quote-1.0.0-rust.fune, or fetch it from a terminal with fune pull construction.day-rate-quote@1.0.0:rust.

The whole function, every language, is one file too: construction.day-rate-quote-1.0.0.fune, 23,229 bytes, sha256 99bc4e96eeef9b0a55ff34a9020b41d78b29d57268ffd4b169f99205d1c6f6a1. 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.day-rate-quote

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

// fune: after construction.day-rate-quote

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 finance.markup-price in construction.day-rate-quote
// fune: replace finance.tax.add-vat in construction.day-rate-quote
// fune: replace math.round-div in construction.day-rate-quote
// fune: replace money.add in construction.day-rate-quote
// fune: replace money.amount in construction.day-rate-quote

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.day-rate-quote --steps.

// fune: step construction.day-rate-quote 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
3 days at 250.00 and 400.00 of materials marked up 20%, standard-rated: the markup is on materials only 300, £250.00, £400.00, 20%, STANDARD, 2026-09-23 → labour £750.00, materials £480.00, net £1,230.00, vat £246.00, gross £1,476.00, vat basis points 20%
2.5 days at 180.00, no materials, not VAT registered 250, £180.00, £0.00, 15%, —, 2026-09-23 → labour £450.00, materials £0.00, net £450.00, vat £0.00, gross £450.00, vat basis points 0%
a third of a day at 199.99 is 65.9967, rounded half up to 66.00; 12.34 of materials at 15% is 14.191, 14.19; VAT 20% of 80.19 is 16.038, 16.04 33, £199.99, £12.34, 15%, STANDARD, 2026-09-23 → labour £66.00, materials £14.19, net £80.19, vat £16.04, gross £96.23, vat basis points 20%
reduced-rated installation at 5% 100, £200.00, £1,000.00, 0%, REDUCED, 2026-09-23 → labour £200.00, materials £1,000.00, net £1,200.00, vat £60.00, gross £1,260.00, vat basis points 5%
zero-rated work 200, £150.00, £300.00, 10%, ZERO, 2026-09-23 → labour £300.00, materials £330.00, net £630.00, vat £0.00, gross £630.00, vat basis points 0%
a quote for a 2010 job uses the 17.5% rate then in force 100, £100.00, £0.00, 0%, STANDARD, 2010-06-30 → labour £100.00, materials £0.00, net £100.00, vat £17.50, gross £117.50, vat basis points 17.5%
materials only: no labour days 0, £250.00, £50.00, 10%, STANDARD, 2026-09-23 → labour £0.00, materials £55.00, net £55.00, vat £11.00, gross £66.00, vat basis points 20%
half a day at 250.00 50, £250.00, £0.00, 0%, —, 2026-09-23 → labour £125.00, materials £0.00, net £125.00, vat £0.00, gross £125.00, vat basis points 0%
half a day at 250.01 is 125.005, rounded half up to 125.01 50, £250.01, £0.00, 0%, —, 2026-09-23 → labour £125.01, materials £0.00, net £125.01, vat £0.00, gross £125.01, vat basis points 0%
materials sold at cost: no markup 100, £200.00, £77.77, 0%, STANDARD, 2026-09-23 → labour £200.00, materials £77.77, net £277.77, vat £55.55, gross £333.32, vat basis points 20%
Show the other 9 tests
CaseArgumentsExpected
negative labour days -50, £250.00, £0.00, 0%, —, 2026-09-23 → error: labourDaysHundredths must be a whole number of hundredths, not negative
fractional hundredths 2.5, £250.00, £0.00, 0%, —, 2026-09-23 → error: labourDaysHundredths must be a whole number of hundredths, not negative
a negative day rate 100, -£0.01, £0.00, 0%, —, 2026-09-23 → error: dayRate must not be negative
negative materials 100, £250.00, -£0.01, 0%, —, 2026-09-23 → error: cost must not be negative
a markup below -100% 100, £250.00, £1.00, -100.01%, —, 2026-09-23 → error: a markup of -10001 basis points gives a negative price
a fractional markup 100, £250.00, £1.00, 0.015%, —, 2026-09-23 → error: must be an integer
day rate and materials in different currencies 100, £250.00, €1.00, 0%, —, 2026-09-23 → error: currency mismatch
an unknown VAT category 100, £250.00, £0.00, 0%, LUXURY, 2026-09-23 → error: no VAT rule for GB/LUXURY
a labour product beyond 2^53 - 1 10,000,000, £10,000,000,000.00, £0.00, 0%, —, 2026-09-23 → error: exceeds 2^53 - 1

More from the author

Edges: zero days (a materials-only quote) and zero materials are fine. Negative days, rates or materials are errors, as is a markup below -100% and mixed currencies. day rate x hundredths must stay within 2^53 - 1.

Files

PathBytes
README.md1,194
impl/python.py2,082
impl/rust.rs3,353
impl/typescript.ts1,847
vectors.json9,957