fleet.mileage-allowance
HMRC approved mileage allowance payments for an employee's own vehicle: tax-free amount, taxable excess or relief.
1.0.2 · published 2026-10-03 by charlie · Anterra
Pinned by 23 tests, run in TypeScript, Python and Rust.
Not professional advice. This capability calculates tax figures from published rules. It is a software component for developers, not tax 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 payroll specialist review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.
What it does
HMRC Approved Mileage Allowance Payments (AMAPs) for business miles an employee drives in their own vehicle: the approved amount (the most the employer can pay tax-free), the taxable excess when the employer pays more, and the Mileage Allowance Relief (MAR) the employee can claim when it pays less. Passenger payments are worked out alongside.
## The 10,000-mile threshold
For example
mileage_allowance(car-van, 0, 1,000, 0, £450.00, £0.00, 2025-06-01)→ approved £450.00, paid £450.00, taxable excess £0.00, relief £0.00, miles at first rate 1,000, miles at second rate 0, first rate pence 45, second rate pence 25, passenger approve… 1,000 car miles in 2025-26 paid at 45p: all approved, nothing taxablemileage_allowance(car-van, 9,500, 1,000, 0, £450.00, £0.00, 2025-06-01)→ approved £350.00, paid £450.00, taxable excess £100.00, relief £0.00, miles at first rate 500, miles at second rate 500, first rate pence 45, second rate pence 25, passenger appro… a claim that crosses 10,000 miles splits: 500 at 45p and 500 at 25p, so a flat 45p leaves 100.00 taxablemileage_allowance(car-van, 12,000, 200, 0, £0.00, £0.00, 2025-12-01)→ approved £50.00, paid £0.00, taxable excess £0.00, relief £50.00, miles at first rate 0, miles at second rate 200, first rate pence 45, second rate pence 25, passenger approved £0… every mile past 10,000 is at 25p; nothing paid gives relief on all of it
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 mileage_allowance(vehicle: &str, previous_miles: i64, miles: i64, passenger_miles: i64, paid: &Money, passenger_paid: &Money, on_date: &str) -> MileageAllowance
| vehicle | MileageVehicle | car-van (both share one 10,000-mile threshold), motorcycle or bicycle |
| previous_miles | int | business miles in this vehicle kind already paid for earlier in the same tax year and employment |
| miles | int | business miles in this claim |
| passenger_miles | int | business miles times fellow employees carried: two passengers for 30 miles is 60; cars and vans only |
| paid | Money | what the employer pays for these miles, excluding passenger payments |
| passenger_paid | Money | what the employer pays in passenger payments for these miles |
| on_date | date | any date in the tax year the journeys were made; decides the rates |
| returns | MileageAllowance |
The types it declares, generated into your project
// MileageVehicle is a string in Rust, one of: "car-van", "motorcycle", "bicycle".
// Parameters take it as &str and results hold it as String.
/// The approved (tax-free) amount for a claim and what is taxable or relievable either side of it.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct MileageAllowance {
/// the approved amount for the miles: HMRC's tax-free ceiling
pub approved: Money,
/// as passed
pub paid: Money,
/// paid above approved: taxable, reported on P11D or payrolled
pub taxable_excess: Money,
/// approved above paid: Mileage Allowance Relief the employee can claim
pub relief: Money,
/// miles at the first-10,000 rate (every mile for a flat-rate vehicle)
pub miles_at_first_rate: i64,
/// miles beyond 10,000 in the tax year
pub miles_at_second_rate: i64,
pub first_rate_pence: i64,
pub second_rate_pence: i64,
/// 5p a passenger mile
pub passenger_approved: Money,
/// passenger payments above the approved amount; there is no relief below it
pub passenger_taxable_excess: Money,
}
Your code names it in one line, in the file that uses it
fune!(fleet.mileage-allowance@^1); // then call mileage_allowance(…)
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::fleet_mileage_allowance_data::{AmapRate, AMAP_RATES, AMAP_RATES_HISTORY}; ← this capability’s own data, compiled from data/amap-rates.json into the same file by fune build
use super::money_add::subtract_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
fn is_iso_date(value: &str) -> bool {
let bytes = value.as_bytes();
bytes.len() == 10
&& bytes[4] == b'-'
&& bytes[7] == b'-'
&& bytes
.iter()
.enumerate()
.all(|(i, b)| i == 4 || i == 7 || b.is_ascii_digit())
}
fn rate_on(vehicle: &str, on_date: &str) -> &'static AmapRate {
let mut best: Option<&'static AmapRate> = None;
let mut earliest: Option<&'static str> = None;
for rule in AMAP_RATES {
if rule.vehicle != vehicle {
continue;
}
if earliest.map_or(true, |e| rule.valid_from < e) {
earliest = Some(rule.valid_from);
}
if on_date < rule.valid_from {
continue;
}
if let Some(valid_to) = rule.valid_to {
if on_date > valid_to {
continue;
}
}
if best.map_or(true, |b| rule.valid_from > b.valid_from) {
best = Some(rule);
}
}
if let Some(rule) = best {
return rule;
}
// A history=current build has dropped the old rows; refuse rather than pay
// an old claim at today's rate.
if AMAP_RATES_HISTORY != "full" {
if let Some(earliest) = earliest {
if on_date < earliest {
panic!(
"no approved mileage rate for {} on {}: this build was installed with history={}, so it only carries rates from {}. Reinstall with history=full for older claims.",
vehicle, on_date, AMAP_RATES_HISTORY, earliest
);
}
}
}
panic!("no approved mileage rate for {} on {}", vehicle, on_date)
}
fn whole_miles(name: &str, value: i64) {
if value < 0 {
panic!("{} must be a whole number of miles, not negative, received {}", name, value);
}
}
fn gbp(name: &str, value: &Money) {
if value.currency != "GBP" {
panic!("{} must be in GBP, received {}", name, value.currency);
}
if value.minor < 0 {
panic!("{} must not be negative, received {}", name, value.minor);
}
}
fn excess_and_shortfall(paid: &Money, approved: &Money) -> (Money, Money) {
let difference = subtract_money(paid, approved);
if difference.minor > 0 {
(difference, money(0, "GBP"))
} else {
(money(0, "GBP"), money(-difference.minor, "GBP"))
}
}
/// HMRC approved mileage allowance payments for business miles in an
/// employee's own vehicle.
///
/// The 10,000-mile threshold is per tax year, so a claim that crosses it is
/// split: `previous_miles` says where in the year this claim starts. Paying a
/// flat 45p (or 55p) on every mile past 10,000 is the usual mistake; the
/// excess over 25p is taxable.
///
/// # Panics
/// Panics on an unknown vehicle, negative miles or amounts, a currency other
/// than GBP, passenger payments for a motorcycle or bicycle, a malformed date,
/// or a date no rate covers.
pub fn mileage_allowance(
vehicle: &str,
previous_miles: i64,
miles: i64,
passenger_miles: i64,
paid: &Money,
passenger_paid: &Money,
on_date: &str,
) -> MileageAllowance {
if vehicle != "car-van" && vehicle != "motorcycle" && vehicle != "bicycle" {
panic!("unknown vehicle \"{}\": use car-van, motorcycle or bicycle", vehicle);
}
whole_miles("previousMiles", previous_miles);
whole_miles("miles", miles);
whole_miles("passengerMiles", passenger_miles);
gbp("paid", paid);
gbp("passengerPaid", passenger_paid);
if !is_iso_date(on_date) {
panic!("onDate must be an ISO date (YYYY-MM-DD), received \"{}\"", on_date);
}
if vehicle != "car-van" && (passenger_miles > 0 || passenger_paid.minor > 0) {
panic!("passenger payments apply only to cars and vans");
}
let rate = rate_on(vehicle, on_date);
let mut miles_at_first_rate = miles;
let mut miles_at_second_rate = 0;
if let Some(threshold) = rate.threshold_miles {
let first_left = (threshold - previous_miles).max(0);
miles_at_first_rate = miles.min(first_left);
miles_at_second_rate = miles - miles_at_first_rate;
}
let approved = money(
miles_at_first_rate * rate.first_rate_pence + miles_at_second_rate * rate.second_rate_pence,
"GBP",
);
let (taxable_excess, relief) = excess_and_shortfall(paid, &approved);
let passenger_approved = if vehicle == "car-van" {
money(passenger_miles * rate_on("passenger", on_date).first_rate_pence, "GBP")
} else {
money(0, "GBP")
};
// There is no relief for passenger payments below 5p, only tax above it.
let (passenger_taxable_excess, _) = excess_and_shortfall(passenger_paid, &passenger_approved);
MileageAllowance {
approved,
paid: paid.clone(),
taxable_excess,
relief,
miles_at_first_rate,
miles_at_second_rate,
first_rate_pence: rate.first_rate_pence,
second_rate_pence: rate.second_rate_pence,
passenger_approved,
passenger_taxable_excess,
}
}
pub fn mileage_allowance_to_value(m: &MileageAllowance) -> Value {
Value::obj(vec![
("approved", money_to_value(&m.approved)),
("paid", money_to_value(&m.paid)),
("taxableExcess", money_to_value(&m.taxable_excess)),
("relief", money_to_value(&m.relief)),
("milesAtFirstRate", Value::Int(m.miles_at_first_rate)),
("milesAtSecondRate", Value::Int(m.miles_at_second_rate)),
("firstRatePence", Value::Int(m.first_rate_pence)),
("secondRatePence", Value::Int(m.second_rate_pence)),
("passengerApproved", money_to_value(&m.passenger_approved)),
("passengerTaxableExcess", money_to_value(&m.passenger_taxable_excess)),
])
}
fn miles_arg(value: &Value, name: &str) -> i64 {
match value {
Value::Int(i) => *i,
Value::Float(f) if f.fract() == 0.0 => *f as i64,
other => panic!(
"{} must be a whole number of miles, not negative, received {:?}",
name, other
),
}
}
pub fn fune_vector(args: &[Value]) -> Value {
mileage_allowance_to_value(&mileage_allowance(
args[0].as_str(),
miles_arg(&args[1], "previousMiles"),
miles_arg(&args[2], "miles"),
miles_arg(&args[3], "passengerMiles"),
&money_from_value(&args[4]),
&money_from_value(&args[5]),
args[6].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 2 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 fleet.mileage-allowance
The manifest, vectors and README with only the Rust implementation. Install it without the registry with fune add ./fleet.mileage-allowance-1.0.2-rust.fune, or fetch it from a terminal with fune pull fleet.mileage-allowance@1.0.2:rust.
The whole function, every language, is one file too: fleet.mileage-allowance-1.0.2.fune, 45,821 bytes, sha256 7c1c278eb23c990fe4d2ec7e7a9ad6310c79813c7d1da2ddad378f54b24ac2dd. 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 fleet.mileage-allowance
after — your function gets the result and the arguments, and returns the final result.
// fune: after fleet.mileage-allowance
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 money.add in fleet.mileage-allowance
// fune: replace money.amount in fleet.mileage-allowance
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 fleet.mileage-allowance --steps.
// fune: step fleet.mileage-allowance 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 | |
|---|---|---|---|
| 1,000 car miles in 2025-26 paid at 45p: all approved, nothing taxable | car-van, 0, 1,000, 0, £450.00, £0.00, 2025-06-01 | → | approved £450.00, paid £450.00, taxable excess £0.00, relief £0.00, miles at first rate 1,000, miles at second rate 0, first rate pence 45, second rate pence 25, passenger approve… |
| a claim that crosses 10,000 miles splits: 500 at 45p and 500 at 25p, so a flat 45p leaves 100.00 taxable | car-van, 9,500, 1,000, 0, £450.00, £0.00, 2025-06-01 | → | approved £350.00, paid £450.00, taxable excess £100.00, relief £0.00, miles at first rate 500, miles at second rate 500, first rate pence 45, second rate pence 25, passenger appro… |
| every mile past 10,000 is at 25p; nothing paid gives relief on all of it | car-van, 12,000, 200, 0, £0.00, £0.00, 2025-12-01 | → | approved £50.00, paid £0.00, taxable excess £0.00, relief £50.00, miles at first rate 0, miles at second rate 200, first rate pence 45, second rate pence 25, passenger approved £0… |
| a claim ending exactly on the 10,000th mile is all at the first rate | car-van, 9,000, 1,000, 0, £400.00, £0.00, 2025-06-01 | → | approved £450.00, paid £400.00, taxable excess £0.00, relief £50.00, miles at first rate 1,000, miles at second rate 0, first rate pence 45, second rate pence 25, passenger approv… |
| 55p from 6 April 2026: an employer still paying 45p leaves 10p a mile of relief | car-van, 0, 100, 0, £45.00, £0.00, 2026-04-06 | → | approved £55.00, paid £45.00, taxable excess £0.00, relief £10.00, miles at first rate 100, miles at second rate 0, first rate pence 55, second rate pence 25, passenger approved £… |
| 5 April 2026 is still the 2025-26 tax year at 45p | car-van, 0, 100, 0, £45.00, £0.00, 2026-04-05 | → | approved £45.00, paid £45.00, taxable excess £0.00, relief £0.00, miles at first rate 100, miles at second rate 0, first rate pence 45, second rate pence 25, passenger approved £0… |
| 40p in the last day of 2010-11 | car-van, 0, 100, 0, £40.00, £0.00, 2011-04-05 | → | approved £40.00, paid £40.00, taxable excess £0.00, relief £0.00, miles at first rate 100, miles at second rate 0, first rate pence 40, second rate pence 25, passenger approved £0… |
| 45p from 6 April 2011 | car-van, 0, 100, 0, £40.00, £0.00, 2011-04-06 | → | approved £45.00, paid £40.00, taxable excess £0.00, relief £5.00, miles at first rate 100, miles at second rate 0, first rate pence 45, second rate pence 25, passenger approved £0… |
| the first day of the scheme, 6 April 2002, is 40p | car-van, 0, 10, 0, £4.00, £0.00, 2002-04-06 | → | approved £4.00, paid £4.00, taxable excess £0.00, relief £0.00, miles at first rate 10, miles at second rate 0, first rate pence 40, second rate pence 25, passenger approved £0.00… |
| a motorcycle is 24p a mile with no threshold | motorcycle, 20,000, 100, 0, £24.00, £0.00, 2025-06-01 | → | approved £24.00, paid £24.00, taxable excess £0.00, relief £0.00, miles at first rate 100, miles at second rate 0, first rate pence 24, second rate pence 24, passenger approved £0… |
Show the other 13 tests
| Case | Arguments | Expected | |
|---|---|---|---|
| a bicycle at 20p, paid 30p a mile: 10p a mile is taxable | bicycle, 0, 50, 0, £15.00, £0.00, 2025-06-01 | → | approved £10.00, paid £15.00, taxable excess £5.00, relief £0.00, miles at first rate 50, miles at second rate 0, first rate pence 20, second rate pence 20, passenger approved £0.… |
| two passengers for 75 miles are 150 passenger miles at 5p; 1.00 paid leaves 2.50 taxable | car-van, 0, 75, 150, £33.75, £10.00, 2025-06-01 | → | approved £33.75, paid £33.75, taxable excess £0.00, relief £0.00, miles at first rate 75, miles at second rate 0, first rate pence 45, second rate pence 25, passenger approved £7.… |
| passenger payments below 5p give no relief | car-van, 0, 75, 150, £33.75, £0.00, 2025-06-01 | → | approved £33.75, paid £33.75, taxable excess £0.00, relief £0.00, miles at first rate 75, miles at second rate 0, first rate pence 45, second rate pence 25, passenger approved £7.… |
| no miles is nothing approved | car-van, 0, 0, 0, £0.00, £0.00, 2025-06-01 | → | approved £0.00, paid £0.00, taxable excess £0.00, relief £0.00, miles at first rate 0, miles at second rate 0, first rate pence 45, second rate pence 25, passenger approved £0.00,… |
| passenger payments on a motorcycle are refused | motorcycle, 0, 10, 10, £2.40, £0.00, 2025-06-01 | → | error: passenger payments apply only to cars and vans |
| no approved mileage rate before 6 April 2002 | car-van, 0, 10, 0, £0.00, £0.00, 2002-04-05 | → | error: no approved mileage rate for car-van on 2002-04-05 |
| negative miles are refused | car-van, 0, -5, 0, £0.00, £0.00, 2025-06-01 | → | error: miles must be a whole number of miles, not negative |
| fractional miles are refused | car-van, 0, 10.5, 0, £0.00, £0.00, 2025-06-01 | → | error: miles must be a whole number of miles, not negative |
| a payment in another currency is refused | car-van, 0, 10, 0, €4.50, £0.00, 2025-06-01 | → | error: paid must be in GBP |
| an unknown vehicle is refused | lorry, 0, 10, 0, £0.00, £0.00, 2025-06-01 | → | error: unknown vehicle "lorry" |
| a malformed date is refused | car-van, 0, 10, 0, £0.00, £0.00, 6/4/2025 | → | error: onDate must be an ISO date |
| a onDate with a trailing newline is refused | car-van, 0, 10, 0, £0.00, £0.00, 2026-09-16 | → | error: onDate must be an ISO date (YYYY-MM-DD), received "2026-09-16 " |
| a onDate in Arabic-Indic digits is refused | car-van, 0, 10, 0, £0.00, £0.00, ٢٠٢٦-٠٩-١٦ | → | error: onDate must be an ISO date (YYYY-MM-DD), received "٢٠٢٦-٠٩-١٦" |
More from the author
Cars and vans get the higher rate for the first 10,000 business miles in the tax year (6 April to 5 April) and 25p after that; the threshold is shared by every car and van the employee uses for the same employment. A claim is usually one month of a year, so `previousMiles` says how many car and van business miles were already paid for this tax year, and the claim is split at the threshold. 9,500 miles already claimed plus 1,000 more is 500 at 45p and 500 at 25p (350.00), not 1,000 at 45p: paying 450.00 leaves 100.00 taxable. Motorcycles and bicycles have one flat rate and no threshold.
## Rates
Dated rows in `data/amap-rates.json`, chosen by `onDate` (any date in the tax year the journeys were made):
| vehicle | from | first 10,000 miles | after | |---|---|---|---| | car or van | 2002-04-06 | 40p | 25p | | car or van | 2011-04-06 | 45p | 25p | | car or van | 2026-04-06 | 55p | 25p | | motorcycle | 2002-04-06 | 24p | 24p | | bicycle | 2002-04-06 | 20p | 20p | | passenger | 2002-04-06 | 5p a passenger mile | |
The 55p rate was announced on 21 May 2026 and applies retrospectively from 6 April 2026, so claims already paid at 45p in 2026-27 have 10p a mile of relief (or can be topped up tax-free). AMAPs began on 6 April 2002; an earlier date is an error. The table declares its effective columns, so a `history=current` build refuses dates before its horizon instead of answering them at today's rate.
## Excess and relief
`taxableExcess` is what was paid above the approved amount; the employer reports it on a P11D or payrolls it. `relief` is the approved amount above what was paid, which the employee claims as MAR. Only one of them is ever non-zero. HMRC works both out over the whole tax year, so if a year's claims are calculated one at a time, total them before reporting.
Passenger payments are 5p per mile for each fellow employee carried on business in a car or van: two passengers for 30 miles is 60 passenger miles. Anything paid above that is taxable; there is no relief below it. Passenger payments for a motorcycle or bicycle are refused.
## Edges
Money must be GBP and not negative; miles must be whole and not negative. Amounts are exact pence (miles times a whole-pence rate), so there is no rounding.
## Sources
- HMRC, "Travel — mileage and fuel rates and allowances" (rates from 2011 to 2026 and from 2026 to 2027, passenger 5p): https://www.gov.uk/government/publications/rates-and-allowances-travel-mileage-and-fuel-allowances/travel-mileage-and-fuel-rates-and-allowances - HM Treasury / HMRC, "Increasing mileage rates" (45p to 55p, retrospective from 6 April 2026): https://www.gov.uk/government/publications/increase-to-approved-mileage-allowance-payments-amaps-and-self-employed-simplified-mileage-rates/increasing-mileage-rates - HMRC, "Expenses and benefits: business travel mileage for employees' own vehicles" (rules for tax, MAR, passenger payments): https://www.gov.uk/expenses-and-benefits-business-travel-mileage/rules-for-tax and https://www.gov.uk/expenses-and-benefits-business-travel-mileage/passenger-payments - HMRC Employment Income Manual EIM31240, "statutory mileage rates: kinds of vehicle": https://www.gov.uk/hmrc-internal-manuals/employment-income-manual/eim31240 - The Approved Mileage Allowance Payments (Rates) Regulations 2011, SI 2011/896 (40p to 45p from 6 April 2011): https://www.legislation.gov.uk/uksi/2011/896/note/made - Income Tax (Earnings and Pensions) Act 2003, s230 as enacted (40p/25p, 24p, 20p) and s234 (5p passenger rate): https://www.legislation.gov.uk/ukpga/2003/1/section/230/enacted , https://www.legislation.gov.uk/ukpga/2003/1/section/234 - Finance Act 2001, Schedule 12 (the same rates from 2002-03): https://www.legislation.gov.uk/ukpga/2001/9/schedule/12/part/1/2001-05-11
1.0.1 fixes Python accepting a trailing newline or non-ASCII digits in onDate; adds tests.
## Notices
Contains public sector information licensed under the Open Government Licence v3.0 (https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/).
Legislation: Crown copyright and database right.
1.0.2 adds its attribution notices (NOTICE). The code and the tests are unchanged.
Files
| Path | Bytes |
|---|---|
| NOTICE | 236 |
| README.md | 4,611 |
| data/amap-rates.json | 1,390 |
| impl/python.py | 4,798 |
| impl/rust.rs | 6,668 |
| impl/typescript.ts | 4,654 |
| vectors.json | 15,678 |