construction.vat-reverse-charge@1.0.1
impl/typescript.ts
3,178 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 { REVERSE_CHARGE_RULES, REVERSE_CHARGE_RULES_HISTORY, REVERSE_CHARGE_RULES_HORIZON } from "./construction_vat_reverse_charge_data.ts"; ← this capability’s own data, compiled from data/reverse-charge.json into the same file by fune build
import { type ReverseChargeDecision, type ReverseChargeReason, type ReverseChargeSupply } from "./construction_vat_reverse_charge_types.ts";
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
function notApplied(reason: ReverseChargeReason): ReverseChargeDecision {
return { applies: false, reason, invoiceWording: null };
}
/**
* Whether the UK VAT domestic reverse charge for building and construction
* services (VATA 1994 s.55A) applies to one supply.
*
* The conditions are checked in a fixed order and the first that fails is the
* reason, so the answer is the same in every language and explains itself.
*/
export function domesticReverseCharge(supply: ReverseChargeSupply): ReverseChargeDecision {
if (typeof supply.supplyDate !== "string" || !ISO_DATE.test(supply.supplyDate)) {
throw new RangeError(`supplyDate must be an ISO date (YYYY-MM-DD), received "${supply.supplyDate}"`);
}
const share = supply.reverseChargeShareBasisPoints;
if (!Number.isInteger(share)) {
throw new TypeError(`reverseChargeShareBasisPoints must be an integer, received ${share}`);
}
if (share < 0 || share > 10000) {
throw new RangeError(`reverseChargeShareBasisPoints must be between 0 and 10000, received ${share}`);
}
if (!["standard", "reduced", "zero", "exempt"].includes(supply.vatCategory)) {
throw new RangeError(`unknown vatCategory "${supply.vatCategory}": expected standard, reduced, zero or exempt`);
}
const date = supply.supplyDate;
let rule: (typeof REVERSE_CHARGE_RULES)[number] | null = null;
for (const row of REVERSE_CHARGE_RULES) {
if (row.jurisdiction !== "GB" || date < row.validFrom) continue;
if (row.validTo !== null && date > row.validTo) continue;
if (rule === null || row.validFrom > rule.validFrom) rule = row;
}
if (rule === null) {
// A pruned build must not answer "before the reverse charge started" for a
// date it simply no longer carries rules for.
if (REVERSE_CHARGE_RULES_HORIZON !== null && date < REVERSE_CHARGE_RULES_HORIZON) {
throw new RangeError(
`no reverse charge rule for ${date}: this build was installed with history=${REVERSE_CHARGE_RULES_HISTORY}, ` +
`so it only carries rules from ${REVERSE_CHARGE_RULES_HORIZON}. Reinstall with history=full.`
);
}
return notApplied("before-start");
}
if (!supply.supplierVatRegistered) return notApplied("supplier-not-vat-registered");
if (!supply.customerVatRegistered) return notApplied("customer-not-vat-registered");
if (!supply.reportedUnderCis) return notApplied("not-reported-under-cis");
if (supply.vatCategory !== "standard" && supply.vatCategory !== "reduced") return notApplied("not-standard-or-reduced");
if (supply.employmentBusiness) return notApplied("employment-business");
if (supply.endUserNotified) return notApplied("end-user-notified");
if (share <= rule.disregardBasisPoints) return notApplied("five-percent-disregard");
return { applies: true, reason: "reverse-charge", invoiceWording: rule.invoiceWording };
}