Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tax free childcare #1004

Open
wants to merge 39 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
adf96a0
Implement tax free childcare
vahid-ahmadi Jan 10, 2025
ee24d61
Parameterize values
vahid-ahmadi Jan 13, 2025
12d24be
Reformatting to pass the tests
vahid-ahmadi Jan 13, 2025
235fd08
Reformatting with black
vahid-ahmadi Jan 13, 2025
b3b1967
Reformatting with black
vahid-ahmadi Jan 13, 2025
55ce6c3
Format code with black
vahid-ahmadi Jan 13, 2025
9952ed9
Format code with black using 79-char line length
vahid-ahmadi Jan 13, 2025
d6b22e5
Fix YAML structure
vahid-ahmadi Jan 13, 2025
b06a47c
Fix age condition YAML file
vahid-ahmadi Jan 13, 2025
90eb823
Fix age condition YAML file
vahid-ahmadi Jan 13, 2025
0c871a5
Fix other YAML files
vahid-ahmadi Jan 13, 2025
b28b5f8
Edit parameterization
vahid-ahmadi Jan 13, 2025
ff1f456
Fix formatting
vahid-ahmadi Jan 13, 2025
d3ad685
Edit age in YAML files
vahid-ahmadi Jan 13, 2025
5c6d359
Replace if with where
vahid-ahmadi Jan 13, 2025
e0221f8
Format with black
vahid-ahmadi Jan 13, 2025
fc83a03
Edit for test passing
vahid-ahmadi Jan 13, 2025
4421154
Edit format with black
vahid-ahmadi Jan 13, 2025
b4e8d1a
Edit format of benefit py file
vahid-ahmadi Jan 13, 2025
cf7f6e6
Edit child as not being boolean
vahid-ahmadi Jan 13, 2025
d4ff3b2
Change if to where
vahid-ahmadi Jan 14, 2025
0058402
Change disability definition
vahid-ahmadi Jan 14, 2025
37c7228
Solve .values problem
vahid-ahmadi Jan 14, 2025
af96f3a
Edit .values error
vahid-ahmadi Jan 14, 2025
e96bc2e
Edit vectorization problem
vahid-ahmadi Jan 14, 2025
153c2f6
Change folder from hmrc to dwp
vahid-ahmadi Jan 14, 2025
c9fbd0c
Use is_disabled_for_benefits variable
vahid-ahmadi Jan 14, 2025
a2f73d2
Add .astype(bool) to fix error
vahid-ahmadi Jan 14, 2025
a061877
Fix bitwise AND operation error
vahid-ahmadi Jan 14, 2025
a9ca8a7
Add .astype(bool) to conditions
vahid-ahmadi Jan 14, 2025
c7c6762
Add unit tests
vahid-ahmadi Jan 14, 2025
59e1b78
Reformat with black
vahid-ahmadi Jan 14, 2025
cebaace
Fix the folders
vahid-ahmadi Jan 15, 2025
c1a5cf0
Fix the folders
vahid-ahmadi Jan 15, 2025
49ec556
Merge branch 'tax-free-childcare-new' of https://github.com/policyeng…
vahid-ahmadi Jan 15, 2025
4a5cad5
Implement free childcare program
vahid-ahmadi Jan 15, 2025
4432db2
Format with black
vahid-ahmadi Jan 15, 2025
c7c1998
Change tests
vahid-ahmadi Jan 15, 2025
789f676
Edit format based on Pavel's comments
vahid-ahmadi Jan 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Copy link
Collaborator

Choose a reason for hiding this comment

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

lets split this file into to separate parameters:
make an age_limit folder
standard.yaml : value: 12
disability.yaml : value: 17
image

Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
description: Age thresholds for tax-free childcare eligibility, including standard and disability-related limits.
metadata:
unit: years
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
unit: years
unit: year

name: childcare_age_limits
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
name: childcare_age_limits

label: Age limits for tax-free childcare eligibility
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
label: Age limits for tax-free childcare eligibility
label: Age limits for tax-free childcare eligibility
period: year

reference:
- title: Childcare Act
href: https://www.legislation.gov.uk/ukdsi/2015/9780111127063
- title: Tax-Free Childcare Guidance
href: https://www.gov.uk/tax-free-childcare?step-by-step-nav=d78aeaf6-1747-4d72-9619-f16efb4dd89d
values:
2025-01-01:
- standard_age_limit: 12
- disability_age_limit: 17
Copy link
Collaborator

Choose a reason for hiding this comment

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

split this into three files:
folder: contributions

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
description: Parameters for tax-free childcare government contribution calculations.
metadata:
unit: currency-GBP
name: contribution_parameters
label: Tax-free childcare contribution parameters
reference:
- title: Childcare Payments Act
href: https://www.legislation.gov.uk/ukdsi/2015/9780111127063
- title: Tax-Free Childcare Guidance
href: https://www.gov.uk/tax-free-childcare?step-by-step-nav=d78aeaf6-1747-4d72-9619-f16efb4dd89d

standard_child:
yearly_max:
values:
2025-01-01: 2000
metadata:
unit: currency-GBP

disabled_child:
yearly_max:
values:
2025-01-01: 4000
metadata:
unit: currency-GBP

government_contribution_ratio:
values:
2025-01-01: 0.25 # 2/8 expressed as decimal
metadata:
unit: /1
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
description: Income thresholds for tax-free childcare eligibility by age group.
metadata:
unit: currency-GBP
name: income_thresholds
label: Income thresholds for tax-free childcare eligibility
reference:
- title: Childcare Payments Act 2014
href: https://www.legislation.gov.uk/ukdsi/2015/9780111127063
- title: Tax-Free Childcare Guidance
href: https://www.gov.uk/tax-free-childcare?step-by-step-nav=d78aeaf6-1747-4d72-9619-f16efb4dd89d

adult:
quarterly_income:
values:
2025-01-01: 2379
metadata:
unit: currency-GBP
min_age:
values:
2025-01-01: 21
metadata:
unit: years

young_adult:
quarterly_income:
values:
2025-01-01: 1788
metadata:
unit: currency-GBP
min_age:
values:
2025-01-01: 18
metadata:
unit: years
max_age:
values:
2025-01-01: 20
metadata:
unit: years

youth:
quarterly_income:
values:
2025-01-01: 1331
metadata:
unit: currency-GBP
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from policyengine_uk.model_api import *


class child_age_eligible(Variable):
value_type = bool
entity = Person
label = "Child age eligibility requirements"
documentation = "Whether this person meets the age and disability requirements for eligibility"
definition_period = YEAR

def formula(person, period, parameters):
"""
Calculate age eligibility based on age and disability conditions.

Returns:
bool: True if eligible (under standard age limit, or under disability age limit with disability), False otherwise
"""
# Get person's characteristics
age = person("age", period)

# Get age thresholds from parameters
age_limits = parameters(
period
).gov.hmrc.childcare_subsidies.tax_free_childcare.age_limits
standard_age_limit = age_limits.standard_age_limit
disability_age_limit = age_limits.disability_age_limit

# Check disability conditions
gc = parameters(period).gov.dwp.pension_credit.guarantee_credit
standard_disability_benefits = gc.child.disability.eligibility
severe_disability_benefits = gc.child.disability.severe.eligibility
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
gc = parameters(period).gov.dwp.pension_credit.guarantee_credit
standard_disability_benefits = gc.child.disability.eligibility
severe_disability_benefits = gc.child.disability.severe.eligibility
gc = parameters(period).gov.dwp.pension_credit.guarantee_credit.child.disability
standard_disability_benefits = gc.eligibility
severe_disability_benefits = gc.severe.eligibility


is_disabled = (
add(person, period, standard_disability_benefits)
| add(person, period, severe_disability_benefits)
) > 0

# Check age conditions using parameterized values
basic_age_condition = age < standard_age_limit
age_under_disability_limit = age < disability_age_limit

# Combine conditions
eligible = basic_age_condition | (
age_under_disability_limit & is_disabled
)

return eligible
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
eligible = basic_age_condition | (
age_under_disability_limit & is_disabled
)
return eligible
return basic_age_condition | (
age_under_disability_limit & is_disabled
)

Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from policyengine_uk.model_api import *


class meets_income_requirements(Variable):
value_type = bool
entity = Person
label = "Income requirements and calculations"
documentation = "Whether this person meets the income requirements for tax-free childcare based on age and income thresholds"
definition_period = YEAR

def formula(person, period, parameters):
"""
Calculate if a person meets income requirements based on their age and income.

Returns:
bool: True if they meet the income conditions for their age group
"""
# Get person's characteristics
age = person("age", period)

# Calculate eligible income
total_income = person("total_income", period)
# Extract investment incomes to subtract
investment_income = add(
person,
period,
[
"private_pension_income",
"savings_interest_income",
"dividend_income",
"property_income",
],
)

yearly_eligible_income = total_income - investment_income
Copy link
Collaborator

Choose a reason for hiding this comment

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

Suggested change
yearly_eligible_income = total_income - investment_income
yearly_eligible_income = max_(total_income - investment_income, 0)

If the value cant be negative


# Get income thresholds from parameters
income_limits = parameters(
period
).gov.hmrc.childcare_subsidies.tax_free_childcare.income_thresholds
quarterly_income = yearly_eligible_income / 4

# Age >= 21
meets_adult_condition = (age >= income_limits.adult.min_age) & (
quarterly_income >= income_limits.adult.quarterly_income
)

# Age 18-20
meets_young_adult_condition = (
(age >= income_limits.young_adult.min_age)
& (age <= income_limits.young_adult.max_age)
& (quarterly_income >= income_limits.young_adult.quarterly_income)
)

# Age < 18
meets_youth_condition = (age < income_limits.young_adult.min_age) & (
quarterly_income >= income_limits.youth.quarterly_income
)

# Combine all conditions
return (
meets_adult_condition
| meets_young_adult_condition
| meets_youth_condition
)
Copy link
Collaborator

Choose a reason for hiding this comment

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

We then want to create unit tests to check the formulas

Example test:
in a new incompatibilities_childcare_eligible.yaml file:

- name: Ineligible due to all credits being 0
  period: 2025
  input:
    working_tax_credit: 0
    child_tax_credit: 0
    universal_credit: 0
  output:
    incompatibilities_childcare_eligible: false

- name: Eligible due to universal credit 
  period: 2025
  input:
    working_tax_credit: 0
    child_tax_credit: 0
    universal_credit: 1
  output:
    incompatibilities_childcare_eligible: true

Copy link
Collaborator

Choose a reason for hiding this comment

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

To run:
policyengine-core test ./policyengine_uk/tests/policy/baseline.....

Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from policyengine_uk.model_api import *


class incompatibilities_childcare_eligible(Variable):
value_type = bool
entity = Person
label = "Tax-Free Childcare Exclusions"
documentation = "Whether the person's benefit unit meets the incompatibility conditions for tax-free childcare (not receiving WTC, CTC, or UC)"
definition_period = YEAR

def formula(person, period, parameters):
"""
Calculate eligibility based on incompatible benefits.

Returns:
bool: True if eligible (no incompatible benefits received), False if receiving any incompatible benefits
"""
# Get the benefit unit the person belongs to
benunit = person.benunit

# Check if receiving any of the mutually exclusive benefits
has_wtc = benunit("working_tax_credit", period) > 0
has_ctc = benunit("child_tax_credit", period) > 0
has_uc = benunit("universal_credit", period) > 0

# Returns True when person's benefit unit does NOT receive any of these benefits
return ~(has_wtc | has_ctc | has_uc)
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
from policyengine_uk.model_api import *


class childcare_work_condition(Variable):
value_type = bool
entity = Person
Copy link
Collaborator

Choose a reason for hiding this comment

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

Similar question for each of the files - do we want to compute this for each person or for the entire Benefit Unit?

label = "Work conditions for tax-free childcare"
documentation = "Whether the person/couple meets work requirements for tax-free childcare"
definition_period = YEAR

def formula(person, period, parameters):
"""Calculate if person meets work conditions for:
- Single working adult
- Couple where either both work or one works and other has disability/incapacity
"""
benunit = person.benunit
is_adult = person("is_adult", period)

# Basic work status
in_work = person("in_work", period)

# Get disability/incapacity conditions like we did in childcare age eligibility
gc = parameters(period).gov.dwp.pension_credit.guarantee_credit
standard_disability_benefits = gc.child.disability.eligibility
severe_disability_benefits = gc.child.disability.severe.eligibility

is_disabled = (
add(person, period, standard_disability_benefits)
| add(person, period, severe_disability_benefits)
) > 0

has_incapacity = person("incapacity_benefit", period) > 0

# Build conditions
# Single adult conditions
is_single = benunit.sum(is_adult) == 1
single_working = is_single & in_work

# Couple conditions
is_couple = benunit.sum(is_adult) == 2
partner_in_work = in_work
partner_has_condition = is_disabled | has_incapacity

couple_both_working = is_couple & in_work & partner_in_work
is_partner_working_with_disabled_person = (
is_couple & partner_in_work & (is_disabled | has_incapacity)
)
is_person_working_with_disabled_partner = (
is_couple & in_work & partner_has_condition
)

return (
single_working
| couple_both_working
| is_person_working_with_disabled_partner
| is_partner_working_with_disabled_person
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
from policyengine_uk.model_api import *


class tax_free_childcare(Variable):
value_type = float
entity = BenUnit
label = "Tax-free childcare government contribution"
documentation = "The amount of government contribution provided through the tax-free childcare scheme"
definition_period = YEAR
unit = GBP

def formula(benunit, period, parameters):
"""
Calculate the government contribution for tax-free childcare.

Args:
benunit: The benefit unit
period: The time period
parameters: Policy parameters

Returns:
float: The calculated government contribution
"""
# Get parents contribution
parents_contribution = benunit("childcare_cost", period)

# Get parameters from the parameter tree
p = parameters(
period
).gov.hmrc.childcare_subsidies.tax_free_childcare.contribution_parameters

# Check eligibility conditions
meets_age_condition = benunit("child_age_eligible", period)
meets_income_condition = benunit.any(
benunit.members("meets_income_requirements", period)
)
is_eligible = (
meets_age_condition
& meets_income_condition
& benunit("incompatibilities_childcare_eligible", period)
)

# Determine the maximum eligible childcare cost for a single child
max_amount = 0
for child in benunit.members("is_child", period):
if is_eligible[child]:
if child("is_disabled", period):
max_amount = p.contribution_rates.disabled_child.yearly_max
else:
max_amount = p.contribution_rates.standard_child.yearly_max

# Calculate the government contribution
government_contribution = min(
parents_contribution
* p.contribution_rates.government_contribution_ratio,
max_amount,
)

return where(is_eligible, government_contribution, 0)
Loading