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"),
};
}