Skip to content

Place the Universal Credit and Pension Credit qualifying young person terminal date from the date of birth - #2300

Draft
MaxGhenis wants to merge 14 commits into
mainfrom
qyp-terminal-date-19
Draft

MaxGhenis wants to merge 14 commits into
mainfrom
qyp-terminal-date-19

Conversation

@MaxGhenis

@MaxGhenis MaxGhenis commented Oct 11, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #2291

Stacked on #2279. Do not merge before it. This PR builds on #2279's Pension Credit qualifying young person; #2279 waits on d1345. Until #2279 merges, the diff here also shows its commits. This PR's own change, against main with #2279 merged:

git diff $(git merge-tree --write-tree origin/main origin/uc-pc-qyp-sixteen-route) origin/qyp-terminal-date-19

Summary

A young person in non-advanced education or approved training is a qualifying young person (QYP) "up to, but not including, the 1st September following their 19th birthday". The model treated a 19-year-old three ways:

Where 19-year-old at school, course begun before 19
Universal Credit, household calculation not a QYP: the terminal-date input defaulted to false
Universal Credit, microdata a QYP: policyengine-uk-data sets that input true for every 19-year-old in qualifying education or training
Pension Credit, everywhere a QYP: no terminal-date condition at all

This PR places the date from the date of birth for both programmes. Against its base (main 4d80b29 with #2279 merged), 2026-27, a lone parent aged 45 with a 19-year-old at school (scratch/probe_D_base_vs_branch.txt; the same on main 9475c95 before #2285):

Variable Base This PR
is_qualifying_young_person_for_universal_credit (the 19-year-old) false true
uc_individual_child_element £0 £4,222.56 (£351.88 × 12)
is_uc_work_allowance_eligible false true
is_qualifying_young_person_for_pension_credit true true

This is a follow-up from #2279 (its items "Pension Credit reg. 4A(1)(b)'s 1 September" and the Universal Credit default).

The law

  • UC Regs 2013 reg. 5(1): "A person who has reached the age of 16 but not the age of 20 is a qualifying young person for the purposes of Part 1 of the Act and these Regulations— (a) up to, but not including, the 1st September following their 16th birthday; and (b) up to, but not including, the 1st September following their 19th birthday, if they are enrolled on, or accepted for, approved training or a course of education [...]".
  • SPC Regs 2002 reg. 4A(1)(b): "up to, but not including, the 1st September following the person's 19th birthday, if the person is enrolled on, or accepted for, approved training or a course of education [...]".

Change

  • New date fact is_before_first_september_following_19th_birthday (Person, year): true when the fiscal year starts (6 April) before the 1 September following the 19th birthday. It reads date_of_birth, or age and months_since_last_birthday, through utils.dates.birth_day, and mirrors Count 16-year-olds outside education as UC and Pension Credit qualifying young persons until the 1 September after their 16th birthday #2279's 16th-birthday fact.
  • Universal Credit. is_before_universal_credit_qualifying_young_person_terminal_date was an input with default_value = False. It now has a formula that returns the date fact. A dataset or situation can still set it.
  • Defaults. The date fact, the Universal Credit variable and the Pension Credit condition all default to true (see "Setting the date for one person" below).
  • Pension Credit. New meets_qualifying_young_person_terminal_date_condition_for_pension_credit (under 19, or the date fact), required by the education route in is_qualifying_young_person_for_pension_credit, with a new parameter terminal_date_age_limit (19).

The annual approximation

The period ends on a fixed 1 September that an annual period cannot split. As in #2279, a year counts if any of it falls in the period, which is the same as the year starting before that 1 September.

  • Every 19-year-old (age on 6 October) counts. Their 19th birthday falls between 7 October of the year before and 6 October, so the following 1 September is in the year or the next, always after 6 April.
  • A 20-year-old born from 1 September to 6 October is before the date until 31 August, but the QYP age limit, read on 6 October, already excludes them. The fact is true for them and changes nothing (YAML Case 7).
  • Nobody is newly excluded. The fact is false only for people aged 20 or over on 6 October.

So the default matches what the microdata and Pension Credit did already. Set either variable to false for a 19-year-old whose 1 September has passed.

Setting the date for one person

The review of this PR found that setting the date fact for one person gave everyone else in the situation false: an input set for some people fills the others with the variable's default and skips its formula for them. A second 19-year-old student then silently lost both statuses.

The fact's formula gives true for everyone under 20 on 6 October, which is everyone the age limit lets be a QYP. So each of the three variables now defaults to true. Setting one for some people leaves everyone else under 20 where the formula puts them. For someone older the default is true where the formula says false, but every reader of these variables is behind the QYP age limit, which excludes them first. Property test: the formula is true for every age 0 to 19 and every month since the last birthday, in three years, and each variable's default is true; and in a household of two 19-year-old students with each variable set false for the first only, the second stays a QYP for both programmes. YAML Case 12 pins the same household.

Two YAML cases changed with it. Case 3's parent, not set, now shows the default (true) for the Universal Credit variable; nothing reads it for a 45-year-old. And child_qualifying_young_person.yaml's "aged 19 requires being before terminal date" case left its after-the-date student unset and relied on the old false default; it now sets that input to false explicitly. #2279 documents the same partial-input behaviour for its 16th-birthday fact, whose formula is not true for everyone it could default for.

Invariants

  1. The date fact is true exactly when the year starts before the 1 September following the 19th birthday, for every date of birth, against calendar arithmetic that shares no code with the model.
  2. Universal Credit's terminal-date variable follows the fact unless set, and setting it changes Universal Credit only.
  3. Over every combination of age, education, training, apprenticeship, own-right benefits, entry age and the two date facts, Universal Credit and Pension Credit agree, and both equal the regulation's conditions.
  4. With the 19th-birthday fact on, both statuses are the earlier definition without a terminal date; switching it on never removes the status; it matters only for a 19-year-old in education or training.
  5. The formula is true for everyone under 20 on 6 October, and true is each terminal-date variable's default, so setting one for some people leaves everyone else's status as it would be with nothing set.

Tests

  • Properties, new test_qualifying_young_person_terminal_date_properties.py:
    • invariants 1 and 2 for every date of birth from 2004 to 2009 (2,192 people, ages 16 to 22 on 6 October 2026), with the 31 August and 1 September edges asserted, and again on Hypothesis-drawn dates of birth and years;
    • invariants 3 and 4 over 6,144 combinations;
    • invariant 2 for each programme's input set against the fact;
    • invariant 5: every age 0 to 25 and month since the last birthday, in 2022, 2026 and 2030, and the mixed household.
  • YAML, new qualifying_young_person_terminal_date.yaml, twelve cases hand-computed (2026-27):
Case Facts Expected
1 lone parent 45, 19-year-old at school UC and PC QYP; child element £351.88 × 12 = £4,222.56; work allowance
2 as 1, UC input false UC not a QYP, element £0; PC still a QYP
3 as 1, date fact false neither
4 pensioner 70, 19-year-old at school PC first-child addition £81.07 × 52 = £4,215.64
5 as 4, date fact false addition £0
6 born 31 August 2007 the period ends on the 19th birthday, in the year: QYP
7 born 15 September 2006, aged 20 on 6 October fact true; not a QYP (age)
8 born 31 August 2006 the date was in 2025: fact false
9 19, course begun at 19 not a QYP (reg. 5(2); reg. 4A(2))
10 19, outside education not a QYP
11 born 1 September 2006: the 1 September following the 19th birthday is in 2026 fact true, but aged 20 on 6 October: not a QYP
12 pensioner 70, two 19-year-olds at school, the fact set false for the first only the second takes the default (true): a QYP for both programmes; the first for neither

Mutation checks, each applied alone in a copy of the branch (YAML cases and property tests that fail):

Mutation YAML cases Property tests
The year compared with > and not >= 1, 2, 4, 6, 7, 10 both calendar tests
A 1 September birthday read as its own "1 September following" 11 the every-date-of-birth test
The 18th birthday in place of the 19th 1, 2, 4, 6, 7, 10 both calendar tests
Pension Credit's terminal condition dropped 3, 5 the agreement grid; the input-override test
The Universal Credit variable back to a false default 1, 6 both calendar tests; the agreement grid

The 1 September mutation was first caught by the property test alone; Case 11 was added to pin it in YAML.

Run locally at b5cbd2e:

Round 2 mutations at b5cbd2e (mutation/prD_r2.txt): each default put back to false fails the two new property tests; the date fact's also fails YAML Cases 3 and 12.

The full suites run in CI.

Microsimulation impact

Real runs on Enhanced FRS 2024-25 (uk-data 1.58.0, sha256 5ba399ea5ab9a6c7), one run each for 2025-26 to 2030-31, exact comparison of every record. Base: main e01f2f5 with #2279 (9d2fadb) merged (local merge 006c0e813). Branch: b5cbd2e. Outputs: impact/prD_r2/.

No record changes in any year, in any of the Universal Credit, Pension Credit, Housing Benefit, benefit cap, net income or poverty outputs. The reason is the data: policyengine-uk-data supplies is_before_universal_credit_qualifying_young_person_terminal_date, true for the 102 records aged 19 in qualifying education in 2026-27, and a supplied input takes precedence over the new formula (impact/prD_r1/context.txt, on the earlier base). The new date fact is true for all 780 records aged 19 that year, so the Pension Credit condition removes no one either. The PR changes household calculations (the table at the top) and any dataset that does not supply the input.

Reproducer: policyengine_uk/tests/test_qualifying_young_person_terminal_date_properties.py::test_universal_credit_and_pension_credit_agree_on_every_combination

axiom: UC reg 5(1)(b) is encoded at uk:regulations/uksi/2013/376/5#continued_education_or_training_qualifying_condition, but no companion case sets the 19th-birthday input false; the cases are queued on TheAxiomFoundation/rulespec-uk#355 (addendum; test-only, waiting on TheAxiomFoundation/rulespec-uk#446, which has the file open). SPC reg 4A(1)(b) TheAxiomFoundation/rulespec-uk#360 queued (addendum; reg 4A's row is in an unreleased ingest and a billed encoder run needs approval)

🤖 Generated with Claude Code

MaxGhenis and others added 7 commits October 10, 2026 18:09
…ducation condition

UC Regs 2013 reg. 5(1)(a) and SPC Regs 2002 reg. 4A(1)(a) make a person who
has reached 16 a qualifying young person up to the 1 September following
their 16th birthday, whatever their education. The model encoded only the
education route, so a 16-year-old outside education was not a UC or PC
qualifying young person.

is_before_first_september_following_16th_birthday places that date from the
date of birth and counts the route for a year that starts (6 April) before
it: every annual age of 16, and a 17-year-old born from 1 September to 6
October. Both QYP variables OR it with the education route, under the same
age band and own-right benefits exclusion. Tests: YAML cases hand-computed
from the regulations (flags, date edges, UC child element and work
allowance, PC child addition), the property-test oracle, a calendar
differential over every birth date in 2008-2011 and across years, and a
grid test of sufficiency, monotonicity, exclusion and UC/PC agreement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
UC Regs 2013 reg. 5(1)(b) and SPC Regs 2002 reg. 4A(1)(b) keep a young
person in education or training a qualifying young person up to, but not
including, the 1 September following their 19th birthday.

- Universal Credit read the date from an input that defaulted to false, so
  a 19-year-old at school was not a qualifying young person in a household
  calculation, while the microdata sets the input true for every
  19-year-old in education.
- Pension Credit did not model the date at all.

Add is_before_first_september_following_19th_birthday, which counts a year
that starts before the date, as the 16th-birthday fact does.
is_before_universal_credit_qualifying_young_person_terminal_date now
follows it unless set, and Pension Credit's new terminal date condition
reads it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Two property tests build a member who is outside every programme's family
as a 16 to 19 year old not in education. At 16 that member is now a UC
qualifying young person under reg 5(1)(a), so the family's UC gains a child
element and the tests' premise no longer holds. Their generators now set
is_before_first_september_following_16th_birthday false for that member.
The test functions are unchanged, so Hypothesis draws the same examples.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…p-terminal-date-19

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A mutation that read a 19th birthday on 1 September as its own
"1 September following" was caught by the calendar property test only.
Case 11 pins the edge in YAML too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in main with #2285 and #2288 merged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis and others added 7 commits October 11, 2026 08:53
…date-of-birth tests

- The route variable's docs now warn that setting it for some people gives
  everyone else false, and point to date_of_birth for placing one person.
  They also state that the rule departs from the 6 October reading that
  is_SP_age uses.
- YAML: a 2025-26 case where the 16-year-old ranks first under the
  two-child limit and the youngest child loses the element (GBP 7,581.72),
  and a case mixing a person with date_of_birth and one without (both in
  the route). The second kills a mutant that reads the raw date_of_birth
  input instead of birth_day.
- means_test_income_members.yaml: the 17-year-old case's name says why they
  are outside the route.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r 20

Review round 1 of #2300: setting the 19th-birthday fact for one person
gave everyone else in the situation false (the default), so another
19-year-old student silently lost the Pension Credit and Universal Credit
status. The formula gives true for everyone under 20 on 6 October, which
is everyone the age limit lets be a qualifying young person, so true is
now the default for the fact and for the two terminal-date variables that
read it. A partial setting then leaves everyone else where the formula
puts them. Tested from age alone for ages 0 to 25 and all months, and in a
mixed household (property test and YAML Case 12).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Case 3's parent, not set, now takes the default (true): harmless, since
every reader is behind the qualifying young person age limit. The older
child_qualifying_young_person.yaml case left its after-terminal-date
person unset and relied on the old false default; it now sets the input
explicitly. The docstrings now say the default is the formula's value
under 20 and that older people are excluded before it is read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in #2279's head 9d2fadb and main e01f2f5 with it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Brings in main e01f2f5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
MaxGhenis added a commit that referenced this pull request Oct 12, 2026
Brings in #2300's round 1 fixes and main e01f2f5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch has not been deployed

No deployments
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.

The 1 September after a qualifying young person's 19th birthday: false by default for Universal Credit, absent for Pension Credit

1 participant