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
dayRateQuote(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 onlydayRateQuote(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 registereddayRateQuote(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.
export function dayRateQuote(labourDaysHundredths: number, dayRate: Money, materialsCost: Money, materialsMarkupBasisPoints: number, vatCategory: string | null, onDate: string): DayRateQuote
| labourDaysHundredths | int | labour in hundredths of a day: 250 = 2.5 days, 50 = half a day |
| dayRate | Money | the charge for one day's labour |
| materialsCost | Money | what the materials cost you, excluding VAT |
| materialsMarkupBasisPoints | int | markup on the materials cost, 1500 = 15% |
| vatCategory | string? | GB VAT category (STANDARD, REDUCED, ZERO); null when you are not VAT registered |
| onDate | date | the date of the quote's supply, which decides the VAT rate |
| returns | DayRateQuote |
The type it declares, generated into your project
/** The quote as the customer sees it. */
export interface DayRateQuote {
/** days x day rate */
readonly labour: Money;
/** materials cost with the markup */
readonly materials: Money;
/** labour plus materials */
readonly net: Money;
readonly vat: Money;
/** net plus VAT: the price quoted */
readonly gross: Money;
/** the VAT rate applied, 0 when none */
readonly vatBasisPoints: number;
}
Your code names it in one line, in the file that uses it
import { dayRateQuote } from "#fune/construction.day-rate-quote@^1";
Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
import { markupPrice } from "./finance_markup_price.ts"; ← from finance.markup-price ^1.0.0 · built alongside by fune
import { addVat } from "./finance_tax_add_vat.ts"; ← from finance.tax.add-vat ^1.0.0 · built alongside by fune
import { roundDiv } from "./math_round_div.ts"; ← from math.round-div ^1.0.0 · built alongside by fune
import { addMoney } from "./money_add.ts"; ← from money.add ^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 DayRateQuote } from "./construction_day_rate_quote_types.ts";
/**
* 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.
*/
export function dayRateQuote(
labourDaysHundredths: number,
dayRate: Money,
materialsCost: Money,
materialsMarkupBasisPoints: number,
vatCategory: string | null,
onDate: string
): DayRateQuote {
if (!Number.isInteger(labourDaysHundredths) || labourDaysHundredths < 0) {
throw new RangeError(`labourDaysHundredths must be a whole number of hundredths, not negative, received ${labourDaysHundredths}`);
}
if (dayRate.minor < 0) {
throw new RangeError(`dayRate must not be negative, received ${dayRate.minor}`);
}
const product = dayRate.minor * labourDaysHundredths;
if (!Number.isSafeInteger(product)) {
throw new RangeError("the labour calculation exceeds 2^53 - 1");
}
const labour = money(roundDiv(product, 100, "half-up"), dayRate.currency);
const materials = markupPrice(materialsCost, materialsMarkupBasisPoints, "markup", "half-up");
const net = addMoney(labour, materials);
if (vatCategory === null) {
return { labour, materials, net, vat: money(0, net.currency), gross: net, vatBasisPoints: 0 };
}
const vat = addVat(net, "GB", vatCategory, onDate);
return { labour, materials, net, vat: vat.tax, gross: vat.gross, vatBasisPoints: vat.basisPoints };
}Install
fune build
With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 5 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 construction.day-rate-quote
The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./construction.day-rate-quote-1.0.0-typescript.fune, or fetch it from a terminal with fune pull construction.day-rate-quote@1.0.0:typescript.
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.
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Case | Arguments | Expected | |
|---|---|---|---|
| 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
| Path | Bytes |
|---|---|
| README.md | 1,194 |
| impl/python.py | 2,082 |
| impl/rust.rs | 3,353 |
| impl/typescript.ts | 1,847 |
| vectors.json | 9,957 |