Functional Weave
Code in TypeScript

charity.gift-aid@1.0.2

impl/typescript.ts

2,249 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 { roundDiv } from "./math_round_div.ts";  ← from math.round-div ^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 GiftAid } from "./charity_gift_aid_types.ts";
import { BASIC_RATE, BASIC_RATE_HISTORY } from "./charity_gift_aid_data.ts";  ← this capability’s own data, compiled from data/basic-rate.json into the same file by fune build

const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;

function basicRate(onDate: string): number {
  const row = BASIC_RATE.find((r) => r.validFrom <= onDate && (r.validTo === null || onDate <= r.validTo));
  if (row !== undefined) return row.basisPoints;
  // A history=current build carries only the rate in force; answering an old
  // gift with today's rate would be a quiet wrong claim, so say why instead.
  if (BASIC_RATE_HISTORY !== "full" && BASIC_RATE.length > 0) {
    const earliest = BASIC_RATE.map((r) => r.validFrom).reduce((a, b) => (b < a ? b : a));
    if (onDate < earliest) {
      throw new RangeError(
        `no Gift Aid basic rate on ${onDate}: this build was installed with history=${BASIC_RATE_HISTORY}, so it only carries rules from ${earliest}. Reinstall with history=full for earlier tax years.`
      );
    }
  }
  throw new RangeError(`no Gift Aid basic rate on ${onDate}`);
}

/**
 * The tax a charity reclaims on a Gift Aid donation. The donor is treated as
 * having given the gift after deducting basic rate tax (ITA 2007 s414), so the
 * charity reclaims donation x rate / (100% - rate): 25p per £1 at 20%. It is
 * always the UK basic rate, for Scottish and Welsh taxpayers too.
 */
export function giftAid(donation: Money, onDate: string): GiftAid {
  if (donation.currency !== "GBP") {
    throw new RangeError(`Gift Aid is claimed in GBP, received ${donation.currency}`);
  }
  if (donation.minor < 0) {
    throw new RangeError(`donation must not be negative, received ${donation.minor}`);
  }
  if (!ISO_DATE.test(onDate)) {
    throw new RangeError(`onDate must be an ISO date (YYYY-MM-DD), received "${onDate}"`);
  }
  const rate = basicRate(onDate);
  // Rounded down: a claim may never exceed the tax treated as deducted.
  const tax = roundDiv(donation.minor * rate, 10000 - rate, "down");
  return {
    donation: money(donation.minor, "GBP"),
    taxReclaimable: money(tax, "GBP"),
    gross: money(donation.minor + tax, "GBP"),
    basicRateBasisPoints: rate,
  };
}