Functional Weave
Code in TypeScript

payroll.gross-to-net Unreviewed

A UK payslip for one pay period: gross pay, pension, PAYE tax, National Insurance, student loans and net pay.

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

Pinned by 8 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 payroll 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 payroll figures from published rules. It is a software component for developers, not tax or legal 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 payroll 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 payroll professional before it is published.

One employee's payslip for one pay period. This capability contains no rates, no thresholds and no rounding of its own: each line comes from the capability that owns it, and what is left here is the order of operations, which is where payslips usually go wrong.

For example

  • grossToNet(gross £3,000.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans , pensio…) → gross £3,000.00, pension employee £0.00, pension employer £0.00, taxable pay £3,000.00, tax £390.20, employee ni £156.16, employer ni £387.45, student loan £0.00, postgraduate loa… 3,000 pounds a month on 1257L, category A, nothing else
  • grossToNet(gross £3,000.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans plan-2, …) → gross £3,000.00, pension employee £99.20, pension employer £74.40, taxable pay £3,000.00, tax £390.20, employee ni £156.16, employer ni £387.45, student loan £49.00, postgraduate … relief at source pension at the minimums, plan 2 and a postgraduate loan
  • grossToNet(gross £3,000.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans , pensio…) → gross £3,000.00, pension employee £150.00, pension employer £90.00, taxable pay £2,850.00, tax £360.20, employee ni £156.16, employer ni £387.45, student loan £0.00, postgraduate … a net pay arrangement comes off before tax but not before NI

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 grossToNet(input: PayslipInput): Payslip
inputPayslipInput
returnsPayslip

The types it declares, generated into your project

/** One employee's pay for one period, and what the payroll knows about them. */
export interface PayslipInput {
  /** gross pay for the period, all of it taxable and NI-able */
  readonly gross: Money;
  readonly frequency: PayFrequency;
  /** decides the tax year, tax period and every rate */
  readonly payDate: string;
  /** as issued by HMRC, e.g. 1257L, S1257L, BR, 1257L M1 */
  readonly taxCode: string;
  /** taxable pay already paid this tax year (including a P45's figure) */
  readonly previousTaxablePayToDate: Money;
  /** tax already deducted this tax year (including a P45's figure) */
  readonly previousTaxToDate: Money;
  /** National Insurance category letter, usually A */
  readonly niCategory: string;
  /** every plan the employee repays; empty for none */
  readonly studentLoanPlans: readonly StudentLoanPlan[];
  /** null when the employee is not in a workplace pension */
  readonly pension: PensionScheme | null;
}

/** Every line of the payslip, and the year-to-date figures the next period needs. */
export interface Payslip {
  readonly gross: Money;
  /** deducted from pay (net of basic-rate relief under relief at source) */
  readonly pensionEmployee: Money;
  /** paid by the employer on top; not deducted */
  readonly pensionEmployer: Money;
  /** gross less a net pay arrangement pension deduction */
  readonly taxablePay: Money;
  /** PAYE income tax; negative is a refund */
  readonly tax: Money;
  readonly employeeNi: Money;
  /** paid by the employer on top; not deducted */
  readonly employerNi: Money;
  /** undergraduate plans */
  readonly studentLoan: Money;
  readonly postgraduateLoan: Money;
  /** what the employee is paid */
  readonly net: Money;
  /** previousTaxablePayToDate plus taxablePay */
  readonly taxablePayToDate: Money;
  /** previousTaxToDate plus tax */
  readonly taxToDate: Money;
  /** tax week or month the payment falls in */
  readonly period: number;
  /** e.g. 2026/27 */
  readonly taxYear: string;
}

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

import { grossToNet } from "#fune/payroll.gross-to-net@^1";
impl/typescript.ts · 69 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 { money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { type Payslip, type PayslipInput } from "./payroll_gross_to_net_types.ts";
import { incomeTax } from "./payroll_income_tax.ts";  ← from payroll.income-tax ^1.0.0 · built alongside by fune
import { nationalInsurance } from "./payroll_national_insurance.ts";  ← from payroll.national-insurance ^1.0.0 · built alongside by fune
import { pensionContributions } from "./payroll_pension_auto_enrolment.ts";  ← from payroll.pension-auto-enrolment ^1.0.0 · built alongside by fune
import { studentLoan } from "./payroll_student_loan.ts";  ← from payroll.student-loan ^1.0.0 · built alongside by fune
import { taxPeriod } from "./payroll_tax_period.ts";  ← from payroll.tax-period ^1.0.0 · built alongside by fune

/**
 * One period's payslip, in the order a UK payroll works it out.
 *
 * The order matters because each deduction is taken from a different figure:
 * a net pay arrangement pension comes off pay before tax but not before
 * National Insurance; tax is on taxable pay to date; NI and student loans are
 * on this period's gross pay alone; relief at source pension comes off net pay.
 */
export function grossToNet(input: PayslipInput): Payslip {
  const gross = input.gross;
  if (gross.currency !== "GBP") {
    throw new RangeError(`payroll amounts must be in GBP, received ${gross.currency} for gross`);
  }
  const when = taxPeriod(input.payDate, input.frequency);

  let pensionEmployee = 0;
  let pensionEmployer = 0;
  let beforeTax = 0;
  if (input.pension !== null) {
    const pension = pensionContributions(gross, input.frequency, input.pension, input.payDate);
    pensionEmployee = pension.employeeDeduction.minor;
    pensionEmployer = pension.employer.minor;
    if (pension.deductBeforeTax) beforeTax = pension.employeeDeduction.minor;
  }
  const taxablePay = money(gross.minor - beforeTax, "GBP");

  const tax = incomeTax(input.taxCode, input.frequency, when.period, taxablePay, input.previousTaxablePayToDate, input.previousTaxToDate, input.payDate);
  const ni = nationalInsurance(gross, input.niCategory, input.frequency, input.payDate, null);

  // An employee with more than one undergraduate plan repays once, against the
  // lowest threshold, which is the largest of the per-plan deductions; a
  // postgraduate loan is repaid alongside it.
  let undergraduate = 0;
  let postgraduate = 0;
  const seen: string[] = [];
  for (const plan of input.studentLoanPlans) {
    if (seen.includes(plan)) throw new RangeError(`student loan plan ${plan} is listed twice`);
    seen.push(plan);
    const deduction = studentLoan(gross, plan, input.frequency, input.payDate).minor;
    if (plan === "postgraduate") postgraduate = deduction;
    else if (deduction > undergraduate) undergraduate = deduction;
  }

  const net = gross.minor - pensionEmployee - tax.tax.minor - ni.employee.minor - undergraduate - postgraduate;
  return {
    gross,
    pensionEmployee: money(pensionEmployee, "GBP"),
    pensionEmployer: money(pensionEmployer, "GBP"),
    taxablePay,
    tax: tax.tax,
    employeeNi: ni.employee,
    employerNi: ni.employer,
    studentLoan: money(undergraduate, "GBP"),
    postgraduateLoan: money(postgraduate, "GBP"),
    net: money(net, "GBP"),
    taxablePayToDate: tax.payToDate,
    taxToDate: tax.taxToDate,
    period: when.period,
    taxYear: when.taxYear,
  };
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 6 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 payroll.gross-to-net
Download for TypeScript payroll.gross-to-net-1.0.1-typescript.fune · 22,446 bytes sha256 4a56b7fa104422075f61fc796896e46b0d4ab599e51ef6707da1be6675813bd5

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./payroll.gross-to-net-1.0.1-typescript.fune, or fetch it from a terminal with fune pull payroll.gross-to-net@1.0.1:typescript.

The whole function, every language, is one file too: payroll.gross-to-net-1.0.1.fune, 31,769 bytes, sha256 c37221261111539c198a5a8f58c94c4736fb0e9603bb5fe8e1d34290fb6a1a56. 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 payroll.gross-to-net

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

// fune: after payroll.gross-to-net

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 money.amount in payroll.gross-to-net
// fune: replace payroll.income-tax in payroll.gross-to-net
// fune: replace payroll.national-insurance in payroll.gross-to-net
// fune: replace payroll.pension-auto-enrolment in payroll.gross-to-net
// fune: replace payroll.student-loan in payroll.gross-to-net
// fune: replace payroll.tax-period in payroll.gross-to-net

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 payroll.gross-to-net --steps.

// fune: step payroll.gross-to-net 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
3,000 pounds a month on 1257L, category A, nothing else gross £3,000.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans , pensio… → gross £3,000.00, pension employee £0.00, pension employer £0.00, taxable pay £3,000.00, tax £390.20, employee ni £156.16, employer ni £387.45, student loan £0.00, postgraduate loa…
relief at source pension at the minimums, plan 2 and a postgraduate loan gross £3,000.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans plan-2, … → gross £3,000.00, pension employee £99.20, pension employer £74.40, taxable pay £3,000.00, tax £390.20, employee ni £156.16, employer ni £387.45, student loan £49.00, postgraduate …
a net pay arrangement comes off before tax but not before NI gross £3,000.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans , pensio… → gross £3,000.00, pension employee £150.00, pension employer £90.00, taxable pay £2,850.00, tax £360.20, employee ni £156.16, employer ni £387.45, student loan £0.00, postgraduate …
a Scottish taxpayer with plans 1 and 2 repays once, at plan 1's lower threshold gross £3,000.00, frequency monthly, pay date 2026-04-28, tax code S1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans plan-2,… → gross £3,000.00, pension employee £0.00, pension employer £0.00, taxable pay £3,000.00, tax £392.27, employee ni £156.16, employer ni £387.45, student loan £68.00, postgraduate lo…
weekly pay under the allowance and the primary threshold: only employer NI gross £200.00, frequency weekly, pay date 2025-04-11, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans , pension — → gross £200.00, pension employee £0.00, pension employer £0.00, taxable pay £200.00, tax £0.00, employee ni £0.00, employer ni £15.60, student loan £0.00, postgraduate loan £0.00, …
a student loan plan listed twice is an error gross £3,000.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans plan-2, … → error: listed twice
gross pay in euros is an error gross €3,000.00, frequency monthly, pay date 2026-04-28, tax code 1257L, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans , pensio… → error: must be in GBP
a code HMRC does not issue is an error gross £3,000.00, frequency monthly, pay date 2026-04-28, tax code 1257Q, previous taxable pay to date £0.00, previous tax to date £0.00, ni category A, student loan plans , pensio… → error: unrecognised tax code

More from the author

1. **Tax period** from the pay date and frequency (`payroll.tax-period`). 2. **Pension** (`payroll.pension-auto-enrolment`), if the employee is in a scheme. Under a *net pay arrangement* the employee's contribution comes off pay before tax; under *relief at source* it is taken from net pay, already reduced by basic-rate relief, and taxable pay is untouched. Neither reduces National Insurance or student loan pay: only salary sacrifice does that, and salary sacrifice is a change to gross pay, so give the reduced gross. 3. **Income tax** on taxable pay (`payroll.income-tax`), cumulative or week 1 / month 1 as the code says. 4. **National Insurance** on this period's gross (`payroll.national-insurance`), by the ordinary per-period method. Directors on the annual earnings period method should call `payroll.national-insurance` directly. 5. **Student loans** on this period's gross (`payroll.student-loan`). An employee with more than one undergraduate plan repays once, at the plan with the lowest threshold, which is the largest single-plan deduction; a postgraduate loan is repaid on top. 6. **Net pay** = gross - pension deduction - tax - employee NI - student loans.

A tax refund makes `tax` negative and increases net pay. Employer NI and the employer pension contribution are reported but not deducted.

Out of scope: statutory payments (give them as part of gross), benefits in kind, attachment of earnings orders, salary sacrifice arithmetic, and deductions after net pay.

Sources: the rules are those of the capabilities listed above, each of which cites its own. The order of deductions follows HMRC's "Employer further guide to PAYE and NICs" (CWG2), https://www.gov.uk/government/publications/cwg2-further-guide-to-paye-and-national-insurance-contributions, and for student loans ("when the employee is on more than one plan, start deductions for the plan with the lowest recovery threshold"; loans are worked out on the same pay as secondary Class 1 NICs), HMRC's "Student loan and postgraduate loan repayment guidance for employers", https://www.gov.uk/guidance/special-rules-for-student-loans.

## Before you rely on this

**Not professional advice.** This capability calculates payroll figures from published rules. It is a software component for developers, not tax or legal 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 payroll professional 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 payroll professional 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.

## Notices

Contains public sector information licensed under the Open Government Licence v3.0 (https://www.nationalarchives.gov.uk/doc/open-government-licence/version/3/).

1.0.1 marks it unreviewed and adds its attribution notices (NOTICE). The code and the tests are unchanged.

Files

PathBytes
NOTICE183
README.md3,834
impl/python.py3,098
impl/rust.rs5,882
impl/typescript.ts3,042
vectors.json8,606