""" Unit tests for the pure tax calculation functions. Reference figures verified against HMRC's tax calculator and HMRC guidance. All tests use the 2025/26 frozen rate structure (same as seed data). """ import sys import os sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from decimal import Decimal from datetime import date import pytest from app.services.tax_calculations import ( tax_year_for_date, parse_tax_code, calculate_income_tax, calculate_ni, calculate_cgt, calculate_dividend_tax, ) # --------------------------------------------------------------------------- # Shared rate fixture (mirrors the seeded data) # --------------------------------------------------------------------------- RATES = { "income_tax": { "bands": [ {"from": 0, "to": 12570, "rate": 0.00}, {"from": 12570, "to": 50270, "rate": 0.20}, {"from": 50270, "to": 125140, "rate": 0.40}, {"from": 125140, "to": None, "rate": 0.45}, ] }, "ni": { "bands": [ {"from": 0, "to": 12570, "rate": 0.00}, {"from": 12570, "to": 50270, "rate": 0.08}, {"from": 50270, "to": None, "rate": 0.02}, ] }, "cgt": {"exempt": 3000, "basic_rate": 0.18, "higher_rate": 0.24}, "dividend": { "allowance": 500, "basic_rate": 0.0875, "higher_rate": 0.3375, "additional_rate": 0.3935, }, } # --------------------------------------------------------------------------- # tax_year_for_date # --------------------------------------------------------------------------- class TestTaxYearForDate: def test_before_april_6(self): assert tax_year_for_date(date(2025, 4, 5)) == 2025 def test_on_april_6(self): assert tax_year_for_date(date(2025, 4, 6)) == 2026 def test_mid_year(self): assert tax_year_for_date(date(2025, 10, 1)) == 2026 def test_january(self): assert tax_year_for_date(date(2026, 1, 15)) == 2026 def test_april_5_boundary(self): # 5 April 2024 → tax_year 2024 assert tax_year_for_date(date(2024, 4, 5)) == 2024 def test_april_6_boundary(self): # 6 April 2024 → tax_year 2025 assert tax_year_for_date(date(2024, 4, 6)) == 2025 # --------------------------------------------------------------------------- # parse_tax_code # --------------------------------------------------------------------------- class TestParseTaxCode: def test_standard_1257l(self): r = parse_tax_code("1257L") assert r["allowance"] == Decimal("12570") assert r["rate_override"] is None assert r["k_code"] is False assert r["no_tax"] is False def test_standard_1257m(self): assert parse_tax_code("1257M")["allowance"] == Decimal("12570") def test_standard_1257n(self): assert parse_tax_code("1257N")["allowance"] == Decimal("12570") def test_br(self): r = parse_tax_code("BR") assert r["allowance"] == Decimal("0") assert r["rate_override"] == Decimal("0.20") def test_d0(self): r = parse_tax_code("D0") assert r["rate_override"] == Decimal("0.40") def test_d1(self): r = parse_tax_code("D1") assert r["rate_override"] == Decimal("0.45") def test_nt(self): r = parse_tax_code("NT") assert r["no_tax"] is True def test_0t(self): r = parse_tax_code("0T") assert r["allowance"] == Decimal("0") assert r["rate_override"] is None def test_k_code(self): r = parse_tax_code("K100") assert r["allowance"] == Decimal("-1000") assert r["k_code"] is True def test_k_code_large(self): r = parse_tax_code("K497") assert r["allowance"] == Decimal("-4970") def test_w1_suffix_stripped(self): r = parse_tax_code("1257L W1") assert r["allowance"] == Decimal("12570") def test_m1_suffix_stripped(self): r = parse_tax_code("1257L/M1") assert r["allowance"] == Decimal("12570") def test_lowercase(self): r = parse_tax_code("1257l") assert r["allowance"] == Decimal("12570") # --------------------------------------------------------------------------- # calculate_income_tax # --------------------------------------------------------------------------- class TestCalculateIncomeTax: def test_below_personal_allowance(self): r = calculate_income_tax(Decimal("10000"), "1257L", RATES) assert r["liability"] == Decimal("0") assert r["taxable_income"] == Decimal("0") assert r["personal_allowance"] == Decimal("12570") def test_at_personal_allowance_boundary(self): r = calculate_income_tax(Decimal("12570"), "1257L", RATES) assert r["liability"] == Decimal("0") def test_basic_rate_salary_30k(self): # £30,000 gross — taxable = £17,430, basic rate 20% r = calculate_income_tax(Decimal("30000"), "1257L", RATES) assert r["taxable_income"] == Decimal("17430") assert r["liability"] == Decimal("3486.00") def test_at_higher_rate_threshold(self): # £50,270 gross — exactly at the basic rate upper; taxable = £37,700 r = calculate_income_tax(Decimal("50270"), "1257L", RATES) assert r["taxable_income"] == Decimal("37700") # 20% on (50270 - 12570) = 37700 expected = (Decimal("37700") * Decimal("0.20")).quantize(Decimal("0.01")) assert r["liability"] == expected def test_higher_rate_taxpayer_60k(self): # £60,000 gross, 1257L # 20% on (50270 - 12570) = 37700 → £7,540 # 40% on (60000 - 50270) = 9730 → £3,892 r = calculate_income_tax(Decimal("60000"), "1257L", RATES) assert r["taxable_income"] == Decimal("47430") # 60000 - 12570 expected_basic = (Decimal("37700") * Decimal("0.20")).quantize(Decimal("0.01")) expected_higher = (Decimal("9730") * Decimal("0.40")).quantize(Decimal("0.01")) assert r["liability"] == expected_basic + expected_higher def test_personal_allowance_taper_110k(self): # £110,000 — allowance tapered by £5,000 → £7,570 r = calculate_income_tax(Decimal("110000"), "1257L", RATES) assert r["personal_allowance"] == Decimal("7570") def test_personal_allowance_taper_125140(self): # At £125,140 allowance tapers to zero r = calculate_income_tax(Decimal("125140"), "1257L", RATES) assert r["personal_allowance"] == Decimal("0") def test_personal_allowance_above_125140(self): # Above £125,140 allowance stays at zero r = calculate_income_tax(Decimal("150000"), "1257L", RATES) assert r["personal_allowance"] == Decimal("0") def test_br_code(self): r = calculate_income_tax(Decimal("30000"), "BR", RATES) assert r["liability"] == (Decimal("30000") * Decimal("0.20")).quantize(Decimal("0.01")) def test_d0_code(self): r = calculate_income_tax(Decimal("30000"), "D0", RATES) assert r["liability"] == (Decimal("30000") * Decimal("0.40")).quantize(Decimal("0.01")) def test_nt_code(self): r = calculate_income_tax(Decimal("100000"), "NT", RATES) assert r["liability"] == Decimal("0") def test_k_code_increases_taxable(self): # K100 = -£1000 allowance → taxable = gross + £1000 r = calculate_income_tax(Decimal("30000"), "K100", RATES) assert r["taxable_income"] == Decimal("31000") def test_remaining_basic_rate_band_basic_taxpayer(self): # £30k gross → remaining = 50270 - 30000 = 20270 r = calculate_income_tax(Decimal("30000"), "1257L", RATES) assert r["remaining_basic_rate_band"] == Decimal("20270") def test_remaining_basic_rate_band_higher_taxpayer(self): # £60k gross > £50270 → band exhausted r = calculate_income_tax(Decimal("60000"), "1257L", RATES) assert r["remaining_basic_rate_band"] == Decimal("0") # --------------------------------------------------------------------------- # calculate_ni # --------------------------------------------------------------------------- class TestCalculateNI: def test_below_threshold(self): r = calculate_ni(Decimal("12570"), RATES) assert r["liability"] == Decimal("0") def test_basic_rate_salary_30k(self): # NI on (30000 - 12570) = £17,430 at 8% = £1,394.40 r = calculate_ni(Decimal("30000"), RATES) assert r["liability"] == Decimal("1394.40") def test_above_upper_earnings_limit(self): # NI on (50270 - 12570) = £37,700 at 8% = £3,016 + (60000 - 50270) = £9,730 at 2% = £194.60 r = calculate_ni(Decimal("60000"), RATES) assert r["liability"] == Decimal("3016.00") + Decimal("194.60") # --------------------------------------------------------------------------- # calculate_cgt # --------------------------------------------------------------------------- class TestCalculateCGT: def test_below_exempt(self): r = calculate_cgt(Decimal("1000"), Decimal("20000"), RATES) assert r["liability"] == Decimal("0") assert r["taxable_gain"] == Decimal("0") def test_exactly_exempt(self): r = calculate_cgt(Decimal("3000"), Decimal("20000"), RATES) assert r["liability"] == Decimal("0") def test_basic_rate_taxpayer(self): # Gain £10,000 — exempt £3,000 — taxable £7,000 all at basic rate 18% r = calculate_cgt(Decimal("10000"), Decimal("20000"), RATES) assert r["taxable_gain"] == Decimal("7000") assert r["liability"] == (Decimal("7000") * Decimal("0.18")).quantize(Decimal("0.01")) def test_higher_rate_taxpayer(self): # remaining_basic_rate_band = 0 → all at higher rate 24% r = calculate_cgt(Decimal("10000"), Decimal("0"), RATES) assert r["liability"] == (Decimal("7000") * Decimal("0.24")).quantize(Decimal("0.01")) def test_split_basic_higher(self): # taxable gain £7,000; remaining_brb £4,000 → £4k at 18%, £3k at 24% r = calculate_cgt(Decimal("10000"), Decimal("4000"), RATES) expected = ( (Decimal("4000") * Decimal("0.18")) + (Decimal("3000") * Decimal("0.24")) ).quantize(Decimal("0.01")) assert r["liability"] == expected def test_negative_gain_no_tax(self): r = calculate_cgt(Decimal("-5000"), Decimal("20000"), RATES) assert r["liability"] == Decimal("0") assert r["taxable_gain"] == Decimal("0") # --------------------------------------------------------------------------- # calculate_dividend_tax # --------------------------------------------------------------------------- class TestCalculateDividendTax: def test_within_allowance(self): r = calculate_dividend_tax(Decimal("400"), Decimal("20000"), RATES) assert r["liability"] == Decimal("0") def test_exactly_allowance(self): r = calculate_dividend_tax(Decimal("500"), Decimal("20000"), RATES) assert r["liability"] == Decimal("0") def test_basic_rate_band(self): # £1,500 dividends — allowance £500 — taxable £1,000 at basic 8.75% r = calculate_dividend_tax(Decimal("1500"), Decimal("20000"), RATES) assert r["taxable_dividends"] == Decimal("1000") assert r["liability"] == (Decimal("1000") * Decimal("0.0875")).quantize(Decimal("0.01")) def test_higher_rate_band(self): # Remaining basic = 0 → taxable £1,000 at higher 33.75% r = calculate_dividend_tax(Decimal("1500"), Decimal("0"), RATES) assert r["liability"] == (Decimal("1000") * Decimal("0.3375")).quantize(Decimal("0.01")) def test_no_dividends(self): r = calculate_dividend_tax(Decimal("0"), Decimal("20000"), RATES) assert r["liability"] == Decimal("0")