Functional Weave
Code in TypeScript

charity.gasds@1.0.0

impl/typescript.ts

3,521 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 GasdsClaim } from "./charity_gasds_types.ts";
import { type GasdsLimit, GASDS_LIMITS, GASDS_LIMITS_HISTORY } from "./charity_gasds_data.ts";  ← this capability’s own data, compiled from data/gasds-limits.json into the same file by fune build

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

function limitsOn(onDate: string): GasdsLimit {
  const row = GASDS_LIMITS.find((r) => r.validFrom <= onDate && (r.validTo === null || onDate <= r.validTo));
  if (row !== undefined) return row;
  if (GASDS_LIMITS_HISTORY !== "full" && GASDS_LIMITS.length > 0) {
    const earliest = GASDS_LIMITS.map((r) => r.validFrom).reduce((a, b) => (b < a ? b : a));
    if (onDate < earliest) {
      throw new RangeError(
        `no GASDS limits on ${onDate}: this build was installed with history=${GASDS_LIMITS_HISTORY}, so it only carries rules from ${earliest}. Reinstall with history=full for earlier tax years.`
      );
    }
  }
  throw new RangeError(`no GASDS limits on ${onDate}`);
}

function checkAmount(name: string, amount: Money): void {
  if (amount.currency !== "GBP") throw new RangeError(`GASDS amounts must be in GBP, received ${amount.currency} for ${name}`);
  if (amount.minor < 0) throw new RangeError(`${name} must not be negative, received ${amount.minor}`);
}

/**
 * The GASDS top-up for one tax year (Small Charitable Donations Act 2012 s1):
 * paid on small donations up to the lower of 10 times the Gift Aid donations
 * claimed in the same year (the matching rule) and the specified amount,
 * which for a charity active in more than one community building may be the
 * sum of each building's local donations, up to £8,000 each (s6).
 */
export function gasds(
  smallDonations: Money,
  giftAidDonations: Money,
  communityBuildingDonations: readonly Money[],
  onDate: string
): GasdsClaim {
  checkAmount("smallDonations", smallDonations);
  checkAmount("giftAidDonations", giftAidDonations);
  communityBuildingDonations.forEach((d) => checkAmount("communityBuildingDonations", d));
  if (!ISO_DATE.test(onDate)) throw new RangeError(`onDate must be an ISO date (YYYY-MM-DD), received "${onDate}"`);
  const rule = limitsOn(onDate);
  if (communityBuildingDonations.length > 0 && !rule.communityBuildings) {
    throw new RangeError(`community building claims before 6 April 2017 are not supported, received a claim for ${onDate}`);
  }
  const inBuildings = communityBuildingDonations.reduce((sum, d) => sum + d.minor, 0);
  if (inBuildings > smallDonations.minor) {
    throw new RangeError(
      `communityBuildingDonations total ${inBuildings}, more than smallDonations of ${smallDonations.minor}`
    );
  }

  let specified = rule.specifiedAmount;
  let usedCommunityBuildings = false;
  // s6 applies only to a charity active in more than one community building.
  if (communityBuildingDonations.length >= 2) {
    const buildings = communityBuildingDonations.reduce((sum, d) => sum + Math.min(d.minor, rule.specifiedAmount), 0);
    if (buildings > specified) {
      specified = buildings;
      usedCommunityBuildings = true;
    }
  }
  const maximum = Math.min(10 * giftAidDonations.minor, specified);
  const eligible = Math.min(smallDonations.minor, maximum);
  return {
    eligibleDonations: money(eligible, "GBP"),
    topUp: giftAid(money(eligible, "GBP"), onDate).taxReclaimable,
    maximumDonations: money(maximum, "GBP"),
    specifiedAmount: money(specified, "GBP"),
    smallDonationLimit: money(rule.smallDonationLimit, "GBP"),
    usedCommunityBuildings,
  };
}