tax

package module
v0.0.0-...-96b0fa3 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 11, 2025 License: Apache-2.0 Imports: 7 Imported by: 0

README

Tax! (UK employment variety)

Are you looking at your payslips wondering how any of those numbers are arrived at? Why they sometimes change from month to month? Why on earth they don't show their working? I was.

There are plenty of online salary take-home pay calculators, but they only show figures for the whole year. They don't allow you to put in your payslips from the start of the tax year and tell you what your next one will be. So I wrote this.

NB: Right now, this will only work if your Tax Code ends with an L, and if you're in National Insurance Contributions (NIC) Letter A. In the fullness of time, I might flesh this out further to cope with more complications. Help is welcome!

The TL;DR is:

  1. Create some Payslips. Payslips must start from the beginning of a tax year (e.g. April 2024) and have no gaps. The zero value is fine for months where you had no income.
  2. Call Complete() on them
  3. Print out the result

For payslips from the past, you can provide the numbers from your actual payslips. But for all payslips, any missing values will be calculated.

I could find no good resources on how PAYE actually works. There seems to be nothing I could find on gov.uk or HMRC's websites. It's all very frustrating. The following is my understanding of how it all works. It creates numbers that match my own payslips fairly closely.

What do you pay income tax on?

The short version is salary - employee pension contributions - personal allowance. What ever is left over is what you pay income tax on. But, income tax and personal allowance, via PAYE (which stands for Pay As You Earn) is all calculated on Year-to-date (YTD) figures (PAYE - the clue's in the name).

So what you actually do is this:

  1. Add up your total income from the start of this tax year, i.e. year-to-date (YTD).
  2. Subtract from that all your pension contributions, YTD.
  3. Also subtract your personal allowance, YTD.
  4. Figure out how much tax you should have paid on this remaining figure, YTD (more on this later).
  5. Subtract from this the amount of tax you've already paid this year. This final number is the amount of tax to pay this month.

What's a personal allowance?

It's an amount of money you're allowed to earn on which you pay no income tax at all. If your Tax Code ends with an L (which I think is by far the most common), remove the L, and add a 0. The default Tax Code is 1257L. This means you're allowed to earn £12570 per year and pay no income tax at all on that.

But! Personal allowance is accrued 1/12th per month. So if your tax code is the default 1257L, then you get personal allowance of £1047.50 per month. It also rolls over: if you earn less than your monthly personal allowance in a month (e.g. less than £1047.50) then any amount left over rolls into the next month, where it can be used.

This means that if you have a period of time in a year when you're unemployed, then your personal allowance will build up. This is why when you start earning again, it can take a couple of months for your income tax to stabilise - it'll start off low because you're using up that personal allowance that's built up.

This also means that if you are unemployed (or not using up your personal allowance) as you move from one tax year to another (e.g. let's say you're unemployed in February and March) then you will have over-paid tax and you'll be able to get a refund: essentially in those months you accrued personal allowance but were unable to use it.

Income Tax YTD

PAYE does personal allowance and income tax all based on YTD figures. But it also scales all the tax bands YTD too.

Here are the tax bands for tax year 2024/25. "Taxable income" is salary - pension - personal allowance, and these figures are for the whole year.

Taxable income Tax rate Band name
£0 - £37,700 20% Basic rate
£37,701 - £125,140 40% Higher rate
over £125,140 45% Additional rate

Let's say your net taxable income for the whole year is £50k. If PAYE didn't scale the tax bands, then it would mean that around Christmas time each year, your income YTD, would pass over the £37,700 threshold, and your income tax would suddenly jump up. No one wants this. It's Christmas!

Therefore, all the tax band thresholds are scaled down YTD too. This means that even in April, right at the start of the tax year, your notional £50k taxable income for this year ahead of you, you're already paying a bit of higher-rate tax, right from month 1 (April). It means that the amount of tax you pay remains the same throughout the year - there are no surprises if your salary remains constant.

But, imagine instead that in April on its own, you earn £70k and then you're unemployed for the rest of the year. Well essentially the tax calculations will assume your salary is going to be £70k*12 = £840k, and so in April you're going to be paying a lot of additional-rate tax. 11 months later or so, you'll probably be able to claim back a lot of tax as it turns out you should have never paid any tax in the additional-rate band at all (plus, you only got to use up 1 month's-worth of your personal allowance, so you've actually over-paid tax for two reasons).

From what I can tell, PAYE is quite cleverly designed so that:

  1. If you remain employed then once your income tax stabilises, it'll remain steady throughout the year.
  2. It's far more likely you over-pay tax (either by not using up your personal allowance, or by the income tax calculations making the assumption that your earnings YTD are a proportional scaling of your total eventual annual income), than under-pay tax. From HMRC's point-of-view, this is a good thing.

National Insurance Contributions

This is totally different. There is no YTD scaling stuff going on here. If your National Insurance Contribution letter is A, then for 2024:

Monthly income NIC tax rate
£1048 - £4189 8%
over £4189 2%

So you just take your monthly income (gross! - don't do any of the subtraction of pensions or personal allowance etc), and plug it in to the above table and that's that.

Net income

Finally then, your net income, or take-home pay, should be more or less:

gross monthly salary - employee pension contribution - income tax - NIC tax

Note that personal allowance doesn't appear here at all - that's only used for calculating your income tax.

Documentation

Index

Constants

This section is empty.

Variables

View Source
var TaxBandsByYear = map[int]*Bands{
	2024: {
		IncomeTax: &TaxBands{
			Duration: Year,
			Bands: []*TaxBand{
				{Minimum: 0, Rate: 0.2},
				{Minimum: 37700, Rate: 0.4},
				{Minimum: 125140, Rate: 0.45},
			},
		},
		NicEmployeeTax: &TaxBands{
			Duration: Month,
			Bands: []*TaxBand{
				{Minimum: 1048, Rate: 0.08},
				{Minimum: 4189, Rate: 0.02},
			},
		},
		NicEmployerTax: &TaxBands{
			Duration: Month,
			Bands: []*TaxBand{
				{Minimum: 758, Rate: 0.138},
			},
		},
		PensionQualifiedEarnings: &TaxBand{
			Minimum: 6240,
			Maximum: 50270,
		},
	},

	2025: {
		IncomeTax: &TaxBands{
			Duration: Year,
			Bands: []*TaxBand{
				{Minimum: 0, Rate: 0.2},
				{Minimum: 37700, Rate: 0.4},
				{Minimum: 125140, Rate: 0.45},
			},
		},
		NicEmployeeTax: &TaxBands{
			Duration: Month,
			Bands: []*TaxBand{
				{Minimum: 1048, Rate: 0.08},
				{Minimum: 4189, Rate: 0.02},
			},
		},
		NicEmployerTax: &TaxBands{
			Duration: Month,
			Bands: []*TaxBand{
				{Minimum: 417, Rate: 0.15},
			},
		},
		PensionQualifiedEarnings: &TaxBand{
			Minimum: 6240,
			Maximum: 50270,
		},
	},
}

Note that for now, the 2025 figures are just copies of 2024.

Functions

This section is empty.

Types

type Bands

type Bands struct {
	IncomeTax                *TaxBands
	NicEmployeeTax           *TaxBands
	NicEmployerTax           *TaxBands
	PensionQualifiedEarnings *TaxBand
}

type Duration

type Duration uint8
const (
	Day Duration = iota
	Week
	Month
	Year
)

func (Duration) String

func (self Duration) String() string

type NumberRate

type NumberRate struct {
	Number   float64
	Duration Duration
}

Number rate models a number per duration. E.g £10,000 /year.

func Monthly

func Monthly(num float64) NumberRate

Short-hand for creating a number per month.

func Yearly

func Yearly(num float64) NumberRate

Short-hand for creating a number per year.

func (NumberRate) Per

func (self NumberRate) Per(rate Duration) NumberRate

Convert this NumberRate to a given duration.

func (NumberRate) String

func (self NumberRate) String() string

type Payslip

type Payslip struct {
	// The tax year. So if the tax year is 2024/25, set this to be
	// 2024. If you leave this blank then the tax year will be
	// calcualed correctly from the Previous payslip. Year is 0 and
	// Previous is nil, then Year will be set to the current year.
	Year int

	// If this is month 1 (i.e. April), then Previous may be nil. In
	// all other cases, Previous must point to the payslip for the
	// Previous month.
	Previous *Payslip

	// The month of the tax year; 1-based. If 0 and Previous is nil,
	// then will be set to 1. If 0 and Previous is not nil, then will
	// be set to (Previous.Month % 12) + 1.
	Month int

	// Input; if zero, will copy from Previous
	TaxCode TaxCode
	// Input only. The zero value is fine to use as an income of 0.
	Salary NumberRate
	// Input only. Expenses are not included in any tax calculations.
	Expenses float64

	// Output only
	GrossIncome float64
	// Output only
	GrossIncomeYTD float64

	// Input only
	PensionType PensionType
	// Input or output - exactly one of EmployeePensionContributionRate
	// and EmployeePensionContribution must be provided.
	EmployeePensionContributionRate float64
	// Input or output - exactly one of EmployeePensionContributionRate
	// and EmployeePensionContribution must be provided.
	EmployeePensionContribution float64
	// Output only
	EmployeePensionContributionYTD float64

	// The employer side of pension contributions. Follows the same
	// rules as for the corresponding employee pension fields.
	EmployerPensionContributionRate float64
	EmployerPensionContribution     float64
	EmployerPensionContributionYTD  float64
	// Output only
	PensionContributionYTD float64

	// Output only
	TaxableIncome float64
	// Output only
	TaxableIncomeYTD float64

	// Output only
	PersonalAllowanceYTD float64
	// Output only
	NetTaxableIncomeYTD float64
	// Output only
	IncomeTaxYTD float64
	// Input or output
	IncomeTax float64

	// Output only
	EmployeeNicTax float64
	// Output only
	EmployeeNicTaxYTD float64

	// Output only
	EmployerNicTax float64
	// Output only
	EmployerNicTaxYTD float64

	// Output only
	NetIncome float64
	// Output only
	NetIncomeYTD float64
}

Payslip models a monthly payslip. Some fields must be provided prior to calling *Payslip.Complete, some fields can be provided (which is helpful if you wish to provide numbers from actual payslips you've received), and some fields are output only.

func (*Payslip) CloseYear

func (self *Payslip) CloseYear() *YearSummary

CloseYear produces a YearSummary based on the year-to-date figures in the Payslip. Normally you'd only call this on a payslip with Month 12 (i.e. March).

func (*Payslip) Complete

func (self *Payslip) Complete()

Complete attempts to fill in all missing fields in this Payslip. If this Payslip is month 1 (i.e. April) then Previous can be nil. In all other cases Previous should point to the Payslip for the previous month.

func (*Payslip) FormattedDate

func (self *Payslip) FormattedDate() string

FormattedDate returns a string such as "January 2006" for this Payslip's year and month.

func (*Payslip) String

func (self *Payslip) String() string

String produces a really nice formatting of the Payslip.

type Payslips

type Payslips []*Payslip

Payslips is the easy way to build a sequence of [*Payslip]s. The oldest payslip must be month 1 and must be at index 0. The payslips should form a contiguous sequence with no months missing.

func (Payslips) Complete

func (self Payslips) Complete()

Complete completes all the payslips, in the right order. It also sets up the Previous field correctly, so you don't need to do that explicitly.

func (Payslips) String

func (self Payslips) String() string

String produces a really nice string of the details of all the payslips, and *YearSummary for every month 12 payslip.

type PensionType

type PensionType uint8
const (
	// Pension contribution rates are interpreted as being a fraction
	// of the month's salary.
	Salary PensionType = iota
	// Pension contributino rates are interpreted as being a fraction
	// of the month's qualifying earnings. This seems to be
	// increasingly common for auto-enrolment pensions in the UK.
	QualifyingEarnings
)

func (PensionType) String

func (self PensionType) String() string

type TaxBand

type TaxBand struct {
	Minimum float64
	Maximum float64
	Rate    float64
}

TaxBand represents a tax band that applies Rate between the Minimum and Maximum bounds.

func (*TaxBand) String

func (self *TaxBand) String() string

type TaxBands

type TaxBands struct {
	Duration Duration
	Bands    []*TaxBand
}

TaxBands model a set of TaxBands. The individual bands must be non-overlapping. It is best to define individual bands with only their Minimums set; the call to *TaxBands.Init will sort the bands, and populate the Maximum field.

func (*TaxBands) Init

func (self *TaxBands) Init()

Init sets up TaxBands for use by sorting them (by the Minimum field, ascending), and by setting the Maximum of any band to be the Minimum of its successor. The Maximum of the last band is set to +Inf.

func (TaxBands) InvertedTax

func (self TaxBands) InvertedTax(tax NumberRate) NumberRate

From an amount of tax paid, figure out what the amount must have been.

func (TaxBands) Tax

func (self TaxBands) Tax(amount NumberRate) NumberRate

Calculate the tax due (to be paid) on an amount.

type TaxCode

type TaxCode string

func (TaxCode) PersonalAllowance

func (self TaxCode) PersonalAllowance() NumberRate

Calculate the Personal Allowance of a Tax Code. Note that this currently only copes with Tax Codes that end with L.

type YearSummary

type YearSummary struct {
	Year        int
	LastPayslip *Payslip

	TaxableIncome     float64
	PersonalAllowance float64
	NetTaxableIncome  float64
	IncomeTax         float64
	IncomeTaxOwing    float64

	NetIncome float64
}

YearSummary can be thought as as similar to a P60. It identifies over- or under-payment of tax (over-payment is far more likely with PAYE).

func (*YearSummary) String

func (summary *YearSummary) String() string

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL