"""UK tax regime — bands, allowances, tapers.""" from decimal import Decimal from hypothesis import given from hypothesis import strategies as st from fire_planner.tax.base import TaxInputs from fire_planner.tax.uk import ( PA_TAPER_CEILING, PERSONAL_ALLOWANCE, UkTaxRegime, taper_personal_allowance, ) def test_pa_no_taper_below_100k() -> None: assert taper_personal_allowance(Decimal("80000")) == PERSONAL_ALLOWANCE def test_pa_full_taper_at_ceiling() -> None: assert taper_personal_allowance(PA_TAPER_CEILING) == Decimal("0") def test_pa_partial_taper_at_110k() -> None: # £10k above floor → £5k reduction off PA assert taper_personal_allowance(Decimal("110000")) == PERSONAL_ALLOWANCE - Decimal("5000") def test_zero_income_zero_tax() -> None: b = UkTaxRegime().compute_tax(TaxInputs()) assert b.total == Decimal("0") def test_isa_only_zero_tax() -> None: b = UkTaxRegime().compute_tax(TaxInputs(isa_withdrawals=Decimal("100000"))) assert b.total == Decimal("0") def test_below_pa_zero_tax() -> None: b = UkTaxRegime().compute_tax(TaxInputs(earned_income=Decimal("12000"))) # NI primary threshold matches PA so NI is zero too. assert b.total == Decimal("0") def test_basic_rate_paye_smoke() -> None: # £30k earned: £17,430 taxable @ 20% = £3,486 income tax # NI: £17,430 @ 8% = £1,394.40 b = UkTaxRegime().compute_tax(TaxInputs(earned_income=Decimal("30000"))) assert b.income_tax == Decimal("3486.00") assert b.national_insurance == Decimal("1394.40") def test_higher_rate_paye_100k() -> None: # £100k earned, PA still full (taper starts strictly above £100k): # taxable = £87,430 # £37,700 @ 20% = £7,540 # £49,730 @ 40% = £19,892 # total income tax = £27,432 b = UkTaxRegime().compute_tax(TaxInputs(earned_income=Decimal("100000"))) assert b.income_tax == Decimal("27432.00") def test_pa_taper_at_125k() -> None: # £125,000: PA = 12,570 - (25,000/2) = 12,570 - 12,500 = 70 # taxable = 125,000 - 70 = 124,930 # £37,700 @ 20% = £7,540 # £87,230 @ 40% = £34,892 # total = £42,432 b = UkTaxRegime().compute_tax(TaxInputs(earned_income=Decimal("125000"))) assert b.income_tax == Decimal("42432.00") def test_additional_rate_above_125k() -> None: # £200k earned: PA fully tapered. # taxable income = £200,000 # £37,700 @ 20% = £7,540 # £87,440 @ 40% = £34,976 # £74,860 @ 45% = £33,687 # total = £76,203 b = UkTaxRegime().compute_tax(TaxInputs(earned_income=Decimal("200000"))) assert b.income_tax == Decimal("76203.00") def test_cgt_basic_rate_only() -> None: # No earned income, £20k gains: # exempt £3k → £17k taxable @ 18% (basic band has plenty of room) # = £3,060 b = UkTaxRegime().compute_tax(TaxInputs(capital_gains=Decimal("20000"))) assert b.capital_gains_tax == Decimal("3060.00") def test_cgt_spans_into_higher_band() -> None: # £30k earned (taxable income £17,430 — well under £37,700 band top) # £40k gains: # exempt £3k → £37k taxable # basic band remaining = 37,700 - 17,430 = 20,270 → @ 18% = £3,648.60 # higher = 37,000 - 20,270 = 16,730 → @ 24% = £4,015.20 # total CGT = £7,663.80 b = UkTaxRegime().compute_tax( TaxInputs(earned_income=Decimal("30000"), capital_gains=Decimal("40000"))) assert b.capital_gains_tax == Decimal("7663.80") def test_dividend_basic_rate() -> None: # No other income, £10k dividends: # allowance £500 → £9,500 taxable # Stacked on top of taxable_ordinary=0, so basic band has £37,700 room. # All £9,500 @ 8.75% = £831.25 b = UkTaxRegime().compute_tax(TaxInputs(dividends=Decimal("10000"))) assert b.dividend_tax == Decimal("831.2500") def test_pension_25pc_tax_free() -> None: # £40k pension drawdown, no other income: # PCLS = £10k tax-free # Taxable pension = £30k → £17,430 taxable @ 20% = £3,486 b = UkTaxRegime().compute_tax(TaxInputs(pension_withdrawal=Decimal("40000"))) assert b.income_tax == Decimal("3486.00") assert b.national_insurance == Decimal("0") # NI not on pension def test_total_equals_sum_of_components() -> None: inputs = TaxInputs( earned_income=Decimal("60000"), capital_gains=Decimal("15000"), dividends=Decimal("8000"), ) b = UkTaxRegime().compute_tax(inputs) assert (b.total == b.income_tax + b.national_insurance + b.capital_gains_tax + b.dividend_tax + b.healthcare_levy + b.other) @given(income=st.decimals( min_value=0, max_value=500_000, places=2, allow_nan=False, allow_infinity=False)) def test_tax_monotone_in_earned_income(income: Decimal) -> None: """Adding earned income never decreases total tax.""" base = UkTaxRegime().compute_tax(TaxInputs(earned_income=income)) plus = UkTaxRegime().compute_tax(TaxInputs(earned_income=income + Decimal("1000"))) assert plus.total >= base.total @given(gains=st.decimals( min_value=0, max_value=500_000, places=2, allow_nan=False, allow_infinity=False)) def test_cgt_monotone_in_gains(gains: Decimal) -> None: base = UkTaxRegime().compute_tax(TaxInputs(capital_gains=gains)) plus = UkTaxRegime().compute_tax(TaxInputs(capital_gains=gains + Decimal("1000"))) assert plus.capital_gains_tax >= base.capital_gains_tax