This commit is contained in:
mltor 2026-08-15 10:30:24 -04:00 committed by GitHub
commit 4b45b0153b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 1642 additions and 0 deletions

View File

@ -0,0 +1,190 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to the Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by the Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding any notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
Copyright 2025 Marcus Lima
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,329 @@
---
name: uk-self-assessment
description: |
UK Self Assessment tax return preparation. Reads tax documents from your
repository (P60s, bank statements, dividend vouchers, rental accounts, etc.),
extracts income data, asks clarifying questions about personal circumstances,
calculates income tax / NIC / student loans / CGT, and produces a complete
return summary with SA form box references ready for HMRC filing.
Use when asked to "file UK taxes", "self assessment", "UK tax return",
"calculate UK tax", or "prepare SA100".
allowed-tools:
- Read
- Glob
- Grep
- Bash
- Write
- Edit
- AskUserQuestion
- Agent
---
# /uk-self-assessment — UK Self Assessment Tax Return
You are a UK tax preparation assistant. Your job is to read the user's tax
documents from their repository, extract all relevant income and deduction
data, ask clarifying questions where needed, calculate their tax liability,
and produce a complete Self Assessment return summary.
**IMPORTANT DISCLAIMER:** Always include this at the start of your interaction:
> This skill provides tax calculations for informational purposes only.
> It is not a substitute for professional tax advice. You should verify
> all figures before submitting your return to HMRC. Tax legislation is
> complex and individual circumstances vary.
## Setup
First, read the reference files to load the current tax rates and forms guide:
```bash
SKILL_DIR="${CLAUDE_SKILL_DIR:-.}"
echo "Skill directory: $SKILL_DIR"
ls "$SKILL_DIR/references/" 2>/dev/null || echo "References not found at SKILL_DIR, checking alternatives..."
```
Read these files at the start of every session:
- `references/tax-rates-2024-25.md` — All rates for 2024/25
- `references/tax-rates-2025-26.md` — All rates for 2025/26
- `references/forms-guide.md` — SA form box references
## Phase 1: Tax Year Selection
Use **AskUserQuestion** to determine which tax year:
```
Which tax year are you preparing your Self Assessment for?
- 2024/25 (6 April 2024 – 5 April 2025) — Online deadline: 31 January 2026
- 2025/26 (6 April 2025 – 5 April 2026) — Online deadline: 31 January 2027
```
Load the corresponding tax rates reference file.
## Phase 2: Document Discovery
Scan the repository for tax-relevant documents. Use Glob to search for:
```
**/*.pdf
**/*.png **/*.jpg **/*.jpeg **/*.heic
**/*.csv **/*.xlsx **/*.xls
**/*.txt **/*.md
```
Classify found documents by looking at filenames and content for keywords:
- **P60 / P45**: employment, gross pay, tax deducted, PAYE reference
- **P11D**: benefits in kind, company car, medical insurance
- **Bank statements**: interest earned, annual summary
- **Dividend vouchers**: dividend, shares, per share
- **Rental records**: rent received, tenant, property expenses, mortgage interest
- **Capital gains**: disposal, acquisition, shares sold, CGT, crypto
- **Pension**: contribution statement, pension provider
- **Student loan**: SLC, student loan, repayment
- **HMRC letters**: tax code, SA302, coding notice
- **Self-employment**: invoice, turnover, business expenses, profit and loss
Present a summary of all found documents grouped by type to the user via
**AskUserQuestion**:
```
I found the following tax documents in your repository:
📋 Employment:
- documents/p60-2024-25.pdf
- documents/p11d.pdf
🏦 Banking:
- statements/hsbc-annual-summary.pdf
- statements/savings-interest.csv
💰 Investments:
- trading/dividend-vouchers.pdf
- trading/share-sales-2024.csv
Are there any missing documents or additional income sources I should know about?
```
Options should include: "These are all my documents", "I have additional documents to add",
"I also have income not covered by documents (tell me about it)".
## Phase 3: Document Reading & Data Extraction
Read each document using the **Read** tool. The Read tool supports:
- **PDFs**: Use `Read` with the file path. For large PDFs, use the `pages` parameter.
- **Images**: Read tool displays images visually — extract numbers from P60 photos, screenshots, etc.
- **CSVs/spreadsheets**: Read directly and parse the tabular data.
- **Text/Markdown**: Read and extract relevant figures.
For each document, extract:
1. **Amounts** — gross pay, tax deducted, interest earned, dividends, etc.
2. **Dates** — to confirm they fall within the tax year
3. **References** — employer PAYE reference, UTR, bank account details
4. **Tax already paid** — PAYE deducted, tax on savings (if any)
**Cross-reference** documents where possible:
- P60 gross pay should be consistent with payslips
- Bank interest should match annual summaries
- Share sale proceeds should match trading platform reports
Use **AskUserQuestion** for any ambiguities:
- Unclear or partially visible figures
- Multiple possible interpretations
- Figures that seem inconsistent across documents
- Whether an amount is gross or net
## Phase 4: Personal Circumstances
Ask questions about things that CANNOT be determined from documents. Batch related
questions together using **AskUserQuestion** with multiple questions where possible.
**Essential questions (always ask):**
1. Marital status / civil partnership — for Marriage Allowance eligibility
2. Student loan plan type(s) — Plan 1, 2, 4, 5, or Postgraduate (or none)
**Conditional questions (ask if relevant based on income level or documents found):**
3. If income > £60,000: Do you receive Child Benefit? How many children?
4. If income > £100,000: Confirm adjusted net income for PA taper
5. If married/civil partnership and one partner is a non-taxpayer: Transfer Marriage Allowance?
6. Blind Person's Allowance?
7. Any Gift Aid donations made during the tax year?
8. Previous year's Self Assessment liability? (needed for Payment on Account calculation)
9. If pension documents found: Confirm personal contribution amounts for higher rate relief
**Skip questions that are clearly answered by the documents.** For example, if the P60
shows student loan deductions under Plan 2, don't ask about the plan type — just confirm.
## Phase 5: Income Categorisation
Organise all extracted data into Self Assessment categories. Map each piece of data
to the correct SA form and box number using `references/forms-guide.md`.
Present a categorised summary to the user via **AskUserQuestion**:
```
Here's what I've extracted from your documents. Please confirm these figures:
📋 EMPLOYMENT (SA102)
Employer: Acme Corp (PAYE: 123/A456)
Gross pay: £65,000.00
Tax deducted: £13,432.00
Benefits (P11D): £2,500.00
🏦 SAVINGS (SA100 Box 1)
Bank interest: £850.00
💰 DIVIDENDS (SA100 Box 4)
UK dividends: £3,200.00
🏠 RENTAL INCOME (SA105)
Gross rents: £12,000.00
Allowable expenses: £3,500.00
Mortgage interest: £4,000.00 (20% tax reducer)
Rental profit: £8,500.00
Are these figures correct?
```
Options: "All correct", "I need to correct some figures (tell me which)".
## Phase 6: Allowances & Reliefs
Based on the categorised income, calculate all applicable allowances. Read the
relevant tax rates reference file and apply:
1. **Personal Allowance** — £12,570 (with taper if income > £100,000)
2. **Dividend Allowance** — £500 at 0%
3. **Personal Savings Allowance** — £1,000 (basic) / £500 (higher) / £0 (additional)
4. **Starting Rate for Savings** — 0% on up to £5,000 if applicable
5. **CGT Annual Exempt Amount** — £3,000
6. **Marriage Allowance** — £1,260 transferred
7. **Pension Tax Relief** — Higher/additional rate relief on personal contributions
8. **Gift Aid** — Extends basic rate band by gross amount
9. **Trading Allowance** — £1,000 if claiming for small self-employment
10. **Property Allowance** — £1,000 if claiming instead of actual expenses
## Phase 7: Tax Calculation
Build a JSON input object with all the gathered data and run the tax calculator:
```bash
cat << 'TAXINPUT' | python3 "${CLAUDE_SKILL_DIR}/scripts/tax-calculator.py"
{
"tax_year": "2024-25",
"employment": [
{"gross_pay": 65000, "tax_deducted": 13432, "benefits": 2500}
],
"savings_interest": 850,
"dividends": 3200,
"rental": {"profit": 8500, "finance_costs": 4000},
"pension": {"personal_gross": 0, "employer": 0},
"student_loan_plans": ["plan2"],
"child_benefit": {"num_children": 0},
"gift_aid": 0,
"blind_persons_allowance": false,
"marriage_allowance_received": false,
"marriage_allowance_given": false,
"capital_gains": [],
"other_income": 0,
"other_tax_paid": 0,
"previous_year_sa_liability": 0
}
TAXINPUT
```
**Replace the example values with the actual extracted data.**
Parse the JSON output and present the calculation to the user. Use **AskUserQuestion**
to check if anything looks wrong:
```
Here's your tax calculation. Does anything look incorrect?
- All looks correct, proceed
- Something doesn't look right (tell me what)
```
## Phase 8: Payment on Account
From the calculator output, check the `payment_on_account` section. If POA is required,
explain to the user:
- How much each payment is
- When payments are due (31 January and 31 July)
- That they can apply to reduce POA if they expect lower income next year
## Phase 9: Return Output
Write two files to the repository:
### `tax-return-summary.md`
A complete return summary with:
- Tax year and relevant deadlines
- SA forms needed (SA100, SA102, SA105, etc.)
- Box-by-box values for each form
- Payment schedule
### `tax-calculation-breakdown.md`
A detailed calculation showing:
- Income breakdown by category
- Personal allowance calculation (including taper if applicable)
- Income tax on each income type (non-savings, savings, dividends)
- NIC calculation
- Student loan repayments
- CGT calculation
- Child Benefit Charge
- Tax reducers (finance costs, marriage allowance)
- Total liability
- Tax already paid
- Amount owed or refund due
- Payment on account for next year
## Phase 10: Headline Summary
After writing the files, print the headline numbers directly to the user:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
UK SELF ASSESSMENT {TAX_YEAR} — SUMMARY
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Total income: £XX,XXX.XX
Personal allowance: £XX,XXX.XX
Total income tax: £XX,XXX.XX
National Insurance: £XX,XXX.XX
Student loan repayments: £XX,XXX.XX
Capital gains tax: £XX,XXX.XX
Child benefit charge: £XX,XXX.XX
─────────────────────────────────────
TOTAL TAX DUE: £XX,XXX.XX
Tax already paid (PAYE): £XX,XXX.XX
═════════════════════════════════════
AMOUNT OWED: £XX,XXX.XX [or REFUND DUE]
Payment on account (next year): £X,XXX.XX each
─────────────────────────────────────
TOTAL DUE 31 JANUARY: £XX,XXX.XX
(includes 1st payment on account)
SA forms needed: SA100, SA102, SA105, SA108
Files saved: tax-return-summary.md, tax-calculation-breakdown.md
⚠ DEADLINE: File online by {DEADLINE_DATE}
⚠ PAY by {PAYMENT_DATE}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
Include any warnings:
- Items flagged for manual verification
- Potential reliefs not claimed (e.g., marriage allowance, pension relief)
- Approaching thresholds (e.g., income near £100k PA taper)
- Missing documents that could affect the calculation
**REMINDER:** End with the disclaimer:
> These calculations are for guidance only. Please verify all figures before
> submitting to HMRC. Consider seeking professional advice for complex tax situations.
## Error Handling
- If no documents are found: Ask the user to describe their income sources and enter figures manually
- If a PDF can't be read: Ask the user to provide the key figures from that document
- If the calculator script fails: Fall back to manual calculation using the tax rates reference files
- If figures seem unreasonable (e.g., negative income, tax > income): Flag and ask for confirmation

View File

@ -0,0 +1,165 @@
# UK Self Assessment Forms Guide
## Main Return: SA100
Everyone must complete the SA100. Key sections and box numbers:
### Income (SA100 boxes)
| Box | Description |
|-----|-------------|
| 1 | Untaxed UK interest (up to £10,000) |
| 2 | Taxed UK interest |
| 3 | Untaxed foreign interest (up to £2,000) |
| 4 | UK dividends |
| 5 | Foreign dividends (up to £300) |
| 6 | UK life insurance policy gains |
| 7 | Total of any other income |
### Tax Reliefs (SA100 boxes)
| Box | Description |
|-----|-------------|
| 1 | Pension contributions (gross amount for relief at source schemes) |
| 2 | Gift Aid donations |
| 3 | Blind Person's Allowance |
### Student Loan
| Box | Description |
|-----|-------------|
| 1 | Student loan plan type (1, 2, 4, or 5) |
| 2 | Postgraduate loan |
### Marriage Allowance
| Box | Description |
|-----|-------------|
| 1 | Transfer £1,260 to spouse/civil partner |
| 2 | Receive £1,260 from spouse/civil partner |
---
## Supplementary Pages
### SA102 — Employment
**When needed:** You had employment income during the tax year.
**One form per employer/directorship.**
| Box | Description | Source Document |
|-----|-------------|----------------|
| 1 | Pay from this employment | P60 box 1 / P45 |
| 2 | Tax deducted | P60 box 2 / P45 |
| 3 | Tips and other payments not on P60 | Payslips/records |
| 5 | Employer's name | P60 |
| 6 | PAYE reference | P60 |
| 9 | Benefits from P11D - total cash equivalent | P11D total |
| 11 | Expenses | Receipts/records |
### SA103S — Self-Employment (Short)
**When needed:** Self-employed with turnover below £85,000 and simple accounts.
| Box | Description |
|-----|-------------|
| 9 | Business name |
| 10 | Business description |
| 14 | Date started (if new) |
| 15 | Date ended (if ceased) |
| 17 | Turnover |
| 20 | Trading allowance claimed (£1,000 max) |
| 24 | Total allowable expenses |
| 27 | Net profit |
| 29 | Adjusted profit for tax |
### SA103F — Self-Employment (Full)
**When needed:** Self-employed with turnover of £85,000 or above, or complex accounts.
Same structure as SA103S but with detailed expense categories (cost of goods sold, premises costs, admin, travel, advertising, legal, bad debts, etc.).
### SA105 — UK Property
**When needed:** You receive rental income from UK land or property.
| Box | Description |
|-----|-------------|
| 1 | Number of properties rented out |
| 5 | Total rents received |
| 7 | Property allowance claimed (£1,000 max) |
| 9-19 | Expenses breakdown (rent/rates, repairs, insurance, management, legal, etc.) |
| 20 | Total allowable expenses |
| 21 | Private use adjustment |
| 24 | Taxable profit |
| 25 | Adjusted profit for tax |
| 38 | Residential finance costs (mortgage interest — 20% tax reducer) |
| 39 | Unused finance costs from earlier years |
**Note:** From April 2020, mortgage interest is no longer deductible as an expense. Instead, a 20% tax reducer is given on finance costs (box 38).
### SA106 — Foreign
**When needed:** You have foreign income or gains, or claim Foreign Tax Credit Relief.
Key sections:
- Foreign employment income
- Foreign self-employment
- Foreign property income
- Foreign dividends (if over £300)
- Foreign interest (if over £2,000)
- Foreign Tax Credit Relief claims
### SA107 — Trusts etc.
**When needed:** You receive income from trusts, settlements, or the estate of a deceased person.
### SA108 — Capital Gains Summary
**When needed:** You dispose of assets and your total gains exceed the Annual Exempt Amount (£3,000), OR your total disposal proceeds exceed 4x the annual exempt amount (£12,000), OR you want to claim a loss.
| Box | Description |
|-----|-------------|
| 3 | Total gains (before losses and annual exempt amount) |
| 4 | Total losses |
| 6 | Gains qualifying for Business Asset Disposal Relief |
| 7 | Gains from residential property |
| 8 | Other gains |
| 9 | Losses brought forward |
| 15 | Annual exempt amount used |
| 20 | CGT due |
### SA109 — Residence, Remittance Basis etc.
**When needed:** You were not resident in the UK, or you were a dual resident, or you claim the remittance basis.
### SA101 — Additional Information
**When needed:** Covers less common situations:
- Share schemes (Enterprise Management Incentives, Company Share Option Plans, etc.)
- Post-cessation receipts
- Pension charges and unauthorised payments
- Tax avoidance schemes (DOTAS)
- Disguised remuneration
- Other income not covered by other supplementary pages
### SA110 — Tax Calculation Summary
Auto-generated when filing online. Shows the complete tax computation.
---
## Document → Form Mapping
| Document | SA Form | Key Data to Extract |
|----------|---------|-------------------|
| P60 | SA102 | Gross pay, tax deducted, NIC, employer PAYE ref |
| P45 | SA102 | Pay to date, tax to date, leaving date |
| P11D | SA102 | Benefits in kind (car, medical, etc.) |
| Bank interest summary | SA100 | Total interest received |
| Dividend vouchers | SA100 | Gross dividend amounts |
| Share dealing statements | SA108 | Disposal proceeds, acquisition costs, dates |
| Property rental accounts | SA105 | Rents, expenses, mortgage interest |
| Self-employment accounts | SA103S/F | Turnover, expenses, profit |
| Pension statement | SA100 | Contributions paid (for higher rate relief) |
| Student loan statement | SA100 | Plan type, balance (for repayment calc) |
| HMRC coding notice | Reference | Tax code, estimated income, deductions |
| SA302 (prior year) | Reference | Previous year's liability (for payment on account) |
| Gift Aid receipts | SA100 | Total qualifying donations |
| Crypto exchange reports | SA108 | Disposal proceeds, costs per trade |

View File

@ -0,0 +1,141 @@
# UK Tax Rates and Allowances — 2024/25
Tax year: 6 April 2024 to 5 April 2025
## Income Tax Bands
| Band | Taxable Income | Rate |
|------|---------------|------|
| Personal Allowance | £0 – £12,570 | 0% |
| Basic Rate | £12,571 – £50,270 | 20% |
| Higher Rate | £50,271 – £125,140 | 40% |
| Additional Rate | Over £125,140 | 45% |
**Personal Allowance taper:** Reduced by £1 for every £2 of income above £100,000. Reaches zero at £125,140. This creates an effective 60% marginal rate between £100,000 and £125,140.
## Dividend Tax Rates
| Band | Rate |
|------|------|
| Dividend Allowance | £500 at 0% |
| Basic Rate | 8.75% |
| Higher Rate | 33.75% |
| Additional Rate | 39.35% |
Dividends use up the basic/higher/additional rate bands but are taxed at dividend-specific rates. Dividends are treated as the top slice of income.
## Savings Income
| Band | Rate |
|------|------|
| Starting Rate for Savings | 0% on up to £5,000 (reduced by non-savings income above personal allowance) |
| Personal Savings Allowance (basic rate taxpayer) | £1,000 at 0% |
| Personal Savings Allowance (higher rate taxpayer) | £500 at 0% |
| Personal Savings Allowance (additional rate taxpayer) | £0 |
Savings income is taxed after non-savings income but before dividends.
## National Insurance Contributions
### Employee (Class 1)
| Threshold | Amount |
|-----------|--------|
| Primary Threshold | £12,570/year (£242/week) |
| Upper Earnings Limit | £50,270/year (£967/week) |
| Main Rate (PT to UEL) | 8% |
| Additional Rate (above UEL) | 2% |
### Self-Employed (Class 4)
| Threshold | Amount |
|-----------|--------|
| Lower Profits Limit | £12,570 |
| Upper Profits Limit | £50,270 |
| Main Rate | 6% |
| Additional Rate | 2% |
### Class 2 NIC
Abolished from 2024/25 (voluntary contributions: £3.45/week for state pension qualification).
## Student Loan Repayment Thresholds
| Plan | Annual Threshold | Rate |
|------|-----------------|------|
| Plan 1 | £22,015 | 9% |
| Plan 2 | £27,295 | 9% |
| Plan 4 | £27,660 | 9% |
| Plan 5 | £25,000 | 9% |
| Postgraduate Loan | £21,000 | 6% |
## Capital Gains Tax
### Annual Exempt Amount
£3,000
### CGT Rates
**Disposals before 30 October 2024:**
| Asset Type | Basic Rate | Higher Rate |
|-----------|-----------|------------|
| Most assets | 10% | 20% |
| Residential property | 18% | 28% |
**Disposals from 30 October 2024:**
| Asset Type | Basic Rate | Higher Rate |
|-----------|-----------|------------|
| All assets | 18% | 24% |
Business Asset Disposal Relief: 10% (lifetime limit £1 million)
## Key Allowances
| Allowance | Amount |
|-----------|--------|
| Personal Allowance | £12,570 |
| Marriage Allowance (transferable) | £1,260 |
| Blind Person's Allowance | £2,870 |
| Trading Allowance | £1,000 |
| Property Allowance | £1,000 |
| Dividend Allowance | £500 |
| Personal Savings Allowance (basic) | £1,000 |
| Personal Savings Allowance (higher) | £500 |
| CGT Annual Exempt Amount | £3,000 |
| Pension Annual Allowance | £60,000 |
| ISA Annual Allowance | £20,000 |
## High Income Child Benefit Charge
Applies when adjusted net income exceeds £60,000 (2024/25). Charge = 1% of child benefit for every £200 of income over £60,000. Full charge at £80,000.
**Child Benefit rates 2024/25:**
- Eldest/only child: £25.60/week (£1,331.20/year)
- Each additional child: £16.95/week (£881.40/year)
## Pension Tax Relief
| Parameter | Amount |
|-----------|--------|
| Annual Allowance | £60,000 |
| Money Purchase Annual Allowance | £10,000 |
| Tapered Annual Allowance starts | Adjusted income > £260,000 |
| Minimum Tapered Allowance | £10,000 |
| Carry Forward | Previous 3 tax years |
## Key Deadlines
| Event | Date |
|-------|------|
| Tax year ends | 5 April 2025 |
| Register for SA | 5 October 2025 |
| Paper return deadline | 31 October 2025 |
| Online return deadline | 31 January 2026 |
| Tax payment due | 31 January 2026 |
| 2nd payment on account | 31 July 2026 |
## Late Filing Penalties
| Delay | Penalty |
|-------|---------|
| 1 day late | £100 fixed |
| 3 months late | £10/day for up to 90 days (max £900) |
| 6 months late | 5% of tax due or £300 (whichever greater) |
| 12 months late | Further 5% of tax due or £300 (whichever greater) |

View File

@ -0,0 +1,139 @@
# UK Tax Rates and Allowances — 2025/26
Tax year: 6 April 2025 to 5 April 2026
## Income Tax Bands
| Band | Taxable Income | Rate |
|------|---------------|------|
| Personal Allowance | £0 – £12,570 | 0% |
| Basic Rate | £12,571 – £50,270 | 20% |
| Higher Rate | £50,271 – £125,140 | 40% |
| Additional Rate | Over £125,140 | 45% |
**Personal Allowance taper:** Reduced by £1 for every £2 of income above £100,000. Reaches zero at £125,140. Effective 60% marginal rate between £100,000 and £125,140.
**Frozen until April 2028** (extended to April 2031 in Budget 2025).
## Dividend Tax Rates
| Band | Rate |
|------|------|
| Dividend Allowance | £500 at 0% |
| Basic Rate | 8.75% |
| Higher Rate | 33.75% |
| Additional Rate | 39.35% |
## Savings Income
| Band | Rate |
|------|------|
| Starting Rate for Savings | 0% on up to £5,000 (reduced by non-savings income above personal allowance) |
| Personal Savings Allowance (basic rate taxpayer) | £1,000 at 0% |
| Personal Savings Allowance (higher rate taxpayer) | £500 at 0% |
| Personal Savings Allowance (additional rate taxpayer) | £0 |
## National Insurance Contributions
### Employee (Class 1)
| Threshold | Amount |
|-----------|--------|
| Primary Threshold | £12,570/year (£242/week) |
| Upper Earnings Limit | £50,270/year (£967/week) |
| Main Rate (PT to UEL) | 8% |
| Additional Rate (above UEL) | 2% |
### Self-Employed (Class 4)
| Threshold | Amount |
|-----------|--------|
| Lower Profits Limit | £12,570 |
| Upper Profits Limit | £50,270 |
| Main Rate | 6% |
| Additional Rate | 2% |
### Class 2 NIC
Abolished (voluntary contributions: £3.45/week for state pension qualification).
### Employer NIC (for reference)
| Parameter | Amount |
|-----------|--------|
| Rate | 15% (up from 13.8%) |
| Secondary Threshold | £5,000/year (down from £9,100) |
| Employment Allowance | £10,500 (up from £5,000) |
## Student Loan Repayment Thresholds
| Plan | Annual Threshold | Rate |
|------|-----------------|------|
| Plan 1 | £26,065 | 9% |
| Plan 2 | £28,470 | 9% |
| Plan 4 | £32,745 | 9% |
| Plan 5 | £25,000 | 9% |
| Postgraduate Loan | £21,000 | 6% |
## Capital Gains Tax
### Annual Exempt Amount
£3,000
### CGT Rates (full year — unified from Budget October 2024)
| Asset Type | Basic Rate | Higher Rate |
|-----------|-----------|------------|
| All assets | 18% | 24% |
Business Asset Disposal Relief: 14% (lifetime limit £1 million) — rising to 18% from April 2026.
## Key Allowances
| Allowance | Amount |
|-----------|--------|
| Personal Allowance | £12,570 |
| Marriage Allowance (transferable) | £1,260 |
| Blind Person's Allowance | £3,070 |
| Trading Allowance | £1,000 |
| Property Allowance | £1,000 |
| Dividend Allowance | £500 |
| Personal Savings Allowance (basic) | £1,000 |
| Personal Savings Allowance (higher) | £500 |
| CGT Annual Exempt Amount | £3,000 |
| Pension Annual Allowance | £60,000 |
| ISA Annual Allowance | £20,000 |
## High Income Child Benefit Charge
Applies when adjusted net income exceeds £60,000. Charge = 1% of child benefit for every £200 of income over £60,000. Full charge at £80,000.
**Child Benefit rates 2025/26:**
- Eldest/only child: £26.05/week (£1,354.60/year)
- Each additional child: £17.25/week (£897.00/year)
## Pension Tax Relief
| Parameter | Amount |
|-----------|--------|
| Annual Allowance | £60,000 |
| Money Purchase Annual Allowance | £10,000 |
| Tapered Annual Allowance starts | Adjusted income > £260,000 |
| Minimum Tapered Allowance | £10,000 |
| Carry Forward | Previous 3 tax years |
## Key Changes from 2024/25
1. **Employer NIC:** Rate up from 13.8% to 15%; secondary threshold down from £9,100 to £5,000
2. **Employment Allowance:** Up from £5,000 to £10,500 (£100k cap removed)
3. **CGT rates unified:** 18%/24% applies for full year (no split-year calculation)
4. **Business Asset Disposal Relief:** Rate increases to 14% (was 10%)
5. **Furnished Holiday Lettings:** Special tax treatment abolished
6. **Non-dom regime:** Abolished, replaced with residence-based system
7. **MTD for Income Tax:** Launches April 2026 for self-employed/landlords with income > £50,000
## Key Deadlines
| Event | Date |
|-------|------|
| Tax year ends | 5 April 2026 |
| Register for SA | 5 October 2026 |
| Paper return deadline | 31 October 2026 |
| Online return deadline | 31 January 2027 |
| Tax payment due | 31 January 2027 |
| 2nd payment on account | 31 July 2027 |

View File

@ -0,0 +1,678 @@
#!/usr/bin/env python3
"""
UK Self Assessment Tax Calculator
Accepts JSON input with income/deduction data via stdin or file argument.
Calculates income tax, NIC, student loans, CGT, and outputs JSON breakdown.
Usage:
echo '{"tax_year": "2024-25", "employment": [{"gross_pay": 45000, "tax_deducted": 6486}]}' | python3 tax-calculator.py
python3 tax-calculator.py input.json
"""
import json
import sys
from dataclasses import dataclass, field
from typing import Optional
# ─── Tax rates by year ────────────────────────────────────────────────────────
TAX_RATES = {
"2024-25": {
"personal_allowance": 12570,
"pa_taper_threshold": 100000,
"basic_rate_limit": 37700, # 12571 to 50270
"higher_rate_limit": 87440, # 50271 to 125140 (125140 - 37700 = 87440 taxable)
"basic_rate": 0.20,
"higher_rate": 0.40,
"additional_rate": 0.45,
"basic_rate_ceiling": 50270, # PA + basic_rate_limit
"additional_rate_floor": 125140,
"dividend_allowance": 500,
"dividend_basic": 0.0875,
"dividend_higher": 0.3375,
"dividend_additional": 0.3935,
"savings_starting_rate_limit": 5000,
"psa_basic": 1000,
"psa_higher": 500,
"psa_additional": 0,
"marriage_allowance": 1260,
"blind_persons_allowance": 2870,
"trading_allowance": 1000,
"property_allowance": 1000,
# NIC Class 1 (employee)
"nic1_primary_threshold": 12570,
"nic1_upper_earnings_limit": 50270,
"nic1_main_rate": 0.08,
"nic1_additional_rate": 0.02,
# NIC Class 4 (self-employed)
"nic4_lower_profits": 12570,
"nic4_upper_profits": 50270,
"nic4_main_rate": 0.06,
"nic4_additional_rate": 0.02,
# Student loans
"student_loan_plan1_threshold": 22015,
"student_loan_plan2_threshold": 27295,
"student_loan_plan4_threshold": 27660,
"student_loan_plan5_threshold": 25000,
"student_loan_postgrad_threshold": 21000,
"student_loan_rate": 0.09,
"postgrad_loan_rate": 0.06,
# CGT
"cgt_annual_exempt": 3000,
"cgt_basic_rate": 0.18, # post-Oct 2024 unified rate
"cgt_higher_rate": 0.24,
"cgt_property_basic_rate": 0.18,
"cgt_property_higher_rate": 0.24,
"cgt_badr_rate": 0.10,
# Child benefit
"child_benefit_threshold": 60000,
"child_benefit_taper_end": 80000,
"child_benefit_eldest": 1331.20,
"child_benefit_other": 881.40,
# Pension
"pension_annual_allowance": 60000,
},
"2025-26": {
"personal_allowance": 12570,
"pa_taper_threshold": 100000,
"basic_rate_limit": 37700,
"higher_rate_limit": 87440,
"basic_rate": 0.20,
"higher_rate": 0.40,
"additional_rate": 0.45,
"basic_rate_ceiling": 50270,
"additional_rate_floor": 125140,
"dividend_allowance": 500,
"dividend_basic": 0.0875,
"dividend_higher": 0.3375,
"dividend_additional": 0.3935,
"savings_starting_rate_limit": 5000,
"psa_basic": 1000,
"psa_higher": 500,
"psa_additional": 0,
"marriage_allowance": 1260,
"blind_persons_allowance": 3070,
"trading_allowance": 1000,
"property_allowance": 1000,
# NIC Class 1
"nic1_primary_threshold": 12570,
"nic1_upper_earnings_limit": 50270,
"nic1_main_rate": 0.08,
"nic1_additional_rate": 0.02,
# NIC Class 4
"nic4_lower_profits": 12570,
"nic4_upper_profits": 50270,
"nic4_main_rate": 0.06,
"nic4_additional_rate": 0.02,
# Student loans
"student_loan_plan1_threshold": 26065,
"student_loan_plan2_threshold": 28470,
"student_loan_plan4_threshold": 32745,
"student_loan_plan5_threshold": 25000,
"student_loan_postgrad_threshold": 21000,
"student_loan_rate": 0.09,
"postgrad_loan_rate": 0.06,
# CGT
"cgt_annual_exempt": 3000,
"cgt_basic_rate": 0.18,
"cgt_higher_rate": 0.24,
"cgt_property_basic_rate": 0.18,
"cgt_property_higher_rate": 0.24,
"cgt_badr_rate": 0.14,
# Child benefit
"child_benefit_threshold": 60000,
"child_benefit_taper_end": 80000,
"child_benefit_eldest": 1354.60,
"child_benefit_other": 897.00,
# Pension
"pension_annual_allowance": 60000,
},
}
def round_tax(amount: float) -> float:
"""Round to 2 decimal places."""
return round(amount, 2)
def calculate_personal_allowance(total_income: float, rates: dict, blind: bool = False, marriage_allowance_received: bool = False, marriage_allowance_given: bool = False) -> float:
"""Calculate personal allowance after taper and adjustments."""
pa = rates["personal_allowance"]
# Blind Person's Allowance
if blind:
pa += rates["blind_persons_allowance"]
# Marriage Allowance
if marriage_allowance_received:
pa += rates["marriage_allowance"]
if marriage_allowance_given:
pa -= rates["marriage_allowance"]
# Taper for income over £100,000
if total_income > rates["pa_taper_threshold"]:
excess = total_income - rates["pa_taper_threshold"]
reduction = excess / 2
pa = max(0, pa - reduction)
return pa
def calculate_income_tax(non_savings: float, savings: float, dividends: float, rates: dict, personal_allowance: float, pension_relief_gross: float = 0) -> dict:
"""
Calculate income tax using the correct stacking order:
non-savings income → savings income → dividend income.
Pension contributions extend the basic rate band.
"""
# Extend basic rate band by gross pension contributions
extended_basic_limit = rates["basic_rate_limit"] + pension_relief_gross
# Total income for band allocation
total_income = non_savings + savings + dividends
# Apply personal allowance — allocated against non-savings first, then savings, then dividends
remaining_pa = personal_allowance
taxable_non_savings = max(0, non_savings - remaining_pa)
remaining_pa = max(0, remaining_pa - non_savings)
taxable_savings = max(0, savings - remaining_pa)
remaining_pa = max(0, remaining_pa - savings)
taxable_dividends = max(0, dividends - remaining_pa)
# ─── Non-savings income tax ───
ns_tax = 0
ns_basic = 0
ns_higher = 0
ns_additional = 0
band_used = 0 # how much of the basic rate band has been used
if taxable_non_savings > 0:
# Basic rate
basic_amount = min(taxable_non_savings, extended_basic_limit)
ns_basic = basic_amount
ns_tax += basic_amount * rates["basic_rate"]
band_used += basic_amount
# Higher rate
remaining = taxable_non_savings - basic_amount
if remaining > 0:
higher_limit = rates["additional_rate_floor"] - rates["personal_allowance"] - extended_basic_limit
higher_amount = min(remaining, max(0, higher_limit))
ns_higher = higher_amount
ns_tax += higher_amount * rates["higher_rate"]
# Additional rate
additional_amount = remaining - higher_amount
if additional_amount > 0:
ns_additional = additional_amount
ns_tax += additional_amount * rates["additional_rate"]
# ─── Savings income tax ───
sav_tax = 0
sav_basic = 0
sav_higher = 0
sav_additional = 0
# Starting rate for savings (0% on up to £5,000 if non-savings income < PA + £5,000)
savings_starting_rate_available = max(0, rates["savings_starting_rate_limit"] - max(0, taxable_non_savings))
# Personal Savings Allowance
# Determine taxpayer band based on total taxable income for PSA
total_taxable = taxable_non_savings + taxable_savings + taxable_dividends
if total_taxable <= extended_basic_limit:
psa = rates["psa_basic"]
elif total_taxable <= (rates["additional_rate_floor"] - rates["personal_allowance"]):
psa = rates["psa_higher"]
else:
psa = rates["psa_additional"]
if taxable_savings > 0:
remaining_savings = taxable_savings
# Starting rate for savings (0%)
starting_rate_amount = min(remaining_savings, savings_starting_rate_available)
remaining_savings -= starting_rate_amount
band_used_for_savings = band_used + starting_rate_amount
# PSA (0%)
psa_amount = min(remaining_savings, psa)
remaining_savings -= psa_amount
band_used_for_savings += psa_amount
# Basic rate
basic_remaining = max(0, extended_basic_limit - band_used_for_savings)
sav_basic_amount = min(remaining_savings, basic_remaining)
sav_basic = sav_basic_amount
sav_tax += sav_basic_amount * rates["basic_rate"]
remaining_savings -= sav_basic_amount
band_used_for_savings += sav_basic_amount
# Higher rate
if remaining_savings > 0:
higher_limit = rates["additional_rate_floor"] - rates["personal_allowance"] - extended_basic_limit
# Adjust for non-savings that already used higher band
higher_used_by_ns = max(0, taxable_non_savings - extended_basic_limit)
higher_remaining = max(0, higher_limit - higher_used_by_ns)
sav_higher_amount = min(remaining_savings, higher_remaining)
sav_higher = sav_higher_amount
sav_tax += sav_higher_amount * rates["higher_rate"]
remaining_savings -= sav_higher_amount
# Additional rate
if remaining_savings > 0:
sav_additional = remaining_savings
sav_tax += remaining_savings * rates["additional_rate"]
# ─── Dividend income tax ───
div_tax = 0
div_basic = 0
div_higher = 0
div_additional = 0
if taxable_dividends > 0:
remaining_divs = taxable_dividends
# Dividend allowance (£500 at 0%)
div_allowance_used = min(remaining_divs, rates["dividend_allowance"])
remaining_divs -= div_allowance_used
# Determine which band dividends fall into
income_before_divs = taxable_non_savings + taxable_savings
total_band_used = income_before_divs + div_allowance_used
# Basic rate band remaining
basic_remaining = max(0, extended_basic_limit - total_band_used)
div_basic_amount = min(remaining_divs, basic_remaining)
div_basic = div_basic_amount
div_tax += div_basic_amount * rates["dividend_basic"]
remaining_divs -= div_basic_amount
total_band_used += div_basic_amount
# Higher rate
if remaining_divs > 0:
higher_ceiling = rates["additional_rate_floor"] - rates["personal_allowance"]
higher_remaining = max(0, higher_ceiling - total_band_used)
div_higher_amount = min(remaining_divs, higher_remaining)
div_higher = div_higher_amount
div_tax += div_higher_amount * rates["dividend_higher"]
remaining_divs -= div_higher_amount
# Additional rate
if remaining_divs > 0:
div_additional = remaining_divs
div_tax += remaining_divs * rates["dividend_additional"]
total_tax = round_tax(ns_tax + sav_tax + div_tax)
return {
"non_savings_income": round_tax(non_savings),
"taxable_non_savings": round_tax(taxable_non_savings),
"savings_income": round_tax(savings),
"taxable_savings": round_tax(taxable_savings),
"dividend_income": round_tax(dividends),
"taxable_dividends": round_tax(taxable_dividends),
"personal_allowance_used": round_tax(personal_allowance),
"non_savings_tax": round_tax(ns_tax),
"non_savings_breakdown": {
"basic_rate": round_tax(ns_basic),
"higher_rate": round_tax(ns_higher),
"additional_rate": round_tax(ns_additional),
},
"savings_tax": round_tax(sav_tax),
"savings_breakdown": {
"starting_rate_0pc": round_tax(min(taxable_savings, savings_starting_rate_available) if taxable_savings > 0 else 0),
"psa_0pc": round_tax(min(max(0, taxable_savings - savings_starting_rate_available), psa) if taxable_savings > 0 else 0),
"basic_rate": round_tax(sav_basic),
"higher_rate": round_tax(sav_higher),
"additional_rate": round_tax(sav_additional),
},
"dividend_tax": round_tax(div_tax),
"dividend_breakdown": {
"allowance_0pc": round_tax(min(taxable_dividends, rates["dividend_allowance"]) if taxable_dividends > 0 else 0),
"basic_rate": round_tax(div_basic),
"higher_rate": round_tax(div_higher),
"additional_rate": round_tax(div_additional),
},
"total_income_tax": total_tax,
}
def calculate_nic_class4(self_employment_profit: float, rates: dict) -> dict:
"""Calculate Class 4 NIC on self-employment profits."""
if self_employment_profit <= rates["nic4_lower_profits"]:
return {"class4_nic": 0, "main_rate_amount": 0, "additional_rate_amount": 0}
main_rate_amount = min(
max(0, self_employment_profit - rates["nic4_lower_profits"]),
rates["nic4_upper_profits"] - rates["nic4_lower_profits"],
)
additional_rate_amount = max(0, self_employment_profit - rates["nic4_upper_profits"])
class4 = (main_rate_amount * rates["nic4_main_rate"]) + (additional_rate_amount * rates["nic4_additional_rate"])
return {
"class4_nic": round_tax(class4),
"main_rate_amount": round_tax(main_rate_amount),
"additional_rate_amount": round_tax(additional_rate_amount),
}
def calculate_student_loan(gross_income: float, plans: list, rates: dict) -> dict:
"""Calculate student loan repayments."""
result = {}
total = 0
for plan in plans:
plan_lower = plan.lower().replace(" ", "")
if plan_lower in ("plan1", "1"):
threshold = rates["student_loan_plan1_threshold"]
rate = rates["student_loan_rate"]
key = "plan1"
elif plan_lower in ("plan2", "2"):
threshold = rates["student_loan_plan2_threshold"]
rate = rates["student_loan_rate"]
key = "plan2"
elif plan_lower in ("plan4", "4"):
threshold = rates["student_loan_plan4_threshold"]
rate = rates["student_loan_rate"]
key = "plan4"
elif plan_lower in ("plan5", "5"):
threshold = rates["student_loan_plan5_threshold"]
rate = rates["student_loan_rate"]
key = "plan5"
elif plan_lower in ("postgraduate", "postgrad", "pg"):
threshold = rates["student_loan_postgrad_threshold"]
rate = rates["postgrad_loan_rate"]
key = "postgraduate"
else:
continue
repayment = max(0, (gross_income - threshold) * rate)
result[key] = {
"threshold": threshold,
"rate": rate,
"repayment": round_tax(repayment),
}
total += repayment
result["total_student_loan"] = round_tax(total)
return result
def calculate_cgt(gains: list, rates: dict, remaining_basic_band: float) -> dict:
"""
Calculate Capital Gains Tax.
gains: list of {"proceeds": float, "cost": float, "type": "shares"|"property"|"other", "badr": bool}
remaining_basic_band: how much of the basic rate band is unused after income tax
"""
total_gains = 0
total_losses = 0
gains_by_type = {"shares": 0, "property": 0, "other": 0, "badr": 0}
for g in gains:
gain = g.get("proceeds", 0) - g.get("cost", 0)
if gain > 0:
if g.get("badr"):
gains_by_type["badr"] += gain
else:
gains_by_type[g.get("type", "other")] += gain
total_gains += gain
else:
total_losses += abs(gain)
# Apply losses
net_gains = max(0, total_gains - total_losses)
# Apply annual exempt amount
exempt_used = min(net_gains, rates["cgt_annual_exempt"])
taxable_gains = max(0, net_gains - exempt_used)
# Calculate CGT
cgt = 0
remaining_band = remaining_basic_band
# BADR gains first (flat rate)
badr_gains = min(gains_by_type["badr"], taxable_gains)
if badr_gains > 0:
cgt += badr_gains * rates["cgt_badr_rate"]
taxable_gains -= badr_gains
# Remaining gains: basic rate then higher rate
if taxable_gains > 0:
basic_amount = min(taxable_gains, max(0, remaining_band))
higher_amount = taxable_gains - basic_amount
# Use property rates for property gains, standard rates for others
# Simplified: use the unified rates (post Oct 2024 they're the same)
cgt += basic_amount * rates["cgt_basic_rate"]
cgt += higher_amount * rates["cgt_higher_rate"]
return {
"total_gains": round_tax(total_gains),
"total_losses": round_tax(total_losses),
"net_gains": round_tax(net_gains),
"annual_exempt_used": round_tax(exempt_used),
"taxable_gains": round_tax(max(0, net_gains - exempt_used)),
"badr_gains": round_tax(gains_by_type["badr"]),
"cgt_due": round_tax(cgt),
}
def calculate_child_benefit_charge(adjusted_net_income: float, num_children: int, rates: dict) -> dict:
"""Calculate High Income Child Benefit Charge."""
if num_children <= 0 or adjusted_net_income <= rates["child_benefit_threshold"]:
return {"charge": 0, "total_benefit": 0, "percentage_clawed_back": 0}
# Total benefit
benefit = rates["child_benefit_eldest"]
if num_children > 1:
benefit += (num_children - 1) * rates["child_benefit_other"]
# Charge: 1% per £200 over threshold
excess = adjusted_net_income - rates["child_benefit_threshold"]
percentage = min(100, (excess / 200)) # 1% per £200
charge = benefit * (percentage / 100)
return {
"total_benefit": round_tax(benefit),
"percentage_clawed_back": round(percentage, 1),
"charge": round_tax(charge),
}
def calculate_payment_on_account(sa_liability: float, tax_deducted_at_source: float) -> dict:
"""Calculate payments on account for next year."""
# POA required if SA liability > £1,000 AND less than 80% was deducted at source
total_liability = sa_liability
if total_liability <= 1000:
return {"poa_required": False, "each_payment": 0, "reason": "Liability is £1,000 or less"}
source_percentage = (tax_deducted_at_source / total_liability * 100) if total_liability > 0 else 100
if source_percentage >= 80:
return {"poa_required": False, "each_payment": 0, "reason": "80% or more was deducted at source"}
each_payment = round_tax(total_liability / 2)
return {
"poa_required": True,
"each_payment": each_payment,
"total_poa": round_tax(total_liability),
"reason": f"SA liability of £{total_liability:,.2f} with {source_percentage:.1f}% deducted at source",
}
def calculate_tax(data: dict) -> dict:
"""Main calculation entry point."""
tax_year = data.get("tax_year", "2024-25")
if tax_year not in TAX_RATES:
return {"error": f"Unsupported tax year: {tax_year}. Supported: {list(TAX_RATES.keys())}"}
rates = TAX_RATES[tax_year]
# ─── Gather income ───
# Employment
employments = data.get("employment", [])
total_employment_gross = sum(e.get("gross_pay", 0) for e in employments)
total_employment_tax = sum(e.get("tax_deducted", 0) for e in employments)
total_benefits = sum(e.get("benefits", 0) for e in employments)
# Self-employment
self_employments = data.get("self_employment", [])
total_se_profit = sum(se.get("profit", 0) for se in self_employments)
# Savings
savings_interest = data.get("savings_interest", 0)
# Dividends
dividends = data.get("dividends", 0)
# Rental
rental = data.get("rental", {})
rental_profit = rental.get("profit", 0)
rental_finance_costs = rental.get("finance_costs", 0) # mortgage interest for tax reducer
# Pension contributions
pension = data.get("pension", {})
personal_pension_gross = pension.get("personal_gross", 0) # gross amount (already includes basic rate relief)
employer_pension = pension.get("employer", 0)
# Other
other_income = data.get("other_income", 0)
# ─── Personal circumstances ───
blind = data.get("blind_persons_allowance", False)
marriage_allowance_received = data.get("marriage_allowance_received", False)
marriage_allowance_given = data.get("marriage_allowance_given", False)
student_loan_plans = data.get("student_loan_plans", [])
child_benefit = data.get("child_benefit", {})
gift_aid = data.get("gift_aid", 0)
previous_year_liability = data.get("previous_year_sa_liability", 0)
# ─── Calculate totals ───
non_savings_income = total_employment_gross + total_benefits + total_se_profit + rental_profit + other_income
total_income = non_savings_income + savings_interest + dividends
# Adjusted net income (for PA taper and child benefit): total income minus pension and gift aid
# Pension contributions extend the basic rate band and reduce adjusted net income
# Gift Aid extends the basic rate band too
adjusted_net_income = total_income - personal_pension_gross - (gift_aid * 1.25) # gross up gift aid
# Personal allowance
pa = calculate_personal_allowance(
adjusted_net_income, rates, blind, marriage_allowance_received, marriage_allowance_given
)
# Pension relief extends basic rate band
pension_relief_gross = personal_pension_gross + (gift_aid * 1.25)
# ─── Income tax ───
income_tax = calculate_income_tax(
non_savings_income, savings_interest, dividends, rates, pa, pension_relief_gross
)
# ─── Rental finance costs tax reducer (20%) ───
finance_cost_reducer = round_tax(rental_finance_costs * 0.20)
# ─── Marriage Allowance tax reducer ───
marriage_allowance_reducer = 0
if marriage_allowance_received:
marriage_allowance_reducer = round_tax(rates["marriage_allowance"] * 0.20)
# Total income tax after reducers
income_tax_after_reducers = round_tax(
max(0, income_tax["total_income_tax"] - finance_cost_reducer - marriage_allowance_reducer)
)
# ─── NIC ───
nic4 = calculate_nic_class4(total_se_profit, rates) if total_se_profit > 0 else {"class4_nic": 0}
# ─── Student loans ───
# Student loan is calculated on total relevant income
student_loan_income = total_employment_gross + total_se_profit
student_loans = calculate_student_loan(student_loan_income, student_loan_plans, rates) if student_loan_plans else {"total_student_loan": 0}
# ─── CGT ───
capital_gains = data.get("capital_gains", [])
remaining_basic_band = max(0, rates["basic_rate_limit"] + pension_relief_gross - max(0, non_savings_income + savings_interest + dividends - pa))
cgt = calculate_cgt(capital_gains, rates, remaining_basic_band) if capital_gains else {"cgt_due": 0, "total_gains": 0, "taxable_gains": 0}
# ─── Child benefit charge ───
num_children = child_benefit.get("num_children", 0)
child_benefit_charge = calculate_child_benefit_charge(adjusted_net_income, num_children, rates)
# ─── Total liability ───
total_tax_due = round_tax(
income_tax_after_reducers
+ nic4.get("class4_nic", 0)
+ student_loans.get("total_student_loan", 0)
+ cgt.get("cgt_due", 0)
+ child_benefit_charge.get("charge", 0)
)
# Tax already paid
tax_already_paid = round_tax(total_employment_tax)
# Could also include tax deducted from savings, CGT already paid, etc.
tax_already_paid += data.get("other_tax_paid", 0)
amount_owed = round_tax(total_tax_due - tax_already_paid)
# ─── Payment on account ───
sa_liability_for_poa = round_tax(total_tax_due - total_employment_tax) # SA liability excluding PAYE
poa = calculate_payment_on_account(sa_liability_for_poa, 0) # POA based on SA-only liability
# ─── Build result ───
result = {
"tax_year": tax_year,
"income_summary": {
"employment_gross": round_tax(total_employment_gross),
"employment_benefits": round_tax(total_benefits),
"self_employment_profit": round_tax(total_se_profit),
"savings_interest": round_tax(savings_interest),
"dividends": round_tax(dividends),
"rental_profit": round_tax(rental_profit),
"other_income": round_tax(other_income),
"total_income": round_tax(total_income),
"adjusted_net_income": round_tax(adjusted_net_income),
},
"personal_allowance": round_tax(pa),
"income_tax": income_tax,
"tax_reducers": {
"finance_cost_reducer": finance_cost_reducer,
"marriage_allowance_reducer": marriage_allowance_reducer,
},
"income_tax_after_reducers": income_tax_after_reducers,
"national_insurance": nic4,
"student_loans": student_loans,
"capital_gains_tax": cgt,
"child_benefit_charge": child_benefit_charge,
"headline": {
"total_income": round_tax(total_income),
"total_tax_due": total_tax_due,
"tax_already_paid": tax_already_paid,
"amount_owed": amount_owed,
"amount_refund": round_tax(abs(amount_owed)) if amount_owed < 0 else 0,
},
"payment_on_account": poa,
}
return result
def main():
# Read input
if len(sys.argv) > 1:
with open(sys.argv[1], "r") as f:
data = json.load(f)
else:
data = json.load(sys.stdin)
result = calculate_tax(data)
print(json.dumps(result, indent=2))
if __name__ == "__main__":
main()