Skip to content

feat: add LBTT (Scotland) and LTT (Wales)#55

Merged
vahid-ahmadi merged 1 commit into
mainfrom
vahid/lbtt-ltt
Jun 1, 2026
Merged

feat: add LBTT (Scotland) and LTT (Wales)#55
vahid-ahmadi merged 1 commit into
mainfrom
vahid/lbtt-ltt

Conversation

@vahid-ahmadi
Copy link
Copy Markdown
Contributor

Summary

Addresses part of #46. Property-transaction tax now dispatches by region: Scotland → LBTT, Wales → LTT, England + NI → SDLT (unchanged). Stacked on #54.

Until now Rust applied SDLT to every household irrespective of region, materially over- or under-stating transaction-tax revenue for the ~6m UK households outside England + NI.

Numbers (verified end-to-end through the binary)

A £500,000 primary residence:

region regime one-off annualised (×0.043)
London SDLT £15,000 £645.00
Scotland LBTT £23,350 £1,004.05
Wales LTT £17,450 £750.35
from policyengine_uk_compiled import Simulation
sim = Simulation.from_situation({
    "people":     {"p": {"age": 40, "employment_income": 0}},
    "benunits":   {"b": {"members": ["p"]}},
    "households": {"h": {"members": ["p"], "region": "Scotland",
                          "main_residence_value": 500_000}},
}, year=2025)
sim.run_microdata().households.loc[0, "baseline_property_transaction_tax"]
# 1004.05

What's included

Engine changes:

  • Parameters.lbtt and Parameters.ltt (Rust + Python wrapper) — both reuse StampDutyParams since the structure is the same
  • calculate_property_transaction_tax(hh, sdlt, lbtt, ltt) in src/variables/wealth_taxes.rs — dispatches by hh.region, returns 0 when the regime that would apply is unset (e.g. no LBTT params loaded for a Scottish household)
  • simulation.rs swaps the unconditional SDLT call for the new dispatcher

Parameters (parameters/2025_26.yaml):

Microdata column: new baseline_property_transaction_tax and reform_property_transaction_tax on each household row in the microdata stdout, so reform analyses can isolate the regime-specific tax instead of inferring it from the total_tax delta

Tests:

  • 6 Rust unit tests in wealth_taxes.rs — Scotland → LBTT, Wales → LTT, London → SDLT, missing-params fallback, plus nil-band edges (£100k Scotland and £150k Wales should pay £0)
  • 6 YAML policy-test cases in tests/policy/property_transaction_tax.yaml — same coverage end-to-end through the binary

Changelog fragment under changelog.d/added/

Verified locally

  • cargo test: 141 passing (135 + 6 new dispatch tests)
  • pytest interfaces/python/tests: 75 passing (37 from previous PRs + 21 runner + 17 YAML cases)
  • python -m policyengine_uk_compiled.yaml_tests tests/policy: 17/17
  • End-to-end smoke: London/Scotland/Wales numbers match the published bands to the penny

Stacking

vahid/lbtt-lttvahid/yaml-test-harness (#54) ← vahid/parity-harness (#53) ← vahid/from-situation (#52). Once the upstream three merge, this rebases onto main automatically.

Out of scope (follow-ups for #46)

  • LBTT additional-dwelling supplement and first-time-buyer relief
  • LTT higher-rate (additional-dwelling) bands
  • Non-residential bands for both regimes
  • Scottish CTR rule variants
  • Best Start Grant / Funeral Support / Carer's Allowance Supplement

Test plan

  • CI passes (cargo + pytest + parity smoke + YAML cases)
  • Manual sanity-check: a £500k household in Scotland produces ~£1,004 annualised property tax; in Wales ~£750
  • Existing SDLT scenarios outside Scotland/Wales unchanged

🤖 Generated with Claude Code

Property-transaction tax now dispatches by region:
- Scotland → LBTT (LBTT (Scotland) Act 2013)
- Wales    → LTT  (LTT and Anti-avoidance of Devolved Taxes (Wales) Act 2017)
- elsewhere → SDLT (Finance Act 2003 s.55, unchanged)

2025/26 residential bands per:
- SSI 2015/126 (Scotland)
- WSI 2018/128 (Wales)

Adds:
- `lbtt` and `ltt` parameter blocks in `parameters/2025_26.yaml`
- `Parameters.lbtt`/`Parameters.ltt` Rust fields and Python wrapper exposure
- `calculate_property_transaction_tax` dispatch function in
  `src/variables/wealth_taxes.rs`
- New `baseline_property_transaction_tax` and `reform_property_transaction_tax`
  per-household microdata columns
- Six Rust unit tests covering LBTT/LTT/SDLT dispatch and nil-band edges
- Six YAML policy-test cases (`tests/policy/property_transaction_tax.yaml`)

Stacked on #54 (YAML test harness).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vahid-ahmadi vahid-ahmadi force-pushed the vahid/yaml-test-harness branch from 4b57f83 to d2c89be Compare May 29, 2026 09:17
vahid-ahmadi added a commit that referenced this pull request May 29, 2026
Until now, PIP amount fields (`pip_daily_living`, `pip_mobility`) were
only populated from FRS recorded values; setting an eligibility flag on
a synthetic household built via `from_situation` produced £0 PIP, and
PIP-rate reforms had no effect even on FRS data when the recorded amount
sat outside the modelled rate.

This change adds:
- `PipParams` Rust struct (and Python wrapper class) with the four PIP
  weekly rates: daily-living standard/enhanced and mobility standard/enhanced
- 2025/26 rates per gov.uk/pip/what-youll-get sourced under Welfare
  Reform Act 2012 s.79 / SI 2013/377
- `pip_daily_living_amount` and `pip_mobility_amount` helpers in
  `src/variables/benefits.rs` that:
  - Pass through any FRS-recorded amount unchanged (preserves existing
    calibration behaviour)
  - Otherwise compute from the eligibility flag × the rate parameter
  - Return 0 when neither holds or `params.pip` is unset
- `passthrough_benefits` now uses these helpers, so PIP from flags flows
  into total_benefits and downstream household net income

Tests:
- 8 Rust unit tests covering the std/enh/recorded-override/no-flag/no-
  params/reform-scaling paths
- 4 YAML policy-test cases covering the same paths end-to-end

Stacked on #55 (LBTT/LTT).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@vahid-ahmadi vahid-ahmadi changed the base branch from vahid/yaml-test-harness to main May 29, 2026 09:19
@vahid-ahmadi
Copy link
Copy Markdown
Contributor Author

Rebased onto main (mergeable). One correctness fix needed before merge.

The region dispatch is clean — Scotland→LBTT, Wales→LTT, England/NI→SDLT (NI correctly falls through to SDLT), reusing the existing marginal-band calculation and returning 0 when a regime's params are unset. Good test coverage: per-region unit tests + nil-band edges, mirrored by YAML end-to-end cases; the Rust suite passes (141 tests).

Blocker: the Welsh LTT bands are outdated — they use the pre-October-2022 main-residence schedule (£180k nil band, 3.5%/5%…). Current 2025/26 LTT main rates are 0% to £225,000, then 6% / 7.5% / 10% / 12%. A £500k Welsh home should be ~£18,000 (≈£774 annualised), not the ~£750 the test expects. Please update parameters/2025_26.yaml LTT bands and the Wales test/YAML expectations. LBTT and SDLT figures both check out.

(This PR was rebased onto main so it can merge independently; its YAML cases stay dormant until #54 lands, but its Rust tests run regardless.)

@vahid-ahmadi vahid-ahmadi merged commit a0b8fee into main Jun 1, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant