Functional Weave
Code in Rust

charity.higher-rate-relief@1.0.0

impl/typescript.ts

4,270 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 { giftAid } from "./charity_gift_aid.ts";  ← from charity.gift-aid ^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 HigherRateRelief, type TaxRegion } from "./charity_higher_rate_relief_types.ts";
import { type IncomeTaxBand, INCOME_TAX_BANDS, INCOME_TAX_BANDS_HISTORY } from "./payroll_income_tax_data.ts";  ← payroll.income-tax’s rule data (^1.0.0) · built alongside by fune

const REGIONS = ["rest-of-uk", "scotland", "wales"];

function bandsFor(region: string, onDate: string): IncomeTaxBand[] {
  const rows = INCOME_TAX_BANDS.filter(
    (b) => b.region === region && b.validFrom <= onDate && (b.validTo === null || onDate <= b.validTo)
  );
  if (rows.length === 0) {
    // A history=current build only carries this year's bands; an older year
    // would otherwise be answered with the wrong bands.
    const froms = INCOME_TAX_BANDS.filter((b) => b.region === region).map((b) => b.validFrom);
    if (INCOME_TAX_BANDS_HISTORY !== "full" && froms.length > 0) {
      const earliest = froms.reduce((a, b) => (b < a ? b : a));
      if (onDate < earliest) {
        throw new RangeError(
          `no income tax bands for ${region} on ${onDate}: this build was installed with history=${INCOME_TAX_BANDS_HISTORY}, so it only carries rules from ${earliest}. Reinstall with history=full for earlier tax years.`
        );
      }
    }
    throw new RangeError(`no income tax bands for ${region} on ${onDate}`);
  }
  return [...rows].sort((a, b) => a.band - b.band);
}

function basicBandIndex(bands: IncomeTaxBand[], region: string, onDate: string): number {
  const index = bands.findIndex((b) => b.basicRate);
  if (index < 0) throw new RangeError(`no basic rate band for ${region} on ${onDate}`);
  return index;
}

/** Tax on income in pence, times 10000 (pence x basis points), unrounded. */
function scaledTax(income: number, bands: IncomeTaxBand[], extendFrom: number, extension: number): number {
  let total = 0;
  let lower = 0;
  for (let i = 0; i < bands.length && income > lower; i++) {
    const band = bands[i];
    const upper = band.upTo === null ? null : band.upTo * 100 + (i >= extendFrom ? extension : 0);
    const top = upper === null || income < upper ? income : upper;
    if (top > lower) total += (top - lower) * band.basisPoints;
    if (upper === null) break;
    lower = upper;
  }
  return total;
}

/**
 * The extra relief a Gift Aid donor gets through their own tax: ITA 2007
 * s414(2)(b) raises the basic rate limit and the higher rate limit (for a
 * Scottish taxpayer, the Scottish basic rate limit and every limit above it,
 * but not the starter rate limit) by the grossed up donations. Income moved
 * from a higher band into a lower one is the relief.
 */
export function higherRateRelief(taxableIncome: Money, donations: Money, region: TaxRegion, onDate: string): HigherRateRelief {
  if (taxableIncome.currency !== "GBP") {
    throw new RangeError(`taxableIncome must be in GBP, received ${taxableIncome.currency}`);
  }
  if (taxableIncome.minor < 0) {
    throw new RangeError(`taxableIncome must not be negative, received ${taxableIncome.minor}`);
  }
  if (!REGIONS.includes(region)) {
    throw new RangeError(`unknown tax region "${region}": expected rest-of-uk, scotland or wales`);
  }
  const grossed = giftAid(donations, onDate);
  const bands = bandsFor(region, onDate);
  const basic = basicBandIndex(bands, region, onDate);
  const ukBands = bandsFor("rest-of-uk", onDate);
  const ukBasic = ukBands[basicBandIndex(ukBands, "rest-of-uk", onDate)];
  if (bands[basic].basisPoints !== ukBasic.basisPoints) {
    // s414A then adjusts the donor's tax by the rate difference; not modelled.
    throw new RangeError(
      `the ${region} basic rate differs from the UK basic rate on ${onDate}; the section 414A adjustment is not supported`
    );
  }
  const before = scaledTax(taxableIncome.minor, bands, basic, 0);
  const after = scaledTax(taxableIncome.minor, bands, basic, grossed.gross.minor);
  const taxBefore = Math.floor(before / 10000);
  const extra = Math.floor((before - after) / 10000);
  return {
    grossDonations: grossed.gross,
    basicRateRelief: grossed.taxReclaimable,
    taxBefore: money(taxBefore, "GBP"),
    taxAfter: money(taxBefore - extra, "GBP"),
    extraRelief: money(extra, "GBP"),
    netCost: money(donations.minor - extra, "GBP"),
  };
}