Functional Weave
Code in Python

charity.gift-aid-eligibility@1.0.2

impl/typescript.ts

3,509 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 { type Money, money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { type GiftAidEligibility } from "./charity_gift_aid_eligibility_types.ts";
import { type BenefitLimit, BENEFIT_LIMITS, BENEFIT_LIMITS_HISTORY } from "./charity_gift_aid_eligibility_data.ts";  ← this capability’s own data, compiled from data/benefit-limits.json into the same file by fune build

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

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

function checkGbp(name: string, amount: Money): void {
  if (amount.currency !== "GBP") {
    throw new RangeError(`Gift Aid amounts must be in GBP, received ${amount.currency} for ${name}`);
  }
}

/**
 * Whether a donation qualifies for Gift Aid (ITA 2007 ss416-418). The benefit
 * test is done on the exact limit, scaled by 10000 so that 25% or 5% of an odd
 * number of pence is never rounded before the comparison: a benefit of 25p on
 * a £1.01 gift (limit 25.25p) passes, where a rounded-down limit would fail it.
 */
export function giftAidEligibility(
  donation: Money,
  benefitValue: Money,
  priorBenefitsThisYear: Money,
  hasDeclaration: boolean,
  donorPaysEnoughTax: boolean,
  onDate: string
): GiftAidEligibility {
  checkGbp("donation", donation);
  checkGbp("benefitValue", benefitValue);
  checkGbp("priorBenefitsThisYear", priorBenefitsThisYear);
  if (donation.minor <= 0) throw new RangeError(`donation must be positive, received ${donation.minor}`);
  if (benefitValue.minor < 0) throw new RangeError(`benefitValue must not be negative, received ${benefitValue.minor}`);
  if (priorBenefitsThisYear.minor < 0) {
    throw new RangeError(`priorBenefitsThisYear must not be negative, received ${priorBenefitsThisYear.minor}`);
  }
  if (!ISO_DATE.test(onDate)) throw new RangeError(`onDate must be an ISO date (YYYY-MM-DD), received "${onDate}"`);
  const rule = limitsOn(onDate);

  // The variable limit, in pence x 10000.
  let scaledLimit: number;
  if (donation.minor <= rule.smallGiftUpTo) {
    scaledLimit = donation.minor * rule.smallGiftBasisPoints;
  } else if (rule.middleGiftUpTo !== null && rule.middleGiftFixed !== null && donation.minor <= rule.middleGiftUpTo) {
    scaledLimit = rule.middleGiftFixed * 10000;
  } else {
    scaledLimit = rule.excessFixed * 10000 + (donation.minor - rule.excessFrom) * rule.excessBasisPoints;
  }

  const reasons: string[] = [];
  if (!hasDeclaration) reasons.push("no-declaration");
  if (!donorPaysEnoughTax) reasons.push("insufficient-tax");
  // "exceeds" and "is more than": a benefit exactly at a limit is allowed.
  if (benefitValue.minor * 10000 > scaledLimit) reasons.push("benefit-over-limit");
  if (benefitValue.minor + priorBenefitsThisYear.minor > rule.annualLimit) reasons.push("annual-benefits-over-limit");

  return {
    eligible: reasons.length === 0,
    reasons,
    benefitLimit: money(Math.floor(scaledLimit / 10000), "GBP"),
    annualBenefitLimit: money(rule.annualLimit, "GBP"),
  };
}