Skip to content

Ordinary loss-cap adjusted total income gives the basic RAS uplift to pure net pay #2486

Open

Description

@MaxGhenis

The ordinary adjusted_total_income formula deducts pure net-pay pension
contributions above relevant earnings when earnings are below £3,600. That
reduces the ITA 2007 s.24A loss-relief cap and can increase ordinary income
tax. This is separate from the annual-allowance reconstruction corrected in
PR #2441 round 6. The ordinary formula is deliberately unchanged in that PR.

Affected location:
policyengine_uk/variables/gov/hmrc/income_tax/reliefs/adjusted_total_income.py,
the relief_limit / relieved_contributions calculation at the end of
formula. It uses min(pension_contributions, max(basic_amount, relevant_earnings)) without checking whether the excess-over-earnings
contribution is relief at source.

Minimal MOCK reproducer

This witness needs no salary sacrifice, employer contribution or annual
allowance charge. All monetary inputs are gross. Raising pure net pay from
£2,000 to £3,600 cannot give additional pension relief because relevant UK
earnings remain £2,000.

POLICYENGINE_UK_DEFAULT_DATASET=MOCK \
OMP_NUM_THREADS=1 OPENBLAS_NUM_THREADS=1 MKL_NUM_THREADS=1 \
VECLIB_MAXIMUM_THREADS=1 NUMEXPR_NUM_THREADS=1 \
.venv/bin/python - <<'PY'
from policyengine_uk import Simulation

for net_pay in (2000, 3600):
    year = 2026
    values = {
        "age": 45,
        "employment_income_before_lsr": 2000,
        "employee_pension_contributions_reported": net_pay,
        "dividend_income": 250000,
        "trading_loss": 200000,
    }
    sim = Simulation(situation={
        "people": {"p": {k: {year: v} for k, v in values.items()}},
        "benunits": {"b": {"members": ["p"]}},
        "households": {"h": {
            "members": ["p"], "region": {year: "LONDON"}
        }},
    })
    names = (
        "pension_contributions_relief", "adjusted_total_income",
        "income_tax_relief_cap", "trade_loss_relief_against_general_income",
        "adjusted_net_income", "income_tax",
    )
    print(net_pay, {v: float(sim.calculate(v, year)[0]) for v in names})
PY

An extended reproducer (held by the UK hub with #2441's round 6 evidence) also runs 2026, 2029 and 2030 at salary
sacrifice NICs caps of £2,000 and infinity, the £2,000-payment control,
and a diagnostic simulation with supplied adjusted_total_income=250000.
The latter demonstrates the downstream effect of the correct loss cap;
it does not alter production formulas.

Expected result and legal basis

Only £2,000 of either net-pay payment is relievable. Total income is
£252,000 before that pension deduction, giving s.24A adjusted total income
of £250,000 and a loss-relief cap of £62,500. The £200,000 current trading
loss is capped at £62,500. Net income after net pay and current-year loss
relief is £187,500.

FA 2004 s.190(3)
subjects the basic-amount increase to
s.191(7), which
restricts that above-earnings relief to relief at source. HMRC
PTM044100
confirms the restriction, as does
PTM044220.
ITA 2007 s.24A(8)
deducts pension contributions receiving the relevant statutory relief;
unrelievable pure net-pay excess cannot reduce its adjusted-total-income
base. The model's RAS-aware pension_contributions_relief_amount already
implements the distinction.

Observed result

The ordinary ATI formula's SHA256 at reviewed
head b6e81966284d4f4d9ff4236dbb3460427e86024a is
ba628b6f36abee376e6c6e732c231dfff300e3d668c4ece309c1ef4ed33de597.
The executable confirmed that same formula hash in the round-6 workspace.
All six year/cap worlds returned the values below. This directly exercises
the ordinary formula that round 6 leaves byte-identical to the reviewed
head; the separate AA helper change cannot resolve this defect.

Net-pay payment Pension relief Ordinary ATI Loss cap / relieved loss ANI after pension relief
£2,000 control £2,000 £250,000 £62,500 £187,500
£3,600 current formula £2,000 £248,400 £62,100 £187,900
£3,600 with correct ATI diagnostic input £2,000 £250,000 £62,500 £187,500

The unrelieved £1,600 net-pay increase therefore reduces allowable loss
relief by £400 and raises ordinary taxable income by £400. In every world,
ordinary income tax is £59,954.859375, versus £59,797.4609375 for the
£2,000-payment control and the supplied-correct-ATI diagnostic. The
overstatement is £157.3984375 (about £157.40); the small fractional-penny
difference from £400 × 39.35% comes from the model's float32 results.
The annual allowance charge is zero in this isolated case. There is no
salary sacrifice in this isolated case, so changing the salary sacrifice
NICs cap has no effect. This issue does not ask to remove legitimate
ordinary-tax changes caused by returned salary in other capped worlds.

Suggested acceptance coverage: the two payments above must produce the
same ordinary ATI, loss cap, loss relief and ordinary income tax; retain a
RAS case that permits the £3,600 basic amount and an age-75-or-older case
that gives no member pension relief. Check mixed net-pay/RAS allocation
against the existing shared helper's convention.

Filed by the UK hub from #2441 round 6's build write-up. The figures come from that job's foreground MOCK run.

Activity

  1. self-assigned this
    on Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions