Skip to content

Gate Savings Credit on Pension Credit eligibility - #2458

Merged
juaristi22 merged 1 commit into
mainfrom
fix-2121-savings-credit-eligibility
Oct 19, 2026
Merged

juaristi22 merged 1 commit into
mainfrom
fix-2121-savings-credit-eligibility

Conversation

@juaristi22

@juaristi22 juaristi22 commented Oct 16, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2121

Computed savings_credit now uses defined_for = "is_pension_credit_eligible", matching Guarantee Credit and Pension Credit entitlement. The existing amount formula, local Savings Credit conditions, mixed-age savings and legacy migration helpers are unchanged. Explicit amount inputs retain their existing override behavior.

A 2026 couple aged 80/60 with £20,000 State Pension and no protected claim history previously had £971.40 raw Savings Credit despite being ineligible for Pension Credit. The existing default marriage/Marriage Allowance assumptions give income tax £1,234, net income £18,766 and 0.6 × (18,766 − 329.75 × 52) = 971.40. The component is now 0; Pension Credit entitlement/payment and Housing Benefit were already 0 and remain so. Clearly eligible older-pensioner controls retain their independently calculated Pension Credit and Housing Benefit amounts.

Reproducer: policyengine_uk/tests/test_savings_credit_eligibility.py::test_shared_gate_preserves_final_pension_credit_and_consumers

The entitlement basis is State Pension Credit Act 2002 s 1, read with s3 and s4. The conditional mixed-age saving in SI 2019/37 art 4 is preserved. This does not certify every 2016 Savings Credit continuity condition, inherited history proxy or Northern Ireland means-test detail; the positive controls avoid relying on protected mixed-age continuity.

Validation: 286 passed (4 new and 282 existing), covering Pension Credit age/eligibility/amounts, final Pension Credit/Housing Benefit/Council Tax Reduction invariance, childcare regressions, receipt/freeze/rate reforms and legacy routes. Both calculation orders and a direct Savings Credit input are covered. Two existing isolated formula tests gain age 80 as their Pension Credit eligibility precondition, with their numeric expectations unchanged. make format, Ruff and git diff --check passed. The initial RED exposed the gate and an old-amount oracle that omitted the default Marriage Allowance; corrected RED reproduced 3 gate failures/1 control pass on unchanged production.

Local validation used Python 3.13.14/Core 3.32.27; the locked Core 3.33.2 environment and full hosted CI remain to be checked. No full-suite/docs/private population run or aggregate effect measurement was performed. Eligible and off-route synthetic final awards were checked; the issue's requested representative microsimulation invariance measurement remains unrun. Existing 2026 rates supplied the numeric regression premises; this PR does not update or freshly audit them.

Impact (UK hub, 2026-10-19): none.

  • The run: Aggregate impact run 39316701085 compares main e2ea32893 with main merged with this head (ab4358193), on certified policyengine-uk-data 1.58.1, fiscal years 2025-26 to 2031-32.
  • The result: no aggregate changes in any year, and no household's net income changes. No check is broken; "VAT is never negative" fails on both sides, as on main. This is the representative invariance measurement for final outputs that savings_credit has no Pension Credit eligibility condition (guarantee_credit fixed in #1896) #2121 asked for.
  • Why: both production readers of savings_credit (pension_credit_entitlement, in_receipt_of_savings_credit_only) already require Pension Credit eligibility. Only the savings_credit component itself changes, and it isn't a reported aggregate.

axiom: TheAxiomFoundation/rulespec-uk#359 queued

@juaristi22 juaristi22 self-assigned this Oct 16, 2026

@vahid-ahmadi vahid-ahmadi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks right, fine to approve. Note the knock-on for #2092.

@juaristi22, gating savings_credit with defined_for = "is_pension_credit_eligible" matches guarantee_credit and pension_credit_entitlement. It also follows SPCA 2002 s1, read with s3: Savings Credit is an element of Pension Credit, so it needs the same entitlement conditions. is_pension_credit_eligible reads is_savings_credit_eligible, not savings_credit, so there is no cycle.

  1. should, policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit/savings_credit/savings_credit.yaml: #2092 (open) adds three cases to this file. They set is_savings_credit_eligible: true but no age, and expect a positive savings_credit. After this PR those cases return 0. I ran #2092's Case 3 inputs on this branch and got 0 where #2092 expects 413.976. Git may merge the two cleanly, so this would only show up as a test failure. Whichever merges second needs age: 80 on those cases, as you did for cases 1 and 2. A comment on #2092 would help its author.

What I checked

  • Read the issue, the PR body and the full diff (including test_savings_credit_eligibility.py), plus is_pension_credit_eligible, is_savings_credit_eligible and the consumers (pension_credit_entitlement, in_receipt_of_savings_credit_only).
  • Worked the both-over-80 couple by hand: Savings Credit 0.6 x (18,000 - 329.75 x 52) = £511.80; Guarantee Credit 18,889 - 18,000 = £889; total £1,400.80. This matches.
  • Ran policyengine-core test policyengine_uk/tests/policy/baseline/gov/dwp/pension_credit: 208 passed. pytest test_savings_credit_eligibility.py: 4 passed. With the formula file reverted: 3 failed, 1 passed.
  • ruff check and ruff format --check on both changed Python files: clean.
  • Hosted CI on 287cd8b is green.
  • Process: the changelog fragment is .fixed.md; the Reproducer and axiom lines are present; the Fix evidence check is green.
  • Not checked: the microsimulation check the issue asks for (that pension_credit and pension_credit_entitlement totals are unchanged). It needs the private data.

🤖 Generated with Claude Code

@vahid-ahmadi vahid-ahmadi left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving, per my review above. The shoulds and nits can follow.

@MaxGhenis

Copy link
Copy Markdown
Collaborator

UK hub: an independent review at 287cd8b01 (Opus 5.5, code reading; it ran no tests) gave APPROVE WITH NITS. Max's d1728 has the hub merge this on its gates; these nits are for a follow-up, not blockers. @juaristi22, FYI.

Confirmed:

  • The law is right. s.1(2), (3)(b) and (4) with s.4(1A), and SI 2019/37 art 4's saving routes through meets_pension_credit_age_conditions.
  • No new dependency cycle, and an input value still overrides (core returns cached inputs before the defined_for mask).
  • The new tests fail on main as the body says, and all the arithmetic checks against the 2026 parameters.

Nits:

  1. A positive case for a protected mixed-age couple (art 4). No test pins one. The reviewer's hand figures, not run: has_mixed_age_couple_pension_credit_saving: true for the 80/60 couple gives savings credit £971.40, Guarantee Credit £123 and Pension Credit £1,094.40.
  2. The docstring. savings_credit.py:11-16 says "zero unless" the unit is eligible, but an input value on an ineligible unit is returned unchanged (test_savings_credit_eligibility.py:144-148). Say "the computed amount".
  3. The reference. savings_credit.py:10 cites only s.3. The gate rests on s.1(2)–(4), s.4(1A) and SI 2019/37 art 4.
  4. Invariants and property tests (Hypothesis), each over random mixed-age, protected and legacy-closure families:
    • a computed savings_credit > 0 implies is_pension_credit_eligible;
    • pension_credit_entitlement == guarantee_credit + savings_credit for units without inputs;
    • gated and ungated runs give identical consumer outputs.

The impact (none) and the axiom: line are now in the body.

🤖 Generated with Claude Code

Merged via the queue into main with commit a34642b Oct 19, 2026
16 checks passed
@juaristi22
juaristi22 deleted the fix-2121-savings-credit-eligibility branch October 19, 2026 22:45
@MaxGhenis

Copy link
Copy Markdown
Collaborator

UK hub audit (session local_8e19f9d5, Opus 5.5), for Max's d1728 (2026-10-19), under which the hub merges #2458, #2497 and #2147 on its gates, after correcting each body's axiom: line to a value the parity rule accepts. The author (@juaristi22) had already enqueued it at 16:41:48Z on 2026-10-19, on head 287cd8b011200c247fa2bc5aaeb4ecafd8aea074, which is the head the reviews cover. The hub verified the gates, fixed the axiom: line, added the impact and did not enqueue again.

🤖 Generated with Claude Code

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.

savings_credit has no Pension Credit eligibility condition (guarantee_credit fixed in #1896)

3 participants