Repository navigation
Conversation
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) <noreply@anthropic.com>
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #10602 +/- ##
=========================================
Coverage 100.00% 100.00%
=========================================
Files 1 3 +2
Lines 15 65 +50
=========================================
+ Hits 15 65 +50
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
Closing as superseded by #10570, merged 2026-11-03, which applies the same IBR and PAYE rule: each borrower pays round(min(rate × discretionary income, own standard payment) × balance share, 2), per 34 CFR 685.209(f)(2)–(3) and (g)(1)(i)(B). #10570 also covers this PR's test cases (Max's four couples, mixed rates, FFEL) and registers the ruling in source_conflicts/candidates.yaml. A trial merge with main left only one docstring sentence in income_driven_repayment_standard_payment and a duplicate changelog fragment, so there's nothing left to land here. |
Part of #10411 (item 4)
Summary
The Income-Based Repayment (IBR) and Pay As You Earn (PAYE) payments now apply each borrower's 10-year standard payment cap before the spousal proration, as Max ruled on #10411 on 2026-10-31 (comment).
The rule
For each borrower:
The (g)(1)(iii) small-payment rules and the Direct Loan balance cap then apply as before.
S_iisincome_driven_repayment_standard_payment, the borrower's own 10-year payment on their Direct and FFEL loans, as Count FFEL debt in student loan proration and caps, use the latest return's AGI, and add the parent PLUS standard payment #10415 left it.balance_share_iisincome_driven_repayment_balance_share: own Direct balance divided by the couple's combined Direct and FFEL balance. It already includes the (g)(1)(i) × (g)(1)(ii) product.min(rate × DI × balance_share, S_i × direct_fraction). The separatedirect_fraction(own Direct / own Direct and FFEL) is gone from both formulas. It was a local variable with no other callers, and with the cap first,balance_sharealready carries the (g)(1)(ii) share.balance_shareequals the olddirect_fraction, andmin(a·c, b·c) = min(a, b)·cfor c > 0.Before and after (Max's table, $30,000 each)
Standard payments: 30,000 / 120 = 250.00 at 0%, 30,000 × 0.01 / (1 − 1.01^−120) = 430.41 at 12%, and 333.06 at 6%. All four rows are YAML cases in both the IBR and PAYE files.
Changed cases
Every couple case whose expected value changes, with the new arithmetic (2026 guideline: 1.5 × 21,640 = 32,460 for two people):
income_based_repayment_payment.yamlCase 20's input changed. At its old AGI, both spouses now reach the cap and pay 166.53 each, so the case would no longer show that each spouse keeps their own rate. At AGI 66,060 the 10% amount stays under the cap and the 15% amount doesn't. Case 24's PAYE output (125) is unchanged, since 250 is below the 333.06 cap.
pay_as_you_earn_payment.yamlNo single-filer expected value changed. In IBR Case 22, two single filers who each hold FFEL loans, I reworded only the comment to show the new order. The values stay at [50.19, 111.02]. The student loan
integration.yamlhas only single and head-of-household filers and is unchanged. No partner contract tests (policyengine_us/tests/policy/baseline/partners/**) use these variables. Nothing in the model readsincome_based_repayment_paymentorpay_as_you_earn_payment; they aren't subtracted from net income.Other edits
income_based_repayment_payment,pay_as_you_earn_paymentandincome_driven_repayment_standard_payment, and added a(g)(1)reference to the last of these.programs.yamlnotes for both plans to put the cap before the proration.changelog.d/student-loan-cap-before-proration.fixed.md.Source-conflict marker
There is no marker in this PR. Max's ruling id,
us:idr-10-year-cap-before-spousal-proration, isn't in rulespec-usknown-source-conflicts.yamlon main, andtest_source_conflicts.pyfails a marker that names an unregistered entry. Once the entry is registered and the vendored register is re-pinned, the deciding line in each formula (capped_amount = min_(...)) can carry# Source conflict us:idr-10-year-cap-before-spousal-proration.Tests
Run on Modal at
bb8a0d51(this PR's head), installed as CI does (uv lock --upgrade-package policyengine-core,uv sync --locked --extra dev, Python 3.14, policyengine-core 3.34.2):test_batched.py policyengine_us/tests/policy/baseline/gov/ed/student_loans --mode per-subdir --workers 1: 202 passed in 8 batches (all 26 IBR and 15 PAYE payment cases), exit 0.pytest policyengine_us/tests/code_health policyengine_us/tests/test_programs_registry.py: 1366 passed, 10 skipped, exit 0.I didn't run anything locally.
axiom: rulespec-us#1455 queued
Coordination
us-student-debt's measures pipeline pins policyengine-us at
f679eedd. It marks four cap-order couples as strict expected failures for this departure (model_departures.ibr_cap_before_proration). Its counterfactual uses the same expression as this PR,min(rate × DI, income_driven_repayment_standard_payment) × income_driven_repayment_balance_share. When it bumps the pin past this PR, it will need to flip those expected failures.🤖 Generated with Claude Code