Functional Weave
Code in Rust

fleet.mileage-allowance@1.0.2

impl/python.py

4,798 bytes · the Python implementation · view raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import re
from typing import Optional, Tuple

from .fleet_mileage_allowance_data import AMAP_RATES, AMAP_RATES_HISTORY  ← this capability’s own data, compiled from data/amap-rates.json into the same file by fune build
from .fleet_mileage_allowance_types import MileageAllowance, MileageVehicle
from .money_add import subtract_money  ← from money.add ^1.0.0 · built alongside by fune
from .money_amount import Money, money  ← from money.amount ^1.0.0 · built alongside by fune

ISO_DATE = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}")


def _rate_on(vehicle: str, on_date: str):
    best = None
    earliest: Optional[str] = None
    for rule in AMAP_RATES:
        if rule.vehicle != vehicle:
            continue
        if earliest is None or rule.valid_from < earliest:
            earliest = rule.valid_from
        if on_date < rule.valid_from:
            continue
        if rule.valid_to is not None and on_date > rule.valid_to:
            continue
        if best is None or rule.valid_from > best.valid_from:
            best = rule
    if best is not None:
        return best
    # 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" and earliest is not None and on_date < earliest:
        raise ValueError(
            "no approved mileage rate for %s on %s: this build was installed with history=%s, "
            "so it only carries rates from %s. Reinstall with history=full for older claims."
            % (vehicle, on_date, AMAP_RATES_HISTORY, earliest)
        )
    raise ValueError("no approved mileage rate for %s on %s" % (vehicle, on_date))


def _whole_miles(name: str, value: int) -> None:
    if isinstance(value, bool) or not isinstance(value, int) or value < 0:
        raise ValueError("%s must be a whole number of miles, not negative, received %s" % (name, value))


def _gbp(name: str, value: Money) -> None:
    if value.currency != "GBP":
        raise ValueError("%s must be in GBP, received %s" % (name, value.currency))
    if value.minor < 0:
        raise ValueError("%s must not be negative, received %d" % (name, value.minor))


def _excess_and_shortfall(paid: Money, approved: Money) -> Tuple[Money, Money]:
    difference = subtract_money(paid, approved)
    if difference.minor > 0:
        return difference, money(0, "GBP")
    return money(0, "GBP"), money(-difference.minor, "GBP")


def mileage_allowance(
    vehicle: MileageVehicle,
    previous_miles: int,
    miles: int,
    passenger_miles: int,
    paid: Money,
    passenger_paid: Money,
    on_date: str,
) -> MileageAllowance:
    """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.
    """
    if vehicle not in ("car-van", "motorcycle", "bicycle"):
        raise ValueError('unknown vehicle "%s": 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 not isinstance(on_date, str) or not ISO_DATE.fullmatch(on_date):
        raise ValueError('onDate must be an ISO date (YYYY-MM-DD), received "%s"' % (on_date,))
    if vehicle != "car-van" and (passenger_miles > 0 or passenger_paid.minor > 0):
        raise ValueError("passenger payments apply only to cars and vans")

    rate = _rate_on(vehicle, on_date)
    miles_at_first_rate = miles
    miles_at_second_rate = 0
    if rate.threshold_miles is not None:
        first_left = max(0, rate.threshold_miles - previous_miles)
        miles_at_first_rate = min(miles, first_left)
        miles_at_second_rate = miles - miles_at_first_rate
    approved = money(
        miles_at_first_rate * rate.first_rate_pence + miles_at_second_rate * rate.second_rate_pence, "GBP"
    )
    taxable_excess, relief = _excess_and_shortfall(paid, approved)

    passenger_approved = money(0, "GBP")
    if vehicle == "car-van":
        passenger_approved = money(passenger_miles * _rate_on("passenger", on_date).first_rate_pence, "GBP")
    # There is no relief for passenger payments below 5p, only tax above it.
    passenger_taxable_excess, _ = _excess_and_shortfall(passenger_paid, passenger_approved)

    return MileageAllowance(
        approved=approved,
        paid=paid,
        taxable_excess=taxable_excess,
        relief=relief,
        miles_at_first_rate=miles_at_first_rate,
        miles_at_second_rate=miles_at_second_rate,
        first_rate_pence=rate.first_rate_pence,
        second_rate_pence=rate.second_rate_pence,
        passenger_approved=passenger_approved,
        passenger_taxable_excess=passenger_taxable_excess,
    )