From bb8a0d51b4205c0308c740674994ac59d6fafa7f Mon Sep 17 00:00:00 2001 From: David Trimmer Date: Mon, 2 Nov 2026 11:17:32 -0500 Subject: [PATCH] Apply the 10-year cap before the spousal proration in IBR and PAYE 34 CFR 685.209(f)(2)-(3) make the payment the lesser of the income-based amount and the borrower's own 10-year standard payment; (g)(1)(i)-(ii) then prorate that amount to the borrower's Direct Loans. Each payment is now min(rate x discretionary income, own standard payment) x balance share, rounded to the cent before the small payment rules and the balance cap. Single filers, and joint filers whose spouse has no eligible loans, are unchanged. Part of #10411 (item 4). Co-Authored-By: Claude Opus 5.5 (1M context) --- ...student-loan-cap-before-proration.fixed.md | 1 + policyengine_us/programs.yaml | 4 +- .../income_based_repayment_payment.yaml | 142 +++++++++++++----- .../pay_as_you_earn_payment.yaml | 118 ++++++++++++--- .../income_based_repayment_payment.py | 59 +++----- ...ncome_driven_repayment_standard_payment.py | 6 +- .../pay_as_you_earn_payment.py | 59 ++++---- 7 files changed, 259 insertions(+), 130 deletions(-) create mode 100644 changelog.d/student-loan-cap-before-proration.fixed.md diff --git a/changelog.d/student-loan-cap-before-proration.fixed.md b/changelog.d/student-loan-cap-before-proration.fixed.md new file mode 100644 index 00000000000..1a1682ac531 --- /dev/null +++ b/changelog.d/student-loan-cap-before-proration.fixed.md @@ -0,0 +1 @@ +Apply the 10-year standard payment cap before the spousal proration in the Income-Based Repayment and Pay As You Earn plan payments, per 34 CFR 685.209(f)(2)-(3) and (g)(1). diff --git a/policyengine_us/programs.yaml b/policyengine_us/programs.yaml index c1bc9f0b631..4cb94f1a03b 100644 --- a/policyengine_us/programs.yaml +++ b/policyengine_us/programs.yaml @@ -1790,7 +1790,7 @@ programs: variable: income_based_repayment_payment parameter_prefix: gov.ed.student_loans.income_based_repayment verified_start_year: 2026 - notes: Monthly payment (15% or 10% for new borrowers of income above 150% of the poverty guideline on the latest return's AGI, spousal and FFEL proration, cap at the borrower's own 10-year standard payment on their current Direct and FFEL balances, small payment adjustment); zero for borrowers with a Direct Loan made on or after July 1, 2026; tax-dependent borrowers treated as ineligible; interest and forgiveness not modeled; not subtracted from household net income + notes: Monthly payment (15% or 10% for new borrowers of income above 150% of the poverty guideline on the latest return's AGI, cap at the borrower's own 10-year standard payment on their current Direct and FFEL balances, then spousal and FFEL proration, small payment adjustment); zero for borrowers with a Direct Loan made on or after July 1, 2026; tax-dependent borrowers treated as ineligible; interest and forgiveness not modeled; not subtracted from household net income - id: save_plan name: SAVE plan (counterfactual) @@ -1814,7 +1814,7 @@ programs: variable: pay_as_you_earn_payment parameter_prefix: gov.ed.student_loans.pay_as_you_earn verified_start_year: 2026 - notes: Monthly payment (10% of income above 150% of the poverty guideline on the latest return's AGI, spousal and FFEL proration, cap at the borrower's own 10-year standard payment on their current Direct and FFEL balances, small payment adjustment); tax-dependent borrowers treated as ineligible; zero for borrowers with a Direct Loan made on or after July 1, 2026; enrollment limits (new borrower, repaying under PAYE on July 1, 2024) and the June 30, 2028 end not modeled; interest and forgiveness not modeled; not subtracted from household net income + notes: Monthly payment (10% of income above 150% of the poverty guideline on the latest return's AGI, cap at the borrower's own 10-year standard payment on their current Direct and FFEL balances, then spousal and FFEL proration, small payment adjustment); tax-dependent borrowers treated as ineligible; zero for borrowers with a Direct Loan made on or after July 1, 2026; enrollment limits (new borrower, repaying under PAYE on July 1, 2024) and the June 30, 2028 end not modeled; interest and forgiveness not modeled; not subtracted from household net income - id: income_contingent_repayment name: Income-Contingent Repayment diff --git a/policyengine_us/tests/policy/baseline/gov/ed/student_loans/income_based_repayment/income_based_repayment_payment.yaml b/policyengine_us/tests/policy/baseline/gov/ed/student_loans/income_based_repayment/income_based_repayment_payment.yaml index 93a9b823204..9c7756615ee 100644 --- a/policyengine_us/tests/policy/baseline/gov/ed/student_loans/income_based_repayment/income_based_repayment_payment.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ed/student_loans/income_based_repayment/income_based_repayment_payment.yaml @@ -1,15 +1,19 @@ # Monthly payment under 20 U.S.C. 1098e and 34 CFR 685.209(f)(2)-(3), (g)(1): # discretionary income = max(AGI - 1.5 x poverty guideline, 0) # payment = lesser of -# 15% (new borrowers 10%) x discretionary income / 12 -# x own Direct balance / couple's Direct and FFEL balance ((g)(1)(i)-(ii)) -# and the borrower's own 10-year standard payment on their Direct and FFEL -# loans (1098e(b)(6)(A); (f)(2)(ii), (f)(3)(ii)) -# x own Direct balance / own Direct and FFEL balance ((g)(1)(ii)) +# 15% (new borrowers 10%) x discretionary income / 12 +# and the borrower's own 10-year standard payment on their Direct and +# FFEL loans ((f)(2)(ii), (f)(3)(ii); 1098e(b)(6)(A)) +# x own Direct balance / couple's Direct and FFEL balance ((g)(1)(i)-(ii)), # rounded to the cent; under 5 -> 0; 5 to under 10 -> 10; at most the # borrower's Direct balance. +# The cap applies before the proration (#10411, ruled 2026-10-31). For a single +# filer, or a joint filer whose spouse has no eligible loans, the order makes +# no difference. # 2026 guideline in the 48 states: 15,960 + 5,680 per additional person. -# Standard payment on 30,000 at 6 percent: 333.06. +# Standard payment at 6 percent: 30,000 -> 333.06; 20,000 -> 222.04; +# 10,000 -> 111.02. At 0 percent, 30,000 / 120 = 250; at 12 percent, +# 30,000 x 0.01 / (1 - 1.01 ^ -120) = 430.41. - name: Case 1, single filer who is not a new borrower pays 15 percent. period: 2026-01 @@ -193,10 +197,11 @@ members: [person1, person2] state_code: TX output: - # 1.5 x 21,640 = 32,460; 15% x 47,540 / 12 = 594.25. - # Shares 75% and 25%: 445.69 and 148.56, below the caps of 666.12 and - # 222.04. - income_based_repayment_payment: [445.69, 148.56] + # 1.5 x 21,640 = 32,460; 15% x 47,540 / 12 = 594.25. Own standard + # payments 666.12 and 222.04; shares 75% and 25%. + # min(594.25, 666.12) x 75% = 445.69. + # min(594.25, 222.04) x 25% = 55.51. + income_based_repayment_payment: [445.69, 55.51] - name: Case 9, married filing separately excludes the spouse's income. period: 2026-01 @@ -445,11 +450,12 @@ members: [person1, person2] state_code: TX output: - # Issue #10411 example. 1.5 x 21,640 = 32,460; 15% x 48,000 / 12 = 600, - # split 50/50: 300 each. Standard payments: 30,000 / 120 = 250 and - # 30,000 x 0.01 / (1 - 1.01 ^ -120) = 430.41. min(300, 250) = 250 and - # min(300, 430.41) = 300 (20 U.S.C. 1098e(b)(6)(A)). - income_based_repayment_payment: [250, 300] + # Issue #10411 example (ruling table, row 1). 1.5 x 21,640 = 32,460; + # 15% x 48,000 / 12 = 600. Standard payments 250 and 430.41; shares 50%. + # min(600, 250) x 50% = 125. + # min(600, 430.41) x 50% = 215.21. + # Together 340.21. + income_based_repayment_payment: [125, 215.21] - name: Case 19, each spouse's own standard payment caps a high income. period: 2026-01 @@ -474,9 +480,11 @@ members: [person1, person2] state_code: TX output: - # 15% x (96,460 - 32,460) / 12 = 800, split 50/50: 400 each. - # min(400, 250) = 250 and min(400, 430.41) = 400. - income_based_repayment_payment: [250, 400] + # Ruling table, row 2. 15% x (96,460 - 32,460) / 12 = 800. + # min(800, 250) x 50% = 125. + # min(800, 430.41) x 50% = 215.21. + # Together 340.21, as at an income-based amount of 600. + income_based_repayment_payment: [125, 215.21] - name: Case 20, spouses keep their own new-borrower and other-borrower rates. period: 2026-01 @@ -496,16 +504,20 @@ tax_unit: members: [person1, person2] filing_status: JOINT - adjusted_gross_income: 92_460 + adjusted_gross_income: 66_060 households: household: members: [person1, person2] state_code: TX output: - # Discretionary income (92,460 - 32,460) / 12 = 5,000. Each spouse's own - # standard payment is 333.06. New borrower: min(10% x 5,000 x 50%, - # 333.06) = 250. Other borrower: min(15% x 5,000 x 50%, 333.06) = 333.06. - income_based_repayment_payment: [250, 333.06] + # Discretionary income (66,060 - 32,460) / 12 = 2,800. Each spouse's own + # standard payment is 333.06. The rates differ, and only the 15 percent + # amount reaches the cap. + # New borrower: min(10% x 2,800, 333.06) x 50% = min(280, 333.06) x 50% + # = 140. + # Other borrower: min(15% x 2,800, 333.06) x 50% = min(420, 333.06) x 50% + # = 166.53. + income_based_repayment_payment: [140, 166.53] - name: Case 21, a spouse's FFEL debt counts in the joint proration. period: 2026-01 @@ -532,10 +544,11 @@ state_code: TX output: # IBR analogue of rulespec-us#1455 C16. 15% x (60,000 - 32,460) / 12 = - # 344.25. The borrower pays 344.25 x 10,000 / 40,000 = 86.06 - # (685.209(g)(1)(i)-(ii)), under their own standard payment of 111.02; the - # spouse has no Direct Loans and owes nothing under the plan. - income_based_repayment_payment: [86.06, 0] + # 344.25, above the borrower's own standard payment of 111.02 on their + # 10,000. The borrower pays min(344.25, 111.02) x 10,000 / 40,000 = 27.76 + # (685.209(g)(1)(i)-(ii)); the spouse has no Direct Loans and owes nothing + # under the plan. + income_based_repayment_payment: [27.76, 0] - name: Case 22, a borrower's own FFEL loans reduce the Direct Loan payment. period: 2026-01 @@ -574,10 +587,10 @@ members: [person2] state_code: TX output: - # 685.209(g)(1)(ii): x 10,000 / 40,000 = 25%. Cap on all 40,000 of - # eligible loans: 444.08, of which the Direct Loans carry 25% = 111.02. - # person1: min(200.75 x 25%, 111.02) = 50.19. - # person2: min(950.75 x 25%, 111.02) = 111.02. + # Standard payment on all 40,000 of eligible loans: 444.08. + # 685.209(g)(1)(ii): x 10,000 / 40,000 = 25%. + # person1: min(200.75, 444.08) x 25% = 50.19. + # person2: min(950.75, 444.08) x 25% = 111.02. income_based_repayment_payment: [50.19, 111.02] - name: Case 23, a Direct Loan balance under 10 dollars caps the 10 dollar minimum payment. @@ -634,8 +647,67 @@ output: # The spouse's Direct Loans stay eligible loans (685.209(b)(5)); (d)(5) # only stops the plan from repaying them. (60,000 - 1.5 x 20,000) / 12 = - # 2,500. Income-Based Repayment 15% = 375 and Pay As You Earn 10% = 250, - # each x 30,000 / 60,000 = 187.50 and 125, under the 333.06 standard - # payment; the spouse owes 0. - income_based_repayment_payment: [187.5, 0] + # 2,500. Income-Based Repayment 15% = 375 and Pay As You Earn 10% = 250. + # IBR: min(375, 333.06) x 30,000 / 60,000 = 166.53. + # PAYE: min(250, 333.06) x 30,000 / 60,000 = 125. + # The spouse owes 0. + income_based_repayment_payment: [166.53, 0] pay_as_you_earn_payment: [125, 0] + +- name: Case 25, joint filers with equal balances and rates under both standard payments. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + federal_student_loan_balance: 30_000 + federal_student_loan_interest_rate: 0.06 + person2: + age: 38 + federal_student_loan_balance: 30_000 + federal_student_loan_interest_rate: 0.06 + tax_units: + tax_unit: + members: [person1, person2] + filing_status: JOINT + adjusted_gross_income: 80_460 + households: + household: + members: [person1, person2] + state_code: TX + output: + # Ruling table, row 3. 15% x (80,460 - 32,460) / 12 = 600, above each + # spouse's own standard payment of 333.06. + # min(600, 333.06) x 50% = 166.53 each, together 333.06. Prorating first + # would give min(300, 333.06) = 300 each. + income_based_repayment_payment: [166.53, 166.53] + +- name: Case 26, a higher income does not raise the payment of joint filers capped by both standard payments. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + federal_student_loan_balance: 30_000 + federal_student_loan_interest_rate: 0.06 + person2: + age: 38 + federal_student_loan_balance: 30_000 + federal_student_loan_interest_rate: 0.06 + tax_units: + tax_unit: + members: [person1, person2] + filing_status: JOINT + adjusted_gross_income: 96_460 + households: + household: + members: [person1, person2] + state_code: TX + output: + # Ruling table, row 4. 15% x (96,460 - 32,460) / 12 = 800. + # min(800, 333.06) x 50% = 166.53 each, together 333.06, half the + # couple's combined standard payment of 666.12. Prorating first would give + # min(400, 333.06) = 333.06 each. + income_based_repayment_payment: [166.53, 166.53] diff --git a/policyengine_us/tests/policy/baseline/gov/ed/student_loans/pay_as_you_earn/pay_as_you_earn_payment.yaml b/policyengine_us/tests/policy/baseline/gov/ed/student_loans/pay_as_you_earn/pay_as_you_earn_payment.yaml index 2b2773a3e4a..349d58bf954 100644 --- a/policyengine_us/tests/policy/baseline/gov/ed/student_loans/pay_as_you_earn/pay_as_you_earn_payment.yaml +++ b/policyengine_us/tests/policy/baseline/gov/ed/student_loans/pay_as_you_earn/pay_as_you_earn_payment.yaml @@ -1,16 +1,19 @@ # Monthly payment under 34 CFR 685.209(b)(4)(ii), (f)(2), (g)(1): # discretionary income = max(AGI - 1.5 x poverty guideline, 0) # payment = lesser of -# 10% x discretionary income / 12 -# x own Direct balance / couple's Direct and FFEL balance ((g)(1)(i)-(ii)) -# and the borrower's own 10-year standard payment on their Direct and FFEL -# loans ((f)(2)(ii)) -# x own Direct balance / own Direct and FFEL balance ((g)(1)(ii)) +# 10% x discretionary income / 12 +# and the borrower's own 10-year standard payment on their Direct and +# FFEL loans ((f)(2)(ii)) +# x own Direct balance / couple's Direct and FFEL balance ((g)(1)(i)-(ii)), # rounded to the cent; under 5 -> 0; 5 to under 10 -> 10; at most the # borrower's Direct balance. +# The cap applies before the proration (#10411, ruled 2026-10-31). For a single +# filer, or a joint filer whose spouse has no eligible loans, the order makes +# no difference. # 2026 guideline in the 48 states: 15,960 + 5,680 per additional person. # Standard payment at 6 percent: 30,000 -> 333.06; 40,000 -> 444.08; -# 20,000 -> 222.04; 10,000 -> 111.02. +# 20,000 -> 222.04; 10,000 -> 111.02. At 0 percent, 30,000 / 120 = 250; at +# 12 percent, 30,000 x 0.01 / (1 - 1.01 ^ -120) = 430.41. - name: Case 1, single filer pays 10 percent of discretionary income. period: 2026-01 @@ -79,10 +82,11 @@ members: [person1, person2] state_code: TX output: - # 1.5 x 21,640 = 32,460; 10% x 47,540 / 12 = 396.17. - # Shares 2/3 and 1/3: 264.11 and 132.06, below the caps of 444.08 and - # 222.04. - pay_as_you_earn_payment: [264.11, 132.06] + # 1.5 x 21,640 = 32,460; 10% x 47,540 / 12 = 396.17. Own standard + # payments 444.08 and 222.04; shares 2/3 and 1/3. + # min(396.17, 444.08) x 2/3 = 264.11. + # min(396.17, 222.04) x 1/3 = 74.01. + pay_as_you_earn_payment: [264.11, 74.01] - name: Case 4, each spouse's own standard payment caps joint filers who both borrowed. period: 2026-01 @@ -107,9 +111,11 @@ members: [person1, person2] state_code: TX output: - # 10% x (150,000 - 32,460) / 12 = 979.50; shares 80% and 20%: 783.60 and - # 195.90, above each spouse's own standard payment of 444.08 and 111.02. - pay_as_you_earn_payment: [444.08, 111.02] + # 10% x (150,000 - 32,460) / 12 = 979.50, above each spouse's own + # standard payment of 444.08 and 111.02; shares 80% and 20%. + # min(979.50, 444.08) x 80% = 355.27. + # min(979.50, 111.02) x 20% = 22.20. + pay_as_you_earn_payment: [355.27, 22.2] - name: Case 5, a payment under 5 dollars becomes zero. period: 2026-01 @@ -255,11 +261,12 @@ members: [person1, person2] state_code: TX output: - # Issue #10411 example. 1.5 x 21,640 = 32,460; 10% x 72,000 / 12 = 600, - # split 50/50: 300 each. Standard payments: 30,000 / 120 = 250 and - # 30,000 x 0.01 / (1 - 1.01 ^ -120) = 430.41. min(300, 250) = 250 and - # min(300, 430.41) = 300. - pay_as_you_earn_payment: [250, 300] + # Issue #10411 example (ruling table, row 1). 1.5 x 21,640 = 32,460; + # 10% x 72,000 / 12 = 600. Standard payments 250 and 430.41; shares 50%. + # min(600, 250) x 50% = 125. + # min(600, 430.41) x 50% = 215.21. + # Together 340.21. + pay_as_you_earn_payment: [125, 215.21] - name: Case 11, each spouse's own standard payment caps a high income. period: 2026-01 @@ -284,9 +291,11 @@ members: [person1, person2] state_code: TX output: - # 10% x (128,460 - 32,460) / 12 = 800, split 50/50: 400 each. - # min(400, 250) = 250 and min(400, 430.41) = 400. - pay_as_you_earn_payment: [250, 400] + # Ruling table, row 2. 10% x (128,460 - 32,460) / 12 = 800. + # min(800, 250) x 50% = 125. + # min(800, 430.41) x 50% = 215.21. + # Together 340.21, as at an income-based amount of 600. + pay_as_you_earn_payment: [125, 215.21] - name: Case 12, a spouse's FFEL debt counts in the joint proration. period: 2026-01 @@ -312,10 +321,11 @@ members: [person1, person2] state_code: TX output: - # 10% x (61,000 - 32,460) / 12 = 237.83. The borrower pays - # 237.83 x 10,000 / 40,000 = 59.46 (685.209(g)(1)(i)-(ii)), under their own - # standard payment of 111.02; the spouse owes nothing. - pay_as_you_earn_payment: [59.46, 0] + # 10% x (61,000 - 32,460) / 12 = 237.83, above the borrower's own + # standard payment of 111.02 on their 10,000. The borrower pays + # min(237.83, 111.02) x 10,000 / 40,000 = 27.76 (685.209(g)(1)(i)-(ii)); + # the spouse owes nothing. + pay_as_you_earn_payment: [27.76, 0] - name: Case 13, a payment within half a cent below 5 dollars rounds to 5 and rises to 10. period: 2026-01 @@ -339,3 +349,61 @@ # 24,539.52 - 23,940 = 599.52; 10% x 599.52 / 12 = 4.996, which rounds to # 5.00 and rises to 10 (the modeling choice in the documentation). pay_as_you_earn_payment: 10 + +- name: Case 14, joint filers with equal balances and rates under both standard payments. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + federal_student_loan_balance: 30_000 + federal_student_loan_interest_rate: 0.06 + person2: + age: 38 + federal_student_loan_balance: 30_000 + federal_student_loan_interest_rate: 0.06 + tax_units: + tax_unit: + members: [person1, person2] + filing_status: JOINT + adjusted_gross_income: 104_460 + households: + household: + members: [person1, person2] + state_code: TX + output: + # Ruling table, row 3. 10% x (104,460 - 32,460) / 12 = 600, above each + # spouse's own standard payment of 333.06. + # min(600, 333.06) x 50% = 166.53 each, together 333.06. Prorating first + # would give min(300, 333.06) = 300 each. + pay_as_you_earn_payment: [166.53, 166.53] + +- name: Case 15, a higher income does not raise the payment of joint filers capped by both standard payments. + period: 2026-01 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 40 + federal_student_loan_balance: 30_000 + federal_student_loan_interest_rate: 0.06 + person2: + age: 38 + federal_student_loan_balance: 30_000 + federal_student_loan_interest_rate: 0.06 + tax_units: + tax_unit: + members: [person1, person2] + filing_status: JOINT + adjusted_gross_income: 128_460 + households: + household: + members: [person1, person2] + state_code: TX + output: + # Ruling table, row 4. 10% x (128,460 - 32,460) / 12 = 800. + # min(800, 333.06) x 50% = 166.53 each, together 333.06, half the + # couple's combined standard payment of 666.12. Prorating first would give + # min(400, 333.06) = 333.06 each. + pay_as_you_earn_payment: [166.53, 166.53] diff --git a/policyengine_us/variables/gov/ed/student_loans/income_based_repayment/income_based_repayment_payment.py b/policyengine_us/variables/gov/ed/student_loans/income_based_repayment/income_based_repayment_payment.py index e8ab75d21b1..76bc5f80d0a 100644 --- a/policyengine_us/variables/gov/ed/student_loans/income_based_repayment/income_based_repayment_payment.py +++ b/policyengine_us/variables/gov/ed/student_loans/income_based_repayment/income_based_repayment_payment.py @@ -8,18 +8,24 @@ class income_based_repayment_payment(Variable): documentation = ( "Monthly payment owed by a borrower under the Income-Based Repayment " "plan, assuming the borrower is enrolled in the plan and in active " - "repayment, not in deferment, forbearance or default. The borrower's " - "plan rate times discretionary income is prorated to their Direct " - "Loans (income_driven_repayment_balance_share, which for joint filers " - "divides by both spouses' Direct and FFEL loans) and capped at the " - "Direct Loan part of the borrower's own 10-year standard payment " + "repayment, not in deferment, forbearance or default. The payment is " + "the lesser of the borrower's plan rate times discretionary income " + "and the borrower's own 10-year standard payment " "(income_driven_repayment_standard_payment on their Direct and FFEL " - "loans, times their Direct share of those loans). The result is " - "rounded to the cent, given the small payment adjustment and capped " - "at the borrower's Direct Loan balance. Spouses keep their own rates, " - "so a new borrower pays 10 percent and the other spouse 15 percent of " - "the same discretionary income before proration, and each spouse's " - "cap covers only their own loans. The cap uses the current balance, " + "loans at their own interest rate), prorated to their Direct Loans " + "(income_driven_repayment_balance_share, which for joint filers " + "divides by both spouses' Direct and FFEL loans). The cap applies " + "before the proration (34 CFR 685.209(f)(2)-(3), (g)(1)(i)-(ii)), so " + "joint filers with equal balances and interest rates whose " + "income-based amount exceeds their standard payments together pay " + "half their combined standard payment, whatever their income. The " + "result is rounded to the cent, given the small payment adjustment " + "and capped at the borrower's Direct Loan balance. Spouses keep their " + "own rates, so a new borrower pays 10 percent and the other spouse 15 " + "percent of the same discretionary income before the cap and " + "proration, and each spouse's cap covers only their own loans. A " + "single filer, or a joint filer whose spouse has no eligible loans, " + "pays the same under either order. The cap uses the current balance, " "not the balance when the borrower entered the plan. The prorated " "payment is rounded to the cent before the small payment adjustment, " "a modeling choice: a calculated payment just under $5, such as " @@ -53,43 +59,28 @@ def formula(person, period, parameters): discretionary_income = person.tax_unit( "income_based_repayment_discretionary_income", period ) - # 685.209(g)(1)(i)-(ii) prorate the income-based amount to the - # borrower's Direct Loans. 20 U.S.C. 1098e(b)(6)(A) and 685.209(f)(2)(ii), - # (f)(3)(ii) cap the payment on all of the borrower's eligible loans at - # their own 10-year standard payment; the Direct Loans carry the - # (g)(1)(ii) part of that cap. + # 685.209(f)(2)(ii) and (f)(3)(ii) take the lesser of the income-based + # amount and the borrower's own 10-year standard payment on their + # Direct and FFEL loans (20 U.S.C. 1098e(b)(6)(A)). 685.209(g)(1)(i)-(ii) + # then prorate that lesser amount to the borrower's Direct Loans. + standard_payment = person("income_driven_repayment_standard_payment", period) + capped_amount = min_(rate * discretionary_income, standard_payment) balance_share = person( "income_driven_repayment_balance_share", period.this_year ) - balance = person("federal_student_loan_balance", period.this_year) - eligible_balance = balance + person( - "federal_ffel_loan_balance", period.this_year - ) - direct_fraction = np.divide( - balance, - eligible_balance, - out=np.zeros_like(balance), - where=eligible_balance > 0, - ) - standard_payment = person("income_driven_repayment_standard_payment", period) # Payments are set in whole cents. Rounding before the small payment # test keeps an exact $5.00 from falling below the threshold through # floating-point error (e.g. 15% x $400 / 12). It also lifts an amount # within half a cent below $5 to $5.00, a modeling choice described in # the documentation. - prorated_payment = np.round( - min_( - rate * discretionary_income * balance_share, - standard_payment * direct_fraction, - ), - 2, - ) + prorated_payment = np.round(capped_amount * balance_share, 2) small_payment = p.income_driven_repayment.small_payment adjusted_payment = where( prorated_payment < small_payment.threshold, 0, max_(prorated_payment, small_payment.minimum), ) + balance = person("federal_student_loan_balance", period.this_year) payment = min_(adjusted_payment, balance) # 20 U.S.C. 1087e(d)(1) and (d)(7)(A) and 34 CFR 685.209(d)(5) limit # the plan to borrowers with no Direct Loan made on or after July 1, diff --git a/policyengine_us/variables/gov/ed/student_loans/income_driven_repayment/income_driven_repayment_standard_payment.py b/policyengine_us/variables/gov/ed/student_loans/income_driven_repayment/income_driven_repayment_standard_payment.py index 83d2358d965..308f3183617 100644 --- a/policyengine_us/variables/gov/ed/student_loans/income_driven_repayment/income_driven_repayment_standard_payment.py +++ b/policyengine_us/variables/gov/ed/student_loans/income_driven_repayment/income_driven_repayment_standard_payment.py @@ -13,7 +13,10 @@ class income_driven_repayment_standard_payment(Variable): "on their own eligible loans, the most the borrower pays under the " "Income-Based Repayment and Pay As You Earn plans (20 U.S.C. " "1098e(b)(6)(A): 'for all loans made to the borrower under part B or " - "D'). Eligible loans are Direct and FFEL loans other than parent PLUS " + "D'). 34 CFR 685.209(f)(2)(ii) and (f)(3)(ii) compare it with the " + "income-based amount, and (g)(1) then prorates the lesser of the two " + "(income_driven_repayment_balance_share). Eligible loans are Direct " + "and FFEL loans other than parent PLUS " "loans and excepted consolidation loans, so this is the 10-year " "standard payment on the person's federal_student_loan_balance plus " "federal_ffel_loan_balance at their federal_student_loan_interest_rate. " @@ -30,6 +33,7 @@ class income_driven_repayment_standard_payment(Variable): "https://www.ecfr.gov/current/title-34/section-685.209#p-685.209(b)(5)", "https://www.ecfr.gov/current/title-34/section-685.209#p-685.209(f)(2)(ii)", "https://www.ecfr.gov/current/title-34/section-685.209#p-685.209(f)(3)(ii)", + "https://www.ecfr.gov/current/title-34/section-685.209#p-685.209(g)(1)", "https://www.ecfr.gov/current/title-34/section-685.208#p-685.208(b)(1)", ) diff --git a/policyengine_us/variables/gov/ed/student_loans/pay_as_you_earn/pay_as_you_earn_payment.py b/policyengine_us/variables/gov/ed/student_loans/pay_as_you_earn/pay_as_you_earn_payment.py index 902f2100ca6..3b56522cdf2 100644 --- a/policyengine_us/variables/gov/ed/student_loans/pay_as_you_earn/pay_as_you_earn_payment.py +++ b/policyengine_us/variables/gov/ed/student_loans/pay_as_you_earn/pay_as_you_earn_payment.py @@ -8,17 +8,23 @@ class pay_as_you_earn_payment(Variable): documentation = ( "Monthly payment owed by a borrower under the Pay As You Earn plan, " "assuming the borrower is enrolled in the plan and in active " - "repayment, not in deferment, forbearance or default. The plan rate " - "times discretionary income is prorated to the borrower's Direct " - "Loans (income_driven_repayment_balance_share, which for joint filers " - "divides by both spouses' Direct and FFEL loans) and capped at the " - "Direct Loan part of the borrower's own 10-year standard payment " + "repayment, not in deferment, forbearance or default. The payment is " + "the lesser of the plan rate times discretionary income and the " + "borrower's own 10-year standard payment " "(income_driven_repayment_standard_payment on their Direct and FFEL " - "loans at their own interest rate, times their Direct share of those " - "loans). The result is rounded to the cent, given the small payment " - "adjustment and capped at the borrower's Direct Loan balance. Each " - "spouse's cap covers only their own loans. As with the Income-Based " - "Repayment plan, the cap uses the current balance, not the balance " + "loans at their own interest rate), prorated to their Direct Loans " + "(income_driven_repayment_balance_share, which for joint filers " + "divides by both spouses' Direct and FFEL loans). The cap applies " + "before the proration (34 CFR 685.209(f)(2), (g)(1)(i)-(ii)), so " + "joint filers with equal balances and interest rates whose " + "income-based amount exceeds their standard payments together pay " + "half their combined standard payment, whatever their income. The " + "result is rounded to the cent, given the small payment adjustment " + "and capped at the borrower's Direct Loan balance. Each spouse's cap " + "covers only their own loans. A single filer, or a joint filer whose " + "spouse has no eligible loans, pays the same under either order. As " + "with the Income-Based Repayment plan, the cap uses the current " + "balance, not the balance " "when the borrower entered the plan. A borrower with a Direct Loan " "made on or after July 1, 2026 cannot use the plan and owes nothing " "under it. The other limits on who can repay under the plan are not " @@ -48,42 +54,29 @@ def formula(person, period, parameters): discretionary_income = person.tax_unit( "pay_as_you_earn_discretionary_income", period ) - # 685.209(g)(1)(i)-(ii) prorate the income-based amount to the - # borrower's Direct Loans. 685.209(f)(2)(ii), as in 20 U.S.C. - # 1098e(b)(6)(A) for the Income-Based Repayment plan, caps the payment - # on all of the borrower's eligible loans at their own 10-year standard - # payment; the Direct Loans carry the (g)(1)(ii) part of that cap. + # 685.209(f)(2)(ii) takes the lesser of the income-based amount and the + # borrower's own 10-year standard payment on their Direct and FFEL + # loans. 685.209(g)(1)(i)-(ii) then prorate that lesser amount to the + # borrower's Direct Loans. + standard_payment = person("income_driven_repayment_standard_payment", period) + capped_amount = min_( + p.pay_as_you_earn.rate * discretionary_income, standard_payment + ) balance_share = person( "income_driven_repayment_balance_share", period.this_year ) - balance = person("federal_student_loan_balance", period.this_year) - eligible_balance = balance + person( - "federal_ffel_loan_balance", period.this_year - ) - direct_fraction = np.divide( - balance, - eligible_balance, - out=np.zeros_like(balance), - where=eligible_balance > 0, - ) - standard_payment = person("income_driven_repayment_standard_payment", period) # Payments are set in whole cents. Rounding before the small payment # test keeps an exact $5.00 from falling below the threshold through # floating-point error. It also lifts an amount within half a cent # below $5 to $5.00, a modeling choice described in the documentation. - prorated_payment = np.round( - min_( - p.pay_as_you_earn.rate * discretionary_income * balance_share, - standard_payment * direct_fraction, - ), - 2, - ) + prorated_payment = np.round(capped_amount * balance_share, 2) small_payment = p.income_driven_repayment.small_payment adjusted_payment = where( prorated_payment < small_payment.threshold, 0, max_(prorated_payment, small_payment.minimum), ) + balance = person("federal_student_loan_balance", period.this_year) payment = min_(adjusted_payment, balance) # 20 U.S.C. 1087e(d)(1) and (d)(7)(A) and 34 CFR 685.209(c)(4)(v), # (d)(5) limit the plan to borrowers with no Direct Loan made on or