Skip to content

Require a Pension Credit payment for savings-credit-only receipt - #2373

Merged
MaxGhenis merged 3 commits into
mainfrom
savings-credit-only-requires-payment
Oct 12, 2026
Merged

MaxGhenis merged 3 commits into
mainfrom
savings-credit-only-requires-payment

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2372

What changes

in_receipt_of_savings_credit_only now needs a Pension Credit payment. It computes paid = benunit("pension_credit", period) > 0 and requires it:

  • on the normal path: eligible & would_claim & paid & (guarantee_credit <= 0) & (savings_credit > 0);
  • under gov.contrib.freeze_pension_credit: paid & baseline....

#1987 (still open) uses the same pattern for in_receipt_of_guarantee_credit. The two PRs edit different files, so they don't conflict.

Why. The pensioner Council Tax Reduction schemes (SI 2012/2885 Sch 1 para 14; WSI 2013/3029 Sch 1 para 8; SSI 2012/319 reg 25) and pension-age Housing Benefit (SI 2006/214 reg 27) use the Secretary of State's assessment for someone "who has, or whose partner has, an award of state pension credit comprising only the savings credit".

Take a reform that stops Pension Credit being paid, for example Scenario(simulation_modifier=lambda sim: sim.tax_benefit_system.neutralize_variable("pension_credit")). On main, a savings-credit-only family kept that assessment, with or without the freeze, even though it no longer has the award. Example, 2026, Maidstone, council tax £2,000:

Single pensioner aged 90, State Pension £12,480 Unreformed Pension Credit stopped, main Pension Credit stopped, this PR
Savings credit computed £892.22 £892.22 £892.22
Pension Credit paid £892.22 £0 £0
in_receipt_of_savings_credit_only true true false
With £12,000 savings and £5,000 other property: CTR capital tested £17,000 (PC assessment) £17,000 £12,000 (savings)
With £12,000 savings and £5,000 other property: CTR £0 £0 £2,000

The tenant version of that family on main also kept a pension-age Housing Benefit tariff income of £0 (reg 27(5)), where Housing Benefit's own rule counts £208 a year (test_known_families_detect_main).

Scope of the proxy. A payment stands in for the award. The model does not withhold awards under 10p a week (SPC Regs 2002 reg 13), so every claimed award is paid. Under disable_simulated_benefits, which sets pension_credit to the reported amount, receipt now also needs reported Pension Credit. The docstring and changelog say both.

Invariants

Each is stated for model-computed credits and tested in policyengine_uk/tests/test_savings_credit_only_receipt_properties.py (Hypothesis, generated populations of pension-age, mixed-age and working-age families, owners and council tenants, in England, Wales and Scotland):

  1. No unreformed change (differential). In the unreformed model pension_credit = pension_credit_entitlement * would_claim_pc, and the entitlement is guarantee credit plus savings credit for an eligible family. So eligible, claiming and savings_credit > 0 already imply a payment. Under this PR the flag and every output that reads it (CTR applicable income, capital and award; HB applicable income, tariff income, capital and award) equal main's, which the test runs through main's formula on the same population.
  2. Receipt implies payment. In every scenario tested, receipt implies pension_credit > 0 and excludes in_receipt_of_guarantee_credit. In the unreformed model, the paid award equals the savings credit.
  3. Payment. A reform that neutralises pension_credit ends receipt for every family, with or without the freeze. This fails on main.
  4. Metamorphic. With Pension Credit not paid, a family not in receipt of a guarantee credit is assessed for CTR and HB exactly as if it did not claim Pension Credit. This fails on main. Families in receipt of a guarantee credit are left out because main's in_receipt_of_guarantee_credit doesn't yet need a payment; Qualify targeted childcare through Pension Credit only on receipt of a guarantee credit #1987 adds that.
  5. Freeze. Under the freeze alone, receipt is the baseline receipt whatever the reform does to the computed credits (savings credit rate rise, higher guarantee, no guarantee), on main and here alike.

Tests run

  • New file on this branch: 6 passed (uv run --frozen pytest policyengine_uk/tests/test_savings_credit_only_receipt_properties.py -n 3).
  • The same file run against origin/main's source: the 4 tests for invariants 3 and 4 fail, each at the assertion that receipt ends; the 2 for invariants 1 and 5 pass, as designed.
  • Readers of the flag on this branch: test_council_tax_reduction_pension_credit_properties.py, test_housing_benefit_savings_credit_only_properties.py and test_council_tax_reduction_carer_income_properties.py, 18 passed.
  • YAML: the four files that assert the flag (HB savings-credit-only, HB pension-age means test, CTR Pension Credit, CTR pensioner earnings disregard) plus tests/policy/baseline/gov/dwp/pension_credit/, 266 passed.
  • ruff format and ruff check on the changed files: clean.

Enhanced FRS comparison

These are real microsimulation runs on uk-data-private 1.58.0 enhanced_frs_2024_25.h5 (61,214 benefit units). One process runs this branch and main's formula, loaded from the file at origin/main 3f174d0, on the same data and compares them. Only aggregates were written. Script and output: flag_compare.py / flag_compare.json (kept locally, not attached: FRS licence).

Unreformed, 2025 to 2030: no change (invariant 1). In every year:

  • the flag is identical under main and this branch;
  • none of the seven outputs that read it changes for any record;
  • pension_credit = pension_credit_entitlement * would_claim_pc holds for every record;
  • no record meets main's conditions without a payment.
Year Savings-credit-only benefit units (records / weighted) Records whose flag differs Records whose CTR or HB outputs differ
2025 261 / 103.0k 0 0
2026 244 / 97.3k 0 0
2027 220 / 93.7k 0 0
2028 201 / 78.1k 0 0
2029 178 / 74.2k 0 0
2030 161 / 79.7k 0 0

Pension Credit stopped (pension_credit neutralised), 2026. Main still treats 244 records (97.3k weighted benefit units) as having a savings-credit-only award; this branch treats none as having one. These figures describe what the fix changes for that hypothetical reform; they are not a costing of the current system. Fix minus main:

Output Records that differ (weighted) Total change
Council Tax Reduction award 13 (4.4k) +£0.86m
Housing Benefit award 40 (36.9k) −£225.1m
Housing Benefit assessable capital 210 (90.1k) +£4,897m
Housing Benefit tariff income 144 (68.5k) +£535.9m
CTR assessable capital 222 (89.2k) +£407.6m

Review

Independent reviews ran on Subfleet lanes. No Codex lane admitted them, so they were served by Opus 5.5:

  • Full review at 05c649b: APPROVE. Its minor findings are addressed in 61b29a8: the reg 13 proxy and disable_simulated_benefits are now in the docstring and changelog, the law references are added, and the test's age range is corrected.
  • Delta review at 61b29a8: APPROVE. Its two nits are applied in 6731dd7: 75 is exactly on the savings credit age cutoff, so the outside range is 67 to 74, and reg 13 withholds payment rather than the award.
  • Confirmation review at 6731dd7: APPROVE. git diff --stat 61b29a81a 6731dd7c4 touches only the two files.

axiom: n/a: reform-path behaviour of a model receipt flag; no change to the encoded law

🤖 Generated with Claude Code

MaxGhenis and others added 3 commits October 11, 2026 22:50
in_receipt_of_savings_credit_only now needs pension_credit > 0, on the
normal path and under the Pension Credit freeze, as #1987 does for
in_receipt_of_guarantee_credit. A reform that stops Pension Credit being
paid no longer leaves savings-credit-only families on the Pension Credit
assessment for pensioner Council Tax Reduction (SI 2012/2885 Sch 1 para
14) and pension-age Housing Benefit (SI 2006/214 reg 27).

Unreformed outputs do not change: pension_credit is the entitlement of a
claiming family, so eligible, claiming and savings_credit > 0 already
imply a payment.

Fixes #2372

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review follow-ups on #2373:
- the docstring now says a payment stands in for the award because the
  model does not withhold awards under 10p a week (SPC Regs 2002 reg 13),
  and that under disable_simulated_benefits receipt also needs reported
  Pension Credit; the changelog fragment says the same;
- references add SPC Regs 2002 reg 13, SI 2012/2885 Sch 1 para 14 and
  SI 2006/214 reg 27;
- the test's outside-the-age-rule range is 67 to 75: with no sex given
  the model's savings credit age rule is met in 2026 from age 76.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Delta-review nits on #2373: at age 75 the model reaches State Pension age
exactly on the savings credit cutoff, so the outside range is 67 to 74;
and reg 13 withholds payment, not the award, so the docstring says so
and notes that modelling it would need the flag to test the award.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@MaxGhenis
MaxGhenis marked this pull request as ready for review October 12, 2026 08:52
@MaxGhenis
MaxGhenis merged commit 3d4c3e3 into main Oct 12, 2026
10 checks passed
@MaxGhenis
MaxGhenis deleted the savings-credit-only-requires-payment branch October 12, 2026 08:52
MaxGhenis added a commit that referenced this pull request Oct 13, 2026
Brings in #2119 (UC claims at legacy closure), #2373, #1905, #2177,
#2305, #2306, #2325, #2376, #2379, #2380, #2388 and #2364.

One conflict, in the UC docs notebook: kept main's new paragraph on
claims at legacy closure and this branch's benefit cap sentence.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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.

in_receipt_of_savings_credit_only ignores whether Pension Credit is paid

1 participant