Functional Weave
Code in TypeScript

fleet.company-car-bik@1.0.1

impl/typescript.ts

6,940 bytes · the TypeScript 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 { daysBetween } from "./dates_days_between.ts";  ← from dates.days-between ^1.0.0 · built alongside by fune
import { ukTaxYear } from "./dates_uk_tax_year.ts";  ← from dates.uk-tax-year ^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 { BIK_BANDS, BIK_BANDS_HISTORY, BIK_LIMITS, BIK_LIMITS_HISTORY, FUEL_MULTIPLIERS, FUEL_MULTIPLIERS_HISTORY } from "./fleet_company_car_bik_data.ts";  ← this capability’s own data, compiled from data/bik-bands.json into the same file by fune build
import { type CompanyCarBik } from "./fleet_company_car_bik_types.ts";

const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
const MAX_SAFE = 9007199254740991;
// Cars first registered before this date keep their NEDC figures, and in
// 2021-22 their own (one point higher) column of the table.
const WLTP_FROM = "2020-04-06";

interface Dated {
  validFrom: string;
  validTo: string | null;
}

function inForce(row: Dated, onDate: string): boolean {
  return row.validFrom <= onDate && (row.validTo === null || onDate <= row.validTo);
}

// A history=current build has dropped old rows; say so rather than answering
// an old tax year with today's figures.
function missing(what: string, label: string, rows: readonly Dated[], history: string, onDate: string): never {
  if (history !== "full") {
    let earliest: string | null = null;
    for (const row of rows) if (earliest === null || row.validFrom < earliest) earliest = row.validFrom;
    if (earliest !== null && onDate < earliest) {
      throw new RangeError(
        `no ${what} for tax year ${label}: this build was installed with history=${history}, ` +
          `so it only carries figures from ${earliest}. Reinstall with history=full for earlier years.`
      );
    }
  }
  throw new RangeError(`no ${what} for tax year ${label}`);
}

function checkGbp(name: string, amount: Money): void {
  if (amount.currency !== "GBP") {
    throw new RangeError(`company car benefit is worked in GBP; ${name} is in ${amount.currency}`);
  }
  if (amount.minor < 0) {
    throw new RangeError(`${name} must not be negative, received ${amount.minor}`);
  }
}

function checkWhole(name: string, value: number, unit: string): void {
  if (!Number.isInteger(value) || value < 0) {
    throw new RangeError(`${name} must be a whole number of ${unit}, not negative, received ${value}`);
  }
}

function checkDate(name: string, value: string): void {
  if (!ISO_DATE.test(value)) {
    throw new RangeError(`${name} must be an ISO date (YYYY-MM-DD), received "${value}"`);
  }
}

// price x basis points x days available / (10000 x days in year), rounded down
// to the penny: one rounding, and never more benefit than the law charges.
function proportion(minor: number, basisPoints: number, daysAvailable: number, daysInYear: number): number {
  const product = minor * basisPoints * daysAvailable;
  if (product > MAX_SAFE) {
    throw new RangeError("the benefit calculation exceeds 2^53 - 1; the list price is too large");
  }
  return roundDiv(product, 10000 * daysInYear, "down");
}

/**
 * The taxable benefit of a company car for one UK tax year (ITEPA 2003 s121).
 *
 * Price less capital contributions (at most 5,000.00), times the appropriate
 * percentage for the car's CO2 figure and electric range in that year, cut
 * pro rata for days the car was unavailable, less what the employee paid for
 * private use. Free fuel adds the year's fuel benefit multiplier times the same
 * percentage, cut for the same days.
 */
export function companyCarBik(
  listPrice: Money,
  capitalContributions: Money,
  co2: number,
  electricRangeMiles: number,
  dieselSupplement: boolean,
  firstRegistered: string,
  onDate: string,
  unavailableDays: number,
  privateUseContributions: Money,
  freeFuel: boolean
): CompanyCarBik {
  checkGbp("listPrice", listPrice);
  checkGbp("capitalContributions", capitalContributions);
  checkGbp("privateUseContributions", privateUseContributions);
  checkWhole("co2", co2, "g/km");
  checkWhole("electricRangeMiles", electricRangeMiles, "miles");
  checkDate("firstRegistered", firstRegistered);
  checkDate("onDate", onDate);
  if (dieselSupplement && co2 === 0) {
    throw new RangeError("a zero-emission car cannot take the diesel supplement");
  }

  const year = ukTaxYear(onDate);
  if (firstRegistered > year.end) {
    throw new RangeError(`firstRegistered (${firstRegistered}) is after the end of tax year ${year.label}`);
  }
  const daysInYear = daysBetween(year.start, year.end) + 1;
  if (!Number.isInteger(unavailableDays) || unavailableDays < 0 || unavailableDays > daysInYear) {
    throw new RangeError(`unavailableDays must be a whole number from 0 to ${daysInYear}, received ${unavailableDays}`);
  }
  const daysAvailable = daysInYear - unavailableDays;
  const registration = firstRegistered < WLTP_FROM ? "before-2020-04-06" : "from-2020-04-06";
  const applies = (row: { registration: string }) => row.registration === "any" || row.registration === registration;

  const limit = BIK_LIMITS.find((row) => applies(row) && inForce(row, onDate));
  if (limit === undefined) missing("company car benefit rates", year.label, BIK_LIMITS, BIK_LIMITS_HISTORY, onDate);

  let basisPoints: number;
  if (co2 >= 75) {
    // s139(3): one point per whole 5 g/km above 75, up to the year's maximum.
    basisPoints = Math.min(limit.from75BasisPoints + 100 * Math.floor((co2 - 75) / 5), limit.maximumBasisPoints);
  } else {
    const band = BIK_BANDS.find(
      (row) =>
        applies(row) &&
        inForce(row, onDate) &&
        row.co2From <= co2 &&
        co2 <= row.co2To &&
        row.rangeFrom <= electricRangeMiles &&
        (row.rangeTo === null || electricRangeMiles <= row.rangeTo)
    );
    if (band === undefined) missing("company car benefit band", year.label, BIK_BANDS, BIK_BANDS_HISTORY, onDate);
    basisPoints = band.basisPoints;
  }
  if (dieselSupplement) {
    basisPoints = Math.min(basisPoints + limit.dieselSupplementBasisPoints, limit.maximumBasisPoints);
  }

  const counted = Math.min(capitalContributions.minor, limit.capitalContributionCapMinor);
  const price = money(Math.max(listPrice.minor - counted, 0), "GBP");
  const provisional = proportion(price.minor, basisPoints, daysAvailable, daysInYear);
  const carBenefit = money(Math.max(provisional - privateUseContributions.minor, 0), "GBP");

  let fuelBenefit = money(0, "GBP");
  // s149(4): electricity, and energy for a car that cannot emit CO2, is not fuel.
  if (freeFuel && co2 > 0) {
    const multiplier = FUEL_MULTIPLIERS.find((row) => inForce(row, onDate));
    if (multiplier === undefined) {
      missing("car fuel benefit multiplier", year.label, FUEL_MULTIPLIERS, FUEL_MULTIPLIERS_HISTORY, onDate);
    }
    fuelBenefit = money(proportion(multiplier.pounds * 100, basisPoints, daysAvailable, daysInYear), "GBP");
  }

  return {
    taxYear: year.label,
    daysInYear,
    daysAvailable,
    price,
    appropriateBasisPoints: basisPoints,
    carBenefit,
    fuelBenefit,
    total: addMoney(carBenefit, fuelBenefit),
  };
}