From 61e4a158fe8b60cc9cd97a16896a171677f6acf2 Mon Sep 17 00:00:00 2001 From: Daphne Hansell <128793799+daphnehanse11@users.noreply.github.com> Date: Thu, 30 Apr 2026 15:53:40 -0400 Subject: [PATCH 1/4] Use actual bronze premium for selected marketplace plans --- changelog.d/changed/8211.md | 1 + .../gov/aca/ptc/marketplace_net_premium.yaml | 14 +++++++++++ ...lected_marketplace_plan_premium_proxy.yaml | 23 +++++++++++++++++++ .../baseline/gov/aca/ptc/used_aca_ptc.yaml | 20 +++++++++++++++- .../ptc/selected_marketplace_plan_category.py | 15 ++++++++++++ ...selected_marketplace_plan_premium_proxy.py | 17 ++++++++++++-- 6 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 changelog.d/changed/8211.md create mode 100644 policyengine_us/variables/gov/aca/ptc/selected_marketplace_plan_category.py diff --git a/changelog.d/changed/8211.md b/changelog.d/changed/8211.md new file mode 100644 index 00000000000..ed1c99d0cef --- /dev/null +++ b/changelog.d/changed/8211.md @@ -0,0 +1 @@ +Use actual lowest-cost bronze premiums when Bronze is the selected Marketplace plan category. diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/marketplace_net_premium.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/marketplace_net_premium.yaml index 21cdf7daa90..ddf141f5162 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/ptc/marketplace_net_premium.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/marketplace_net_premium.yaml @@ -41,3 +41,17 @@ selected_marketplace_plan_premium_proxy: 8_000 used_aca_ptc: 0 marketplace_net_premium: 8_000 + +- name: Bronze selected plan net premium uses actual bronze premium. + absolute_error_margin: 0.01 + period: 2026 + input: + pays_aca_premium: true + selected_marketplace_plan_category: BRONZE + slcsp: 10_000 + lcbp: 5_000 + aca_ptc: 3_000 + output: + selected_marketplace_plan_premium_proxy: 5_000 + used_aca_ptc: 3_000 + marketplace_net_premium: 2_000 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/selected_marketplace_plan_premium_proxy.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/selected_marketplace_plan_premium_proxy.yaml index 039ca1137b2..24ef11f2628 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/ptc/selected_marketplace_plan_premium_proxy.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/selected_marketplace_plan_premium_proxy.yaml @@ -63,3 +63,26 @@ pays_aca_premium: true is_aca_ptc_eligible: false selected_marketplace_plan_premium_proxy: 8_000 + +- name: Case 6, bronze selected plan uses the lowest-cost bronze premium. + absolute_error_margin: 0.01 + period: 2026 + input: + pays_aca_premium: true + selected_marketplace_plan_category: BRONZE + slcsp: 10_000 + lcbp: 6_500 + output: + selected_marketplace_plan_premium_proxy: 6_500 + +- name: Case 7, silver selected plan keeps the benchmark-ratio proxy. + absolute_error_margin: 0.01 + period: 2026 + input: + pays_aca_premium: true + selected_marketplace_plan_category: SILVER + slcsp: 10_000 + lcbp: 6_500 + selected_marketplace_plan_benchmark_ratio: 0.9 + output: + selected_marketplace_plan_premium_proxy: 9_000 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/ptc/used_aca_ptc.yaml b/policyengine_us/tests/policy/baseline/gov/aca/ptc/used_aca_ptc.yaml index 3c04f3d7eff..c1f36cf129c 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/ptc/used_aca_ptc.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/ptc/used_aca_ptc.yaml @@ -13,7 +13,7 @@ used_aca_ptc: 200 unused_aca_ptc: 0 -- name: Case 2, a bronze-like ratio leaves some ACA PTC unused. +- name: Case 2, a lower-than-benchmark selected-plan ratio leaves some ACA PTC unused. absolute_error_margin: 0.01 period: 2025 input: @@ -56,3 +56,21 @@ selected_marketplace_plan_premium_proxy: 0 used_aca_ptc: 0 unused_aca_ptc: 200 + +- name: Case 5, bronze selected plan uses actual bronze premium before applying PTC. + absolute_error_margin: 0.01 + period: 2026 + input: + is_aca_ptc_eligible: true + pays_aca_premium: true + selected_marketplace_plan_category: BRONZE + slcsp: 10_000 + lcbp: 7_000 + aca_magi: 20_000 + aca_required_contribution_percentage: 0.04 + tax_unit_is_filer: true + output: + aca_ptc: 9_200 + selected_marketplace_plan_premium_proxy: 7_000 + used_aca_ptc: 7_000 + unused_aca_ptc: 2_200 diff --git a/policyengine_us/variables/gov/aca/ptc/selected_marketplace_plan_category.py b/policyengine_us/variables/gov/aca/ptc/selected_marketplace_plan_category.py new file mode 100644 index 00000000000..a1d6ae04ed5 --- /dev/null +++ b/policyengine_us/variables/gov/aca/ptc/selected_marketplace_plan_category.py @@ -0,0 +1,15 @@ +from policyengine_us.model_api import * + + +class MarketplacePlanCategory(Enum): + BRONZE = "Bronze" + SILVER = "Silver" + + +class selected_marketplace_plan_category(Variable): + value_type = Enum + possible_values = MarketplacePlanCategory + default_value = MarketplacePlanCategory.SILVER + entity = TaxUnit + label = "Selected Marketplace plan category" + definition_period = YEAR diff --git a/policyengine_us/variables/gov/aca/ptc/selected_marketplace_plan_premium_proxy.py b/policyengine_us/variables/gov/aca/ptc/selected_marketplace_plan_premium_proxy.py index 604dc448498..873517e42df 100644 --- a/policyengine_us/variables/gov/aca/ptc/selected_marketplace_plan_premium_proxy.py +++ b/policyengine_us/variables/gov/aca/ptc/selected_marketplace_plan_premium_proxy.py @@ -1,4 +1,7 @@ from policyengine_us.model_api import * +from policyengine_us.variables.gov.aca.ptc.selected_marketplace_plan_category import ( + MarketplacePlanCategory, +) class selected_marketplace_plan_premium_proxy(Variable): @@ -12,9 +15,19 @@ def formula(tax_unit, period, parameters): takes_up_aca_if_eligible = tax_unit("takes_up_aca_if_eligible", period) person = tax_unit.members pays_marketplace_premium = tax_unit.sum(person("pays_aca_premium", period)) > 0 + selected_plan_category = tax_unit( + "selected_marketplace_plan_category", period + ) + silver_premium = tax_unit("slcsp", period) * tax_unit( + "selected_marketplace_plan_benchmark_ratio", period + ) + selected_plan_premium = where( + selected_plan_category == MarketplacePlanCategory.BRONZE, + tax_unit("lcbp", period), + silver_premium, + ) return where( takes_up_aca_if_eligible & pays_marketplace_premium, - tax_unit("slcsp", period) - * tax_unit("selected_marketplace_plan_benchmark_ratio", period), + selected_plan_premium, 0, ) From db709c2fe3564b0424f95b093c65c05efbee84d0 Mon Sep 17 00:00:00 2001 From: Daphne Hansell <128793799+daphnehanse11@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:04:48 -0400 Subject: [PATCH 2/4] Format selected marketplace plan premium proxy --- .../gov/aca/ptc/selected_marketplace_plan_premium_proxy.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/policyengine_us/variables/gov/aca/ptc/selected_marketplace_plan_premium_proxy.py b/policyengine_us/variables/gov/aca/ptc/selected_marketplace_plan_premium_proxy.py index 873517e42df..2772a7f3d33 100644 --- a/policyengine_us/variables/gov/aca/ptc/selected_marketplace_plan_premium_proxy.py +++ b/policyengine_us/variables/gov/aca/ptc/selected_marketplace_plan_premium_proxy.py @@ -15,9 +15,7 @@ def formula(tax_unit, period, parameters): takes_up_aca_if_eligible = tax_unit("takes_up_aca_if_eligible", period) person = tax_unit.members pays_marketplace_premium = tax_unit.sum(person("pays_aca_premium", period)) > 0 - selected_plan_category = tax_unit( - "selected_marketplace_plan_category", period - ) + selected_plan_category = tax_unit("selected_marketplace_plan_category", period) silver_premium = tax_unit("slcsp", period) * tax_unit( "selected_marketplace_plan_benchmark_ratio", period ) From 090045290305f1ff123bcef14a4ac86e92521751 Mon Sep 17 00:00:00 2001 From: Daphne Hansell <128793799+daphnehanse11@users.noreply.github.com> Date: Thu, 30 Apr 2026 16:20:46 -0400 Subject: [PATCH 3/4] Add ACA cost-sharing reduction actuarial values --- changelog.d/added/8188.md | 1 + changelog.d/changed/8211.md | 1 - .../gov/aca/csr/actuarial_value/highest.yaml | 10 ++++ .../gov/aca/csr/actuarial_value/lowest.yaml | 10 ++++ .../gov/aca/csr/actuarial_value/middle.yaml | 10 ++++ .../income_threshold/highest_av_maximum.yaml | 10 ++++ .../gov/aca/csr/income_threshold/maximum.yaml | 10 ++++ .../income_threshold/middle_av_maximum.yaml | 10 ++++ .../gov/aca/csr/income_threshold/minimum.yaml | 10 ++++ .../gov/aca/metal_actuarial_value/bronze.yaml | 10 ++++ .../gov/aca/metal_actuarial_value/silver.yaml | 10 ++++ .../gov/aca/csr/marketplace_csr_category.yaml | 53 +++++++++++++++++++ .../gov/aca/csr/marketplace_csr_eligible.yaml | 28 ++++++++++ ...marketplace_effective_actuarial_value.yaml | 14 +++++ .../csr/marketplace_csr_actuarial_value.py | 26 +++++++++ .../gov/aca/csr/marketplace_csr_category.py | 36 +++++++++++++ .../gov/aca/csr/marketplace_csr_eligible.py | 22 ++++++++ .../marketplace_effective_actuarial_value.py | 18 +++++++ ...lected_marketplace_plan_actuarial_value.py | 18 +++++++ 19 files changed, 306 insertions(+), 1 deletion(-) create mode 100644 changelog.d/added/8188.md delete mode 100644 changelog.d/changed/8211.md create mode 100644 policyengine_us/parameters/gov/aca/csr/actuarial_value/highest.yaml create mode 100644 policyengine_us/parameters/gov/aca/csr/actuarial_value/lowest.yaml create mode 100644 policyengine_us/parameters/gov/aca/csr/actuarial_value/middle.yaml create mode 100644 policyengine_us/parameters/gov/aca/csr/income_threshold/highest_av_maximum.yaml create mode 100644 policyengine_us/parameters/gov/aca/csr/income_threshold/maximum.yaml create mode 100644 policyengine_us/parameters/gov/aca/csr/income_threshold/middle_av_maximum.yaml create mode 100644 policyengine_us/parameters/gov/aca/csr/income_threshold/minimum.yaml create mode 100644 policyengine_us/parameters/gov/aca/metal_actuarial_value/bronze.yaml create mode 100644 policyengine_us/parameters/gov/aca/metal_actuarial_value/silver.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_csr_category.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_csr_eligible.yaml create mode 100644 policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_effective_actuarial_value.yaml create mode 100644 policyengine_us/variables/gov/aca/csr/marketplace_csr_actuarial_value.py create mode 100644 policyengine_us/variables/gov/aca/csr/marketplace_csr_category.py create mode 100644 policyengine_us/variables/gov/aca/csr/marketplace_csr_eligible.py create mode 100644 policyengine_us/variables/gov/aca/csr/marketplace_effective_actuarial_value.py create mode 100644 policyengine_us/variables/gov/aca/ptc/selected_marketplace_plan_actuarial_value.py diff --git a/changelog.d/added/8188.md b/changelog.d/added/8188.md new file mode 100644 index 00000000000..514eff1a274 --- /dev/null +++ b/changelog.d/added/8188.md @@ -0,0 +1 @@ +Add selected Marketplace plan categories and ACA cost-sharing reduction actuarial value variables. diff --git a/changelog.d/changed/8211.md b/changelog.d/changed/8211.md deleted file mode 100644 index ed1c99d0cef..00000000000 --- a/changelog.d/changed/8211.md +++ /dev/null @@ -1 +0,0 @@ -Use actual lowest-cost bronze premiums when Bronze is the selected Marketplace plan category. diff --git a/policyengine_us/parameters/gov/aca/csr/actuarial_value/highest.yaml b/policyengine_us/parameters/gov/aca/csr/actuarial_value/highest.yaml new file mode 100644 index 00000000000..7329bea0ba9 --- /dev/null +++ b/policyengine_us/parameters/gov/aca/csr/actuarial_value/highest.yaml @@ -0,0 +1,10 @@ +description: The Affordable Care Act sets this actuarial value for the highest ordinary silver cost-sharing reduction plan variation. +values: + 2014-01-01: 0.94 +metadata: + unit: /1 + period: year + label: ACA CSR highest silver plan actuarial value + reference: + - title: 45 CFR § 156.420(a)(1)(ii) - Plan variations + href: https://www.law.cornell.edu/cfr/text/45/156.420#a_1_ii diff --git a/policyengine_us/parameters/gov/aca/csr/actuarial_value/lowest.yaml b/policyengine_us/parameters/gov/aca/csr/actuarial_value/lowest.yaml new file mode 100644 index 00000000000..997f7478d6a --- /dev/null +++ b/policyengine_us/parameters/gov/aca/csr/actuarial_value/lowest.yaml @@ -0,0 +1,10 @@ +description: The Affordable Care Act sets this actuarial value for the lowest ordinary silver cost-sharing reduction plan variation. +values: + 2014-01-01: 0.73 +metadata: + unit: /1 + period: year + label: ACA CSR lowest silver plan actuarial value + reference: + - title: 45 CFR § 156.420(a)(3)(ii) - Plan variations + href: https://www.law.cornell.edu/cfr/text/45/156.420#a_3_ii diff --git a/policyengine_us/parameters/gov/aca/csr/actuarial_value/middle.yaml b/policyengine_us/parameters/gov/aca/csr/actuarial_value/middle.yaml new file mode 100644 index 00000000000..c62968148d8 --- /dev/null +++ b/policyengine_us/parameters/gov/aca/csr/actuarial_value/middle.yaml @@ -0,0 +1,10 @@ +description: The Affordable Care Act sets this actuarial value for the middle ordinary silver cost-sharing reduction plan variation. +values: + 2014-01-01: 0.87 +metadata: + unit: /1 + period: year + label: ACA CSR middle silver plan actuarial value + reference: + - title: 45 CFR § 156.420(a)(2)(ii) - Plan variations + href: https://www.law.cornell.edu/cfr/text/45/156.420#a_2_ii diff --git a/policyengine_us/parameters/gov/aca/csr/income_threshold/highest_av_maximum.yaml b/policyengine_us/parameters/gov/aca/csr/income_threshold/highest_av_maximum.yaml new file mode 100644 index 00000000000..9d333bcaf90 --- /dev/null +++ b/policyengine_us/parameters/gov/aca/csr/income_threshold/highest_av_maximum.yaml @@ -0,0 +1,10 @@ +description: The Affordable Care Act sets this maximum household income threshold for the highest ordinary cost-sharing reduction actuarial value. +values: + 2014-01-01: 1.5 +metadata: + unit: /1 + period: year + label: ACA CSR highest actuarial value income threshold + reference: + - title: 45 CFR § 155.305(g)(2)(i) - Eligibility standards + href: https://www.law.cornell.edu/cfr/text/45/155.305#g_2_i diff --git a/policyengine_us/parameters/gov/aca/csr/income_threshold/maximum.yaml b/policyengine_us/parameters/gov/aca/csr/income_threshold/maximum.yaml new file mode 100644 index 00000000000..026bd678627 --- /dev/null +++ b/policyengine_us/parameters/gov/aca/csr/income_threshold/maximum.yaml @@ -0,0 +1,10 @@ +description: The Affordable Care Act sets this maximum household income threshold for ordinary income-based cost-sharing reductions. +values: + 2014-01-01: 2.5 +metadata: + unit: /1 + period: year + label: ACA CSR maximum income threshold + reference: + - title: 45 CFR § 155.305(g)(1)(i)(C) - Eligibility standards + href: https://www.law.cornell.edu/cfr/text/45/155.305#g_1_i_C diff --git a/policyengine_us/parameters/gov/aca/csr/income_threshold/middle_av_maximum.yaml b/policyengine_us/parameters/gov/aca/csr/income_threshold/middle_av_maximum.yaml new file mode 100644 index 00000000000..0bc1d71bc17 --- /dev/null +++ b/policyengine_us/parameters/gov/aca/csr/income_threshold/middle_av_maximum.yaml @@ -0,0 +1,10 @@ +description: The Affordable Care Act sets this maximum household income threshold for the middle ordinary cost-sharing reduction actuarial value. +values: + 2014-01-01: 2 +metadata: + unit: /1 + period: year + label: ACA CSR middle actuarial value income threshold + reference: + - title: 45 CFR § 155.305(g)(2)(ii) - Eligibility standards + href: https://www.law.cornell.edu/cfr/text/45/155.305#g_2_ii diff --git a/policyengine_us/parameters/gov/aca/csr/income_threshold/minimum.yaml b/policyengine_us/parameters/gov/aca/csr/income_threshold/minimum.yaml new file mode 100644 index 00000000000..89c136cc607 --- /dev/null +++ b/policyengine_us/parameters/gov/aca/csr/income_threshold/minimum.yaml @@ -0,0 +1,10 @@ +description: The Affordable Care Act sets this minimum household income threshold for ordinary income-based cost-sharing reductions. +values: + 2014-01-01: 1 +metadata: + unit: /1 + period: year + label: ACA CSR minimum income threshold + reference: + - title: 45 CFR § 155.305(g)(2)(i) - Eligibility standards + href: https://www.law.cornell.edu/cfr/text/45/155.305#g_2_i diff --git a/policyengine_us/parameters/gov/aca/metal_actuarial_value/bronze.yaml b/policyengine_us/parameters/gov/aca/metal_actuarial_value/bronze.yaml new file mode 100644 index 00000000000..a66873519be --- /dev/null +++ b/policyengine_us/parameters/gov/aca/metal_actuarial_value/bronze.yaml @@ -0,0 +1,10 @@ +description: The Affordable Care Act sets this actuarial value for bronze Marketplace plans. +values: + 2014-01-01: 0.6 +metadata: + unit: /1 + period: year + label: ACA bronze plan actuarial value + reference: + - title: 45 CFR § 156.140(b)(1) - Levels of coverage + href: https://www.law.cornell.edu/cfr/text/45/156.140#b_1 diff --git a/policyengine_us/parameters/gov/aca/metal_actuarial_value/silver.yaml b/policyengine_us/parameters/gov/aca/metal_actuarial_value/silver.yaml new file mode 100644 index 00000000000..9a90e5f5c5c --- /dev/null +++ b/policyengine_us/parameters/gov/aca/metal_actuarial_value/silver.yaml @@ -0,0 +1,10 @@ +description: The Affordable Care Act sets this actuarial value for silver Marketplace plans. +values: + 2014-01-01: 0.7 +metadata: + unit: /1 + period: year + label: ACA silver plan actuarial value + reference: + - title: 45 CFR § 156.140(b)(2) - Levels of coverage + href: https://www.law.cornell.edu/cfr/text/45/156.140#b_2 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_csr_category.yaml b/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_csr_category.yaml new file mode 100644 index 00000000000..25888017bdd --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_csr_category.yaml @@ -0,0 +1,53 @@ +- name: Case 1, silver enrollee at 125 percent FPL receives 94 percent AV CSR. + absolute_error_margin: 0.001 + period: 2026 + input: + is_aca_ptc_eligible: true + aca_magi_fraction: 1.25 + selected_marketplace_plan_category: SILVER + output: + person_receives_aca: true + marketplace_csr_eligible: true + marketplace_csr_category: AV_94 + selected_marketplace_plan_actuarial_value: 0.7 + marketplace_csr_actuarial_value: 0.94 + marketplace_effective_actuarial_value: 0.94 + +- name: Case 2, silver enrollee at 175 percent FPL receives 87 percent AV CSR. + absolute_error_margin: 0.001 + period: 2026 + input: + is_aca_ptc_eligible: true + aca_magi_fraction: 1.75 + selected_marketplace_plan_category: SILVER + output: + marketplace_csr_eligible: true + marketplace_csr_category: AV_87 + marketplace_csr_actuarial_value: 0.87 + marketplace_effective_actuarial_value: 0.87 + +- name: Case 3, silver enrollee at 225 percent FPL receives 73 percent AV CSR. + absolute_error_margin: 0.001 + period: 2026 + input: + is_aca_ptc_eligible: true + aca_magi_fraction: 2.25 + selected_marketplace_plan_category: SILVER + output: + marketplace_csr_eligible: true + marketplace_csr_category: AV_73 + marketplace_csr_actuarial_value: 0.73 + marketplace_effective_actuarial_value: 0.73 + +- name: Case 4, silver enrollee at 275 percent FPL receives no CSR. + absolute_error_margin: 0.001 + period: 2026 + input: + is_aca_ptc_eligible: true + aca_magi_fraction: 2.75 + selected_marketplace_plan_category: SILVER + output: + marketplace_csr_eligible: false + marketplace_csr_category: NONE + marketplace_csr_actuarial_value: 0 + marketplace_effective_actuarial_value: 0.7 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_csr_eligible.yaml b/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_csr_eligible.yaml new file mode 100644 index 00000000000..6195cefd67e --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_csr_eligible.yaml @@ -0,0 +1,28 @@ +- name: Case 1, bronze enrollee is ineligible for ordinary CSR. + absolute_error_margin: 0.001 + period: 2026 + input: + is_aca_ptc_eligible: true + aca_magi_fraction: 1.25 + selected_marketplace_plan_category: BRONZE + output: + marketplace_csr_eligible: false + marketplace_csr_category: NONE + selected_marketplace_plan_actuarial_value: 0.6 + marketplace_csr_actuarial_value: 0 + marketplace_effective_actuarial_value: 0.6 + +- name: Case 2, silver enrollee without PTC eligibility is ineligible for CSR. + absolute_error_margin: 0.001 + period: 2026 + input: + is_aca_ptc_eligible: false + aca_magi_fraction: 1.25 + selected_marketplace_plan_category: SILVER + output: + person_receives_aca: false + marketplace_csr_eligible: false + marketplace_csr_category: NONE + selected_marketplace_plan_actuarial_value: 0.7 + marketplace_csr_actuarial_value: 0 + marketplace_effective_actuarial_value: 0.7 diff --git a/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_effective_actuarial_value.yaml b/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_effective_actuarial_value.yaml new file mode 100644 index 00000000000..729120a7362 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_effective_actuarial_value.yaml @@ -0,0 +1,14 @@ +- name: Case 1, CSR changes effective actuarial value but not Marketplace net premium. + absolute_error_margin: 0.001 + period: 2026 + input: + is_aca_ptc_eligible: true + aca_magi_fraction: 1.25 + selected_marketplace_plan_category: SILVER + selected_marketplace_plan_premium_proxy: 5_000 + used_aca_ptc: 3_000 + output: + selected_marketplace_plan_actuarial_value: 0.7 + marketplace_csr_category: AV_94 + marketplace_effective_actuarial_value: 0.94 + marketplace_net_premium: 2_000 diff --git a/policyengine_us/variables/gov/aca/csr/marketplace_csr_actuarial_value.py b/policyengine_us/variables/gov/aca/csr/marketplace_csr_actuarial_value.py new file mode 100644 index 00000000000..fa4dbf1914f --- /dev/null +++ b/policyengine_us/variables/gov/aca/csr/marketplace_csr_actuarial_value.py @@ -0,0 +1,26 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.aca.csr.marketplace_csr_category import ( + MarketplaceCSRCategory, +) + + +class marketplace_csr_actuarial_value(Variable): + value_type = float + entity = TaxUnit + label = "Marketplace cost-sharing reduction actuarial value" + unit = "/1" + definition_period = YEAR + reference = "https://www.law.cornell.edu/cfr/text/45/156.420#a" + defined_for = "marketplace_csr_eligible" + + def formula(tax_unit, period, parameters): + category = tax_unit("marketplace_csr_category", period) + p = parameters(period).gov.aca.csr.actuarial_value + return select( + [ + category == MarketplaceCSRCategory.AV_94, + category == MarketplaceCSRCategory.AV_87, + category == MarketplaceCSRCategory.AV_73, + ], + [p.highest, p.middle, p.lowest], + ) diff --git a/policyengine_us/variables/gov/aca/csr/marketplace_csr_category.py b/policyengine_us/variables/gov/aca/csr/marketplace_csr_category.py new file mode 100644 index 00000000000..f747bf51f51 --- /dev/null +++ b/policyengine_us/variables/gov/aca/csr/marketplace_csr_category.py @@ -0,0 +1,36 @@ +from policyengine_us.model_api import * + + +class MarketplaceCSRCategory(Enum): + NONE = "None" + AV_94 = "94 percent actuarial value" + AV_87 = "87 percent actuarial value" + AV_73 = "73 percent actuarial value" + + +class marketplace_csr_category(Variable): + value_type = Enum + possible_values = MarketplaceCSRCategory + default_value = MarketplaceCSRCategory.NONE + entity = TaxUnit + label = "Marketplace cost-sharing reduction category" + definition_period = YEAR + reference = "https://www.law.cornell.edu/cfr/text/45/155.305#g_2" + + def formula(tax_unit, period, parameters): + eligible = tax_unit("marketplace_csr_eligible", period) + magi_fraction = tax_unit("aca_magi_fraction", period) + p = parameters(period).gov.aca.csr.income_threshold + return select( + [ + eligible & (magi_fraction <= p.highest_av_maximum), + eligible & (magi_fraction <= p.middle_av_maximum), + eligible & (magi_fraction <= p.maximum), + ], + [ + MarketplaceCSRCategory.AV_94, + MarketplaceCSRCategory.AV_87, + MarketplaceCSRCategory.AV_73, + ], + default=MarketplaceCSRCategory.NONE, + ) diff --git a/policyengine_us/variables/gov/aca/csr/marketplace_csr_eligible.py b/policyengine_us/variables/gov/aca/csr/marketplace_csr_eligible.py new file mode 100644 index 00000000000..bf2cfd73c14 --- /dev/null +++ b/policyengine_us/variables/gov/aca/csr/marketplace_csr_eligible.py @@ -0,0 +1,22 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.aca.ptc.selected_marketplace_plan_category import ( + MarketplacePlanCategory, +) + + +class marketplace_csr_eligible(Variable): + value_type = bool + entity = TaxUnit + label = "Tax unit is eligible for ordinary Marketplace cost-sharing reductions" + definition_period = YEAR + reference = "https://www.law.cornell.edu/cfr/text/45/155.305#g_1" + + def formula(tax_unit, period, parameters): + person = tax_unit.members + receives_aca = tax_unit.sum(person("person_receives_aca", period)) > 0 + category = tax_unit("selected_marketplace_plan_category", period) + silver_selected = category == MarketplacePlanCategory.SILVER + magi_fraction = tax_unit("aca_magi_fraction", period) + p = parameters(period).gov.aca.csr.income_threshold + income_eligible = (magi_fraction >= p.minimum) & (magi_fraction <= p.maximum) + return receives_aca & silver_selected & income_eligible diff --git a/policyengine_us/variables/gov/aca/csr/marketplace_effective_actuarial_value.py b/policyengine_us/variables/gov/aca/csr/marketplace_effective_actuarial_value.py new file mode 100644 index 00000000000..94b84815fe6 --- /dev/null +++ b/policyengine_us/variables/gov/aca/csr/marketplace_effective_actuarial_value.py @@ -0,0 +1,18 @@ +from policyengine_us.model_api import * + + +class marketplace_effective_actuarial_value(Variable): + value_type = float + entity = TaxUnit + label = "Marketplace selected plan effective actuarial value" + unit = "/1" + definition_period = YEAR + reference = ( + "https://www.law.cornell.edu/cfr/text/45/156.140#b", + "https://www.law.cornell.edu/cfr/text/45/156.420#a", + ) + + def formula(tax_unit, period, parameters): + selected_plan_av = tax_unit("selected_marketplace_plan_actuarial_value", period) + csr_av = tax_unit("marketplace_csr_actuarial_value", period) + return max_(selected_plan_av, csr_av) diff --git a/policyengine_us/variables/gov/aca/ptc/selected_marketplace_plan_actuarial_value.py b/policyengine_us/variables/gov/aca/ptc/selected_marketplace_plan_actuarial_value.py new file mode 100644 index 00000000000..aeca2481fa0 --- /dev/null +++ b/policyengine_us/variables/gov/aca/ptc/selected_marketplace_plan_actuarial_value.py @@ -0,0 +1,18 @@ +from policyengine_us.model_api import * +from policyengine_us.variables.gov.aca.ptc.selected_marketplace_plan_category import ( + MarketplacePlanCategory, +) + + +class selected_marketplace_plan_actuarial_value(Variable): + value_type = float + entity = TaxUnit + label = "Selected Marketplace plan actuarial value" + unit = "/1" + definition_period = YEAR + reference = "https://www.law.cornell.edu/cfr/text/45/156.140#b" + + def formula(tax_unit, period, parameters): + category = tax_unit("selected_marketplace_plan_category", period) + p = parameters(period).gov.aca.metal_actuarial_value + return where(category == MarketplacePlanCategory.BRONZE, p.bronze, p.silver) From 3fdaad44b2c9a4e077832893aa844ced871f5931 Mon Sep 17 00:00:00 2001 From: Daphne Hansell <128793799+daphnehanse11@users.noreply.github.com> Date: Mon, 4 May 2026 09:55:10 -0400 Subject: [PATCH 4/4] Fix CSR eligibility below poverty line --- .../gov/aca/csr/income_threshold/minimum.yaml | 10 ---------- .../gov/aca/csr/marketplace_csr_category.yaml | 14 ++++++++++++++ .../gov/aca/csr/marketplace_csr_eligible.py | 5 +++-- 3 files changed, 17 insertions(+), 12 deletions(-) delete mode 100644 policyengine_us/parameters/gov/aca/csr/income_threshold/minimum.yaml diff --git a/policyengine_us/parameters/gov/aca/csr/income_threshold/minimum.yaml b/policyengine_us/parameters/gov/aca/csr/income_threshold/minimum.yaml deleted file mode 100644 index 89c136cc607..00000000000 --- a/policyengine_us/parameters/gov/aca/csr/income_threshold/minimum.yaml +++ /dev/null @@ -1,10 +0,0 @@ -description: The Affordable Care Act sets this minimum household income threshold for ordinary income-based cost-sharing reductions. -values: - 2014-01-01: 1 -metadata: - unit: /1 - period: year - label: ACA CSR minimum income threshold - reference: - - title: 45 CFR § 155.305(g)(2)(i) - Eligibility standards - href: https://www.law.cornell.edu/cfr/text/45/155.305#g_2_i diff --git a/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_csr_category.yaml b/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_csr_category.yaml index 25888017bdd..7798bf29dc4 100644 --- a/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_csr_category.yaml +++ b/policyengine_us/tests/policy/baseline/gov/aca/csr/marketplace_csr_category.yaml @@ -51,3 +51,17 @@ marketplace_csr_category: NONE marketplace_csr_actuarial_value: 0 marketplace_effective_actuarial_value: 0.7 + +- name: Case 5, APTC-eligible silver enrollee below 100 percent FPL receives 94 percent AV CSR. + absolute_error_margin: 0.001 + period: 2026 + input: + is_aca_ptc_eligible: true + aca_magi_fraction: 0.75 + selected_marketplace_plan_category: SILVER + output: + person_receives_aca: true + marketplace_csr_eligible: true + marketplace_csr_category: AV_94 + marketplace_csr_actuarial_value: 0.94 + marketplace_effective_actuarial_value: 0.94 diff --git a/policyengine_us/variables/gov/aca/csr/marketplace_csr_eligible.py b/policyengine_us/variables/gov/aca/csr/marketplace_csr_eligible.py index bf2cfd73c14..05d9f668320 100644 --- a/policyengine_us/variables/gov/aca/csr/marketplace_csr_eligible.py +++ b/policyengine_us/variables/gov/aca/csr/marketplace_csr_eligible.py @@ -17,6 +17,7 @@ def formula(tax_unit, period, parameters): category = tax_unit("selected_marketplace_plan_category", period) silver_selected = category == MarketplacePlanCategory.SILVER magi_fraction = tax_unit("aca_magi_fraction", period) - p = parameters(period).gov.aca.csr.income_threshold - income_eligible = (magi_fraction >= p.minimum) & (magi_fraction <= p.maximum) + income_eligible = ( + magi_fraction <= parameters(period).gov.aca.csr.income_threshold.maximum + ) return receives_aca & silver_selected & income_eligible