Functional Weave
Code in TypeScript

finance.tax.vat-return Unreviewed

UK nine-box VAT return totals for a period from categorised sales and purchases (VAT Notice 700/12).

1.0.1 · published 2026-10-03 by charlie · Anterra

Pinned by 19 tests, run in TypeScript, Python and Rust.

Unreviewed. This capability’s implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified chartered accountant or VAT specialist has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.

Not professional advice. This capability calculates tax figures from published rules. It is a software component for developers, not tax advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed in its README, and have a chartered accountant or VAT specialist review how you use it, before anyone relies on the output. Provided “as is” under its licence, without warranty.

What it does

Status: needs review by a qualified accountant before it is published.

The nine boxes of a UK VAT return, totalled from the period's transactions. The transactions arrive already categorised (the VAT on each is what was invoiced or self-accounted), and this does the part that goes wrong in spreadsheets: which box each kind of transaction feeds, credit notes, the reverse charge, and the whole-pound boxes.

For example

  • vatReturn(transactions ×11, 2026-04-01, 2026-06-30) → box1 £250.00, box2 £0.00, box3 £250.00, box4 £130.00, box5 £120.00, box6 £1,020.00, box7 £650.00, box8 £0.00, box9 £0.00, repayment false, transactions 9 a quarter with sales, a credit note, reverse charges, import VAT and out-of-period items
  • vatReturn(transactions ×2, 2026-04-01, 2026-06-30) → box1 £0.00, box2 £40.20, box3 £40.20, box4 £40.20, box5 £0.00, box6 £123.00, box7 £200.00, box8 £123.00, box9 £200.00, repayment false, transactions 2 Northern Ireland goods to and from the EU fill boxes 2, 8 and 9
  • vatReturn(transactions ×2, 2026-04-01, 2026-06-30) → box1 £2.00, box2 £0.00, box3 £2.00, box4 £200.00, box5 £198.00, box6 £10.00, box7 £1,000.00, box8 £0.00, box9 £0.00, repayment true, transactions 2 more VAT reclaimed than due is a repayment, and box 5 has no sign

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

export function vatReturn(transactions: readonly VatTransaction[], periodStart: string, periodEnd: string): VatReturn
transactionsVatTransaction[]sales and purchases, each with its tax point; those outside the period are left out
periodStartdatefirst day of the VAT period
periodEnddatelast day of the VAT period, inclusive
returnsVatReturn

The types it declares, generated into your project

export type VatDirection = "sale" | "purchase";

export type VatTreatment = "taxable" | "exempt" | "outside-scope" | "domestic-reverse-charge" | "services-reverse-charge" | "postponed-import-vat" | "ni-eu-goods";

/** One sale or purchase, as the ledger categorised it. */
export interface VatTransaction {
  /** tax point */
  readonly date: string;
  readonly direction: VatDirection;
  readonly treatment: VatTreatment;
  /** excluding VAT, GBP; negative for a credit note */
  readonly net: Money;
  /** the VAT on it (for reverse charges, the VAT self-accounted); negative for a credit note */
  readonly vat: Money;
}

/** The nine boxes. Boxes 1 to 5 in pounds and pence; boxes 6 to 9 in whole pounds. */
export interface VatReturn {
  /** VAT due on sales and other outputs */
  readonly box1: Money;
  /** VAT due on acquisitions of goods into Northern Ireland from the EU */
  readonly box2: Money;
  /** total VAT due: box 1 plus box 2 */
  readonly box3: Money;
  /** VAT reclaimed on purchases and other inputs */
  readonly box4: Money;
  /** net VAT: the difference between boxes 3 and 4, never negative */
  readonly box5: Money;
  /** total value of sales and other outputs excluding VAT, pence dropped */
  readonly box6: Money;
  /** total value of purchases and other inputs excluding VAT, pence dropped */
  readonly box7: Money;
  /** goods supplied from Northern Ireland to the EU, pence dropped */
  readonly box8: Money;
  /** goods acquired into Northern Ireland from the EU, pence dropped */
  readonly box9: Money;
  /** true when box 4 exceeds box 3, so HMRC owes the business */
  readonly repayment: boolean;
  /** how many transactions fell in the period */
  readonly transactions: number;
}

Your code names it in one line, in the file that uses it

import { vatReturn } from "#fune/finance.tax.vat-return@^1";
impl/typescript.ts · 111 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import { daysBetween } from "./dates_days_between.ts";  ← from dates.days-between ^1.0.0 · built alongside by fune
import { roundDiv } from "./math_round_div.ts";  ← from math.round-div ^1.0.0 · built alongside by fune
import { addMoney } from "./money_add.ts";  ← from money.add ^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 VatReturn, type VatTransaction } from "./finance_tax_vat_return_types.ts";

const ZERO_VAT_SALES = ["exempt", "outside-scope", "domestic-reverse-charge", "services-reverse-charge", "ni-eu-goods"];
const ZERO_VAT_PURCHASES = ["exempt", "outside-scope"];
const TREATMENTS = [
  "taxable",
  "exempt",
  "outside-scope",
  "domestic-reverse-charge",
  "services-reverse-charge",
  "postponed-import-vat",
  "ni-eu-goods",
];

/** Drop the pence, towards zero: boxes 6 to 9 are whole pounds. */
function wholePounds(amount: Money): Money {
  return money(roundDiv(amount.minor, 100, "down") * 100, amount.currency);
}

/**
 * The nine boxes of a UK VAT return for a period.
 *
 * Each transaction feeds the boxes its direction and treatment call for
 * (VAT Notice 700/12 sections 3 and 4.6); boxes 6 to 9 drop the pence after
 * totalling.
 */
export function vatReturn(transactions: readonly VatTransaction[], periodStart: string, periodEnd: string): VatReturn {
  if (daysBetween(periodStart, periodEnd) < 0) {
    throw new RangeError(`VAT return period ends ${periodEnd}, before it starts ${periodStart}`);
  }
  const box = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0].map(() => money(0, "GBP"));
  let count = 0;
  transactions.forEach((t, index) => {
    const n = index + 1;
    if (t.direction !== "sale" && t.direction !== "purchase") {
      throw new RangeError(`transaction ${n}: unknown direction "${t.direction}"`);
    }
    if (!TREATMENTS.includes(t.treatment)) {
      throw new RangeError(`transaction ${n}: unknown VAT treatment "${t.treatment}"`);
    }
    if (t.net.currency !== "GBP" || t.vat.currency !== "GBP") {
      throw new RangeError(`transaction ${n}: amounts must be in GBP`);
    }
    const sale = t.direction === "sale";
    if (sale && t.treatment === "postponed-import-vat") {
      throw new RangeError(`transaction ${n}: postponed-import-vat applies to purchases only`);
    }
    if (t.vat.minor !== 0 && (sale ? ZERO_VAT_SALES : ZERO_VAT_PURCHASES).includes(t.treatment)) {
      throw new RangeError(`transaction ${n}: ${t.treatment} ${t.direction}s must have zero VAT`);
    }
    const inPeriod = daysBetween(periodStart, t.date) >= 0 && daysBetween(t.date, periodEnd) >= 0;
    if (!inPeriod) return;
    count += 1;

    const add = (b: number, amount: Money): void => {
      box[b] = addMoney(box[b], amount);
    };
    if (sale) {
      if (t.treatment === "taxable") add(1, t.vat);
      if (t.treatment !== "outside-scope") add(6, t.net);
      if (t.treatment === "ni-eu-goods") add(8, t.net);
    } else {
      switch (t.treatment) {
        case "taxable":
          add(4, t.vat);
          add(7, t.net);
          break;
        case "exempt":
          add(7, t.net);
          break;
        case "outside-scope":
          break;
        case "services-reverse-charge":
          add(6, t.net);
        // falls through: the customer also accounts as for a domestic reverse charge
        case "domestic-reverse-charge":
        case "postponed-import-vat":
          add(1, t.vat);
          add(4, t.vat);
          add(7, t.net);
          break;
        case "ni-eu-goods":
          add(2, t.vat);
          add(4, t.vat);
          add(7, t.net);
          add(9, t.net);
          break;
      }
    }
  });

  const box3 = addMoney(box[1], box[2]);
  const difference = box3.minor - box[4].minor;
  return {
    box1: box[1],
    box2: box[2],
    box3,
    box4: box[4],
    box5: money(difference < 0 ? -difference : difference, "GBP"),
    box6: wholePounds(box[6]),
    box7: wholePounds(box[7]),
    box8: wholePounds(box[8]),
    box9: wholePounds(box[9]),
    repayment: difference < 0,
    transactions: count,
  };
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 4 dependencies, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:

fune add finance.tax.vat-return
Download for TypeScript finance.tax.vat-return-1.0.1-typescript.fune · 27,682 bytes sha256 34390823a46210312071a5c76ddb4fd54728e2998b16d8add7631409adad4824

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./finance.tax.vat-return-1.0.1-typescript.fune, or fetch it from a terminal with fune pull finance.tax.vat-return@1.0.1:typescript.

The whole function, every language, is one file too: finance.tax.vat-return-1.0.1.fune, 37,576 bytes, sha256 81f3e3501c2945d9de2e7f02bbad017010b8b7bccd8e0ef52a5423d4419a8dcd. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before finance.tax.vat-return

after — your function gets the result and the arguments, and returns the final result.

// fune: after finance.tax.vat-return

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

// fune: replace dates.days-between in finance.tax.vat-return
// fune: replace math.round-div in finance.tax.vat-return
// fune: replace money.add in finance.tax.vat-return
// fune: replace money.amount in finance.tax.vat-return

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show finance.tax.vat-return --steps.

// fune: step finance.tax.vat-return after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
a quarter with sales, a credit note, reverse charges, import VAT and out-of-period items transactions ×11, 2026-04-01, 2026-06-30 → box1 £250.00, box2 £0.00, box3 £250.00, box4 £130.00, box5 £120.00, box6 £1,020.00, box7 £650.00, box8 £0.00, box9 £0.00, repayment false, transactions 9
Northern Ireland goods to and from the EU fill boxes 2, 8 and 9 transactions ×2, 2026-04-01, 2026-06-30 → box1 £0.00, box2 £40.20, box3 £40.20, box4 £40.20, box5 £0.00, box6 £123.00, box7 £200.00, box8 £123.00, box9 £200.00, repayment false, transactions 2
more VAT reclaimed than due is a repayment, and box 5 has no sign transactions ×2, 2026-04-01, 2026-06-30 → box1 £2.00, box2 £0.00, box3 £2.00, box4 £200.00, box5 £198.00, box6 £10.00, box7 £1,000.00, box8 £0.00, box9 £0.00, repayment true, transactions 2
boxes 6 and 7 drop the pence rather than round them transactions ×2, 2026-04-01, 2026-06-30 → box1 £200.00, box2 £0.00, box3 £200.00, box4 £0.00, box5 £200.00, box6 £999.00, box7 £0.00, box8 £0.00, box9 £0.00, repayment false, transactions 2
pence are dropped after totalling, not per line transactions ×2, 2026-04-01, 2026-06-30 → box1 £0.00, box2 £0.00, box3 £0.00, box4 £0.00, box5 £0.00, box6 £1,001.00, box7 £0.00, box8 £0.00, box9 £0.00, repayment false, transactions 2
a period of only a credit note: negative boxes, dropped towards zero transactions ×1, 2026-04-01, 2026-06-30 → box1 -£24.69, box2 £0.00, box3 -£24.69, box4 £0.00, box5 £24.69, box6 -£123.00, box7 £0.00, box8 £0.00, box9 £0.00, repayment true, transactions 1
transactions on the first and last days are in the period transactions ×2, 2026-04-01, 2026-06-30 → box1 £4.00, box2 £0.00, box3 £4.00, box4 £0.00, box5 £4.00, box6 £20.00, box7 £0.00, box8 £0.00, box9 £0.00, repayment false, transactions 2
a supplier's reverse-charge sales go in box 6 only transactions ×2, 2026-04-01, 2026-06-30 → box1 £0.00, box2 £0.00, box3 £0.00, box4 £0.00, box5 £0.00, box6 £500.00, box7 £0.00, box8 £0.00, box9 £0.00, repayment false, transactions 2
exempt and outside-scope purchases: box 7 for the first, nothing for the second transactions ×2, 2026-04-01, 2026-06-30 → box1 £0.00, box2 £0.00, box3 £0.00, box4 £0.00, box5 £0.00, box6 £0.00, box7 £30.00, box8 £0.00, box9 £0.00, repayment false, transactions 2
an empty period , 2026-04-01, 2026-06-30 → box1 £0.00, box2 £0.00, box3 £0.00, box4 £0.00, box5 £0.00, box6 £0.00, box7 £0.00, box8 £0.00, box9 £0.00, repayment false, transactions 0
Show the other 9 tests
CaseArgumentsExpected
a period that ends before it starts is refused , 2026-04-01, 2026-03-31 → error: VAT return period ends 2026-03-31, before it starts 2026-04-01
VAT on an exempt sale is refused transactions ×1, 2026-04-01, 2026-06-30 → error: transaction 1: exempt sales must have zero VAT
VAT on a supplier's reverse-charge sale is refused transactions ×2, 2026-04-01, 2026-06-30 → error: transaction 2: domestic-reverse-charge sales must have zero VAT
postponed import VAT on a sale is refused transactions ×1, 2026-04-01, 2026-06-30 → error: transaction 1: postponed-import-vat applies to purchases only
amounts in another currency are refused transactions ×1, 2026-04-01, 2026-06-30 → error: transaction 1: amounts must be in GBP
an unknown treatment is refused transactions ×1, 2026-04-01, 2026-06-30 → error: transaction 1: unknown VAT treatment "standard"
an unknown direction is refused transactions ×1, 2026-04-01, 2026-06-30 → error: transaction 1: unknown direction "refund"
transactions outside the period are still checked transactions ×1, 2026-04-01, 2026-06-30 → error: transaction 1: exempt sales must have zero VAT
an impossible tax point is refused transactions ×1, 2026-04-01, 2026-06-30 → error: not a real calendar date

More from the author

A transaction is in the period when its tax point (`date`) is on or after `periodStart` and on or before `periodEnd`; the rest are left out, so a whole ledger can be passed. Every transaction is still checked, in or out of the period, and errors name it by its 1-based position.

| treatment | sale | purchase | |---|---|---| | `taxable` (standard, reduced or zero-rated) | box 1 VAT, box 6 net | box 4 VAT, box 7 net | | `exempt` | box 6 net | box 7 net | | `outside-scope` (not a supply: wages, grants, loans, dividends) | nothing | nothing | | `domestic-reverse-charge` (construction, specified goods) | box 6 net | box 1 and box 4 VAT, box 7 net | | `services-reverse-charge` (international services) | box 6 net | box 1 and box 4 VAT, boxes 6 and 7 net | | `postponed-import-vat` | refused | box 1 and box 4 VAT, box 7 net | | `ni-eu-goods` (Northern Ireland and the EU) | boxes 6 and 8 net | box 2 and box 4 VAT, boxes 7 and 9 net |

The mapping follows HMRC VAT Notice 700/12, "How to fill in and submit your VAT Return", sections 3.2 to 3.10 (the boxes) and 4.6 (reverse charge: the supplier fills in box 6; the customer boxes 1, 4 and 7, plus box 6 for gold and international services), https://www.gov.uk/guidance/how-to-fill-in-and-submit-your-vat-return-vat-notice-70012, read on 23 September 2026. Exempt, outside-scope, supplier-side reverse charge and NI-to-EU sales must carry zero VAT, and a VAT figure on them is refused as a miscategorisation.

**Credit notes** are negative transactions and reduce the boxes they would have fed, as 3.2 and 3.5 require ("deduct any VAT on credit notes").

**Box 5** is the difference between boxes 3 and 4 without a sign, as the form requires ("do not enter a minus sign in box 5"); `repayment` says which way it goes.

**Boxes 6 to 9** are whole pounds. The pence are dropped after totalling (not per transaction), truncating towards zero, so 999.99 of sales is 999 in box 6. Notice 700/12 does not state the rounding for these boxes; dropping the pence is the common practice and should be confirmed.

**Not covered.** Input VAT is taken as fully recoverable: partial exemption, blocked input tax (business entertainment, most cars) and non-reclaimable import VAT must be taken out of the purchase's `vat` before it is passed. Special schemes (Flat Rate, see finance.tax.vat-flat-rate; cash accounting; retail and margin schemes) change several boxes and are not applied here, nor are bad debt relief, fuel scale charges or error corrections, other than by the caller entering them as taxable sales or purchases. Amounts must be in GBP.

## Before you rely on this

**Not professional advice.** This capability calculates tax figures from published rules. It is a software component for developers, not tax advice. Rules change and every rate here has an effective date. Check that the dates cover your case. Verify results against the official sources listed above, and have a chartered accountant or VAT specialist review how you use it, before anyone relies on the output. Provided "as is" under its licence, without warranty.

**Unreviewed.** This capability's implementations agree in every language and pass its published test vectors, which were worked out from the official sources cited. But no qualified chartered accountant or VAT specialist has yet checked those vectors, or confirmed that the capability covers the cases it claims. Treat it as a draft. Do not use it for real people, money or decisions without your own expert review. Once a qualified reviewer signs off, this notice is replaced with their name, qualification and the date. Each new version needs fresh sign-off.

1.0.1 marks it unreviewed. The code and the tests are unchanged.

Files

PathBytes
README.md4,150
impl/python.py3,736
impl/rust.rs5,679
impl/typescript.ts3,897
vectors.json12,494