Functional Weave
Code in TypeScript

construction.vat-reverse-charge@1.0.0

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 };
}