diff --git a/changelog.d/fix-mt-ehrc-filer-gains-ss-roles.fixed.md b/changelog.d/fix-mt-ehrc-filer-gains-ss-roles.fixed.md new file mode 100644 index 00000000000..6ea58342f76 --- /dev/null +++ b/changelog.d/fix-mt-ehrc-filer-gains-ss-roles.fixed.md @@ -0,0 +1 @@ +Montana's elderly homeowner/renter credit now counts each person's capital gains without losses netted on the same return or carried over from a prior year, and counts Social Security once when a person is input as both a tax unit dependent and the head or spouse. diff --git a/policyengine_us/parameters/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/gross_income_sources.yaml b/policyengine_us/parameters/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/gross_income_sources.yaml index 7c3c2184cdf..cf106919fa7 100644 --- a/policyengine_us/parameters/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/gross_income_sources.yaml +++ b/policyengine_us/parameters/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/gross_income_sources.yaml @@ -80,8 +80,11 @@ values: - mt_elderly_homeowner_or_renter_credit_received - mt_elderly_homeowner_or_renter_credit_federal_refundable_credits - mt_elderly_homeowner_or_renter_credit_property_tax_rebate_received -# Social Security under § 15-30-2337(9)(a)(viii) is added separately in -# mt_elderly_homeowner_or_renter_credit_gross_household_income.py because the -# untaxed portion is computed as social_security - taxable_social_security. +# mt_elderly_homeowner_or_renter_credit_gross_household_income.py adds three +# items separately: a tax unit dependent's own gross income (their income is +# on their own return, not in adjusted_gross_income_person); the untaxed +# portion of Social Security under § 15-30-2337(9)(a)(viii), computed as +# social_security less the taxable part already counted; and the losses +# deducted in federal AGI. # This year's elderly homeowner/renter credit is excluded to avoid a circular # reference; the credit received during the year counts instead. diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml index 77815f66850..26c8d9b63a2 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit.yaml @@ -842,3 +842,159 @@ mt_elderly_homeowner_or_renter_credit_pre_one_claimant: [50, 410] mt_elderly_homeowner_or_renter_credit_selected_claimant: [false, true] mt_elderly_homeowner_or_renter_credit: [0, 410] + +- name: 2024 Schedule 2EC, a household member's netted capital loss does not raise the credit + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + real_estate_taxes: 2_000 + person2: + age: 30 + long_term_capital_gains: 10_000 + short_term_capital_gains: -4_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Line 5 = 20_000; line 17 = 10_000 (the gain without the loss); line + # 18 = 30_000; line 20 = 17_400; line 22 = 17_400 x 0.05 = 870; line 27 = + # 2_000 - 870 = 1_130; line 29 = 100%; line 30 = 1_130. Netting the loss + # would give line 18 = 26_000, line 22 = 670 and the 1_150 cap. + mt_elderly_homeowner_or_renter_credit_net_household_income: [870, 0] + mt_elderly_homeowner_or_renter_credit: [1_130, 0] + +- name: 2024 Schedule 2EC, a capital loss netted between spouses on a joint return is added back once + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + long_term_capital_gains: 10_000 + real_estate_taxes: 2_000 + person2: + age: 68 + short_term_capital_gains: -4_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Federal AGI 20_000 + 10_000 - 4_000 = 26_000; the 4_000 loss is added + # back once. Line 18 = 30_000; line 22 = 870; line 30 = 1_130. + mt_elderly_homeowner_or_renter_credit_net_household_income: [870, 0] + mt_elderly_homeowner_or_renter_credit: [1_130, 0] + +- name: 2024 Schedule 2EC, a minor filing alone in the household counts once + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + real_estate_taxes: 2_000 + is_tax_unit_dependent: false + person2: + age: 16 + employment_income: 10_000 + is_tax_unit_dependent: false + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Line 18 = 20_000 + 10_000 = 30_000; line 30 = 1_130. Counting the + # minor's wages twice would give 40_000, line 22 = 1_370 and a credit of + # (2_000 - 1_370) x 30% = 189. + mt_elderly_homeowner_or_renter_credit: [1_130, 0] + +- name: 2024 Schedule 2EC, a capital loss carryover does not raise the credit + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + long_term_capital_gains: 6_000 + long_term_capital_loss_carryover: 4_000 + real_estate_taxes: 2_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_name: MT + output: + # Line 5 = 20_000; line 7 = the year's 10_000 gain; line 18 = 30_000; + # line 22 = 870; line 30 = 1_130. Counting the 6_000 net would give line + # 18 = 26_000, line 22 = 670 and the 1_150 cap. + mt_elderly_homeowner_or_renter_credit: 1_130 + +- name: 2024 Schedule 2EC, a carryover against a net loss supplied directly adds no gain + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 30_000 + capital_gains: -5_000 + long_term_capital_loss_carryover: 5_000 + real_estate_taxes: 2_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_name: MT + output: + # Line 18 = 30_000; line 22 = 17_400 x 0.05 = 870; line 30 = 1_130. + # Counting the carryover as a gain would give 35_000 and 880 x 40% = 352. + mt_elderly_homeowner_or_renter_credit: 1_130 + +- name: 2024 Schedule 2EC, a dependent's gain counts when another person's capital_gains is supplied + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + capital_gains: 0 + real_estate_taxes: 2_000 + person2: + age: 16 + long_term_capital_gains: 10_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Line 18 = 20_000 + 10_000 = 30_000; line 22 = 870; line 30 = 1_130. + mt_elderly_homeowner_or_renter_credit: [1_130, 0] diff --git a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.yaml b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.yaml index ec00f8c2fad..88c8b004aa0 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/mt/tax/income/credits/elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.yaml @@ -336,3 +336,449 @@ # extent that the amount recovered reduced the claimant's Montana income # tax in a prior year". mt_elderly_homeowner_or_renter_credit_gross_household_income: 10_000 + +- name: A filer's short-term loss netted against their own long-term gain does not reduce gross household income + period: 2024 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + person2: + age: 30 + long_term_capital_gains: 10_000 + short_term_capital_gains: -4_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # § 15-30-2337(9)(a): federal AGI "without regard to loss"; 2024 + # Schedule 2EC line 7: "do not include any capital losses". The member's + # own return nets the 4_000 loss into a 6_000 gain, so no loss reaches + # loss_ald; line 17 still carries the 10_000 gain. + mt_elderly_homeowner_or_renter_credit_gross_household_income: [20_000, 10_000] + +- name: A claimant's short-term loss netted against their own long-term gain does not reduce gross household income + period: 2024 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + long_term_capital_gains: 10_000 + short_term_capital_gains: -4_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_name: MT + output: + # Line 5 pensions 20_000 plus line 7 capital gain 10_000, without the + # 4_000 loss: 30_000. + mt_elderly_homeowner_or_renter_credit_gross_household_income: 30_000 + +- name: A minor alone in a tax unit and not input as a dependent counts once + period: 2024 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + is_tax_unit_dependent: false + person2: + age: 16 + employment_income: 10_000 + is_tax_unit_dependent: false + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # The minor is neither head (no adult in the unit), spouse nor + # dependent. irs_gross_income counts people who are not dependents and + # dependent_gross_income people who are not the head or spouse, so only + # the dependent gate keeps the 10_000 wages from counting twice. + is_tax_unit_head: [true, false] + is_tax_unit_dependent: [false, false] + mt_elderly_homeowner_or_renter_credit_gross_household_income: [20_000, 10_000] + +- name: Social Security counts in full for a person input as a dependent who is also the head + period: 2024 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + is_tax_unit_dependent: false + person2: + age: 40 + social_security_retirement: 60_000 + is_tax_unit_dependent: true + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + marital_units: + marital_unit1: + members: [person1] + marital_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Every adult in tax_unit2 is input as a dependent, so person2 is also + # its head. Neither irs_gross_income (dependents) nor + # dependent_gross_income (heads) counts their taxable benefits, so none + # may be subtracted: § 15-30-2337(9)(a)(viii) counts all 60_000. + is_tax_unit_head: [true, true] + is_tax_unit_dependent: [false, true] + mt_elderly_homeowner_or_renter_credit_gross_household_income: [20_000, 60_000] + +- name: A business loss on another household member's return does not reduce gross household income + period: 2024 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + person2: + age: 40 + taxable_interest_income: 10_000 + self_employment_income: -5_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # The member's federal AGI is 10_000 - 5_000 = 5_000; the 5_000 loss is + # added back on their own return's head. 2024 Schedule 2EC line 10: + # "Do not include any losses generated from the business." + mt_elderly_homeowner_or_renter_credit_gross_household_income: [20_000, 10_000] + +- name: A dependent in another household member's tax unit counts + period: 2024 + absolute_error_margin: 0.01 + input: + people: + person1: + age: 70 + taxable_pension_income: 15_000 + person2: + age: 35 + employment_income: 25_000 + person3: + age: 5 + person4: + age: 8 + employment_income: 3_000 + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2, person3, person4] + households: + household: + members: [person1, person2, person3, person4] + state_name: MT + output: + # Three generations. The daughter's line 8 credits: federal EITC with + # two children 6_960 - 21.06% x (25_000 - 22_720) = 6_479.83; refundable + # child tax credit min(2 x 1_700, 15% x (25_000 - 2_500)) = 3_375; + # Montana EITC 10% = 647.98. Her total: 25_000 + 6_479.83 + 3_375 + + # 647.98 = 35_502.81. The grandchild is her dependent; their 3_000 of + # wages count through the dependent path. + mt_elderly_homeowner_or_renter_credit_gross_household_income: [15_000, 35_502.81, 0, 3_000] + +- name: A prior-year capital loss carryover does not reduce the year's gain + period: 2024 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + # A 10_000 gain this year less a 4_000 carryover (Schedule D line 15). + long_term_capital_gains: 6_000 + long_term_capital_loss_carryover: 4_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_name: MT + output: + # Line 7 counts the year's gain, 10_000, without the carried loss. + mt_elderly_homeowner_or_renter_credit_gross_household_income: 30_000 + +- name: A carryover larger than the year's gain leaves the gain counted and the rest unused + period: 2024 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + # A 2_000 gain this year less a 10_000 carryover. + long_term_capital_gains: -8_000 + long_term_capital_loss_carryover: 10_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_name: MT + output: + # 20_000 + the year's 2_000 gain; the unused 8_000 of carryover counts + # neither as income nor as a loss. + mt_elderly_homeowner_or_renter_credit_gross_household_income: 22_000 + +- name: A carryover with no gain this year adds nothing + period: 2024 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + long_term_capital_gains: -5_000 + long_term_capital_loss_carryover: 5_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_name: MT + output: + mt_elderly_homeowner_or_renter_credit_gross_household_income: 20_000 + +- name: A dependent's capital loss carryover does not reduce their gain + period: 2024 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + person2: + age: 16 + long_term_capital_gains: 6_000 + long_term_capital_loss_carryover: 4_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # Line 17: the dependent's 10_000 gain this year, as on a filer's return. + mt_elderly_homeowner_or_renter_credit_gross_household_income: [20_000, 10_000] + +- name: A long-term loss netted against a short-term gain does not reduce gross household income + period: 2024 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + long_term_capital_gains: -4_000 + short_term_capital_gains: 10_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_name: MT + output: + mt_elderly_homeowner_or_renter_credit_gross_household_income: 30_000 + +- name: A gain is counted when the person's capital losses exceed it + period: 2024 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + long_term_capital_gains: -10_000 + short_term_capital_gains: 4_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_name: MT + output: + # Federal AGI 20_000 - 3_000 (the § 1211(b) limit); the 3_000 is added + # back and the 4_000 short-term gain counts: 24_000. + mt_elderly_homeowner_or_renter_credit_gross_household_income: 24_000 + +- name: Social Security counts in full for a spouse who is also input as a dependent + period: 2024 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + is_tax_unit_dependent: false + person2: + age: 50 + social_security_retirement: 60_000 + is_tax_unit_dependent: true + person3: + age: 45 + social_security_retirement: 60_000 + is_tax_unit_dependent: true + tax_units: + tax_unit1: + members: [person1] + tax_unit2: + members: [person2, person3] + marital_units: + marital_unit1: + members: [person1] + marital_unit2: + members: [person2, person3] + households: + household: + members: [person1, person2, person3] + state_name: MT + output: + # Both adults of tax_unit2 are input as dependents, so they stay head and + # spouse as well. § 15-30-2337(9)(a)(viii) counts all of each one's + # benefits. + is_tax_unit_head: [true, true, false] + is_tax_unit_spouse: [false, false, true] + mt_elderly_homeowner_or_renter_credit_gross_household_income: [20_000, 60_000, 60_000] + +- name: A carryover with a net capital loss supplied directly counts no gain + period: 2024 + input: + people: + person1: + age: 70 + taxable_pension_income: 30_000 + # A net amount supplied directly, without long-term and short-term + # parts; with the 5_000 carryover added back, the year's net is zero. + capital_gains: -5_000 + long_term_capital_loss_carryover: 5_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_name: MT + output: + # Federal AGI 30_000 - 3_000 (§ 1211(b)); the 3_000 is added back. No + # gain this year, so line 7 is zero: 30_000. + mt_elderly_homeowner_or_renter_credit_gross_household_income: 30_000 + +- name: A carryover with a net capital gain supplied directly restores the year's gain + period: 2024 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + capital_gains: 6_000 + long_term_capital_loss_carryover: 4_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_name: MT + output: + # The year's net gain is 6_000 + 4_000 = 10_000: 30_000. + mt_elderly_homeowner_or_renter_credit_gross_household_income: 30_000 + +- name: A dependent's carryover with a net capital loss supplied directly counts no gain + period: 2024 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + person2: + age: 16 + employment_income: 10_000 + capital_gains: -5_000 + long_term_capital_loss_carryover: 5_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + mt_elderly_homeowner_or_renter_credit_gross_household_income: [20_000, 10_000] + +- name: A dependent's long-term gain counts when another person's capital_gains is supplied + period: 2024 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + capital_gains: 0 + person2: + age: 16 + long_term_capital_gains: 10_000 + tax_units: + tax_unit: + members: [person1, person2] + households: + household: + members: [person1, person2] + state_name: MT + output: + # capital_gains is an input for person1, so person2 holds its default of + # zero rather than the sum of their parts. The parts decide: line 17 + # counts the dependent's 10_000 gain. + mt_elderly_homeowner_or_renter_credit_gross_household_income: [20_000, 10_000] + +- name: The long-term and short-term parts decide the gains when capital_gains disagrees with them + period: 2024 + input: + people: + person1: + age: 70 + taxable_pension_income: 20_000 + long_term_capital_gains: 10_000 + short_term_capital_gains: -4_000 + capital_gains: 20_000 + tax_units: + tax_unit: + members: [person1] + households: + household: + members: [person1] + state_name: MT + output: + # The parts show a 10_000 gain and a 4_000 loss; line 7 counts 10_000, + # not the inconsistent 20_000 net: 30_000. + mt_elderly_homeowner_or_renter_credit_gross_household_income: 30_000 diff --git a/policyengine_us/tests/test_mt_elderly_credit_capital_gains.py b/policyengine_us/tests/test_mt_elderly_credit_capital_gains.py new file mode 100644 index 00000000000..2377af122f6 --- /dev/null +++ b/policyengine_us/tests/test_mt_elderly_credit_capital_gains.py @@ -0,0 +1,222 @@ +"""Invariant tests: capital gains in Montana elderly homeowner/renter credit +income are counted without losses, however they are supplied. + +MCA 15-30-2337(9)(a) defines income as "federal adjusted gross income, +without regard to loss", and 2024 Schedule 2EC line 7 says "do not include +any capital losses". So a person's capital gains enter gross household income +as the year's gains without losses. Three losses can hide them: +- a short-term loss netted against a long-term gain, or the reverse; +- the prior-year long-term capital loss carryover, which + long_term_capital_gains already includes; +- losses netted against capital gain distributions. + +The model counts gains through federal AGI and the loss_ald add-back for +filers, and through dependent_gross_income for tax unit dependents, and +restores what each path nets away. + +The gain is a piecewise-linear function of a few amounts. So instead of +random draws, one vectorized simulation covers a grid that reaches every +sign region of the long-term amount plus carryover, the short-term amount, +and a net amount, with and without distributions. It supplies them in three +styles: + +- parts: long-term and short-term amounts, with capital_gains set to their + sum; +- sparse parts: long-term and short-term amounts with capital_gains omitted. + Because capital_gains is an input for other people in the same + simulation, these people hold its default of zero, not the sum of their + parts (review round 3 of #10139); +- net: capital_gains alone, without parts. + +Each case appears three ways: (A) a household member who is the claimant's +dependent, (B) a member who files their own return, and (C) the claimant +themselves. + +1. Gross household income is the claimant's pension plus the gains without + losses plus distributions. The reference is written from Schedule 2EC + line 7, independently of the model: with parts, + max(0, long-term + carryover) + max(0, short-term); without them, + max(0, net + carryover). +2. The claimant's credit is the same in A and B. + +The model computes in single precision, so comparisons allow one cent or +eight float32 spacings at the case's total, whichever is larger. +""" + +import itertools + +import numpy as np +import pytest + +from policyengine_us import Simulation + +YEAR = 2024 +PENSION = 20_000 +PROPERTY_TAX = 2_000 +TOLERANCE = 0.01 + +LONG_TERM = [-12_000, -3_000, 0, 4_000, 11_000] +SHORT_TERM = [-7_000, 0, 2_000, 9_000] +NET = [-9_000, -2_500, 0, 3_000, 14_000] +CARRYOVER = [0, 2_500, 15_000] +DISTRIBUTIONS = [0, 4_000] + + +def grid(): + cases = [] + for style in ["parts", "sparse"]: + for lt, st, co, dist in itertools.product( + LONG_TERM, SHORT_TERM, CARRYOVER, DISTRIBUTIONS + ): + if lt == 0 and st == 0: + continue # no parts: covered by the net style + cases.append( + dict(style=style, long_term=lt, short_term=st, net=lt + st) + | dict(carryover=co, distributions=dist) + ) + for net, co, dist in itertools.product(NET, CARRYOVER, DISTRIBUTIONS): + cases.append( + dict(style="net", long_term=0, short_term=0, net=net) + | dict(carryover=co, distributions=dist) + ) + return cases + + +def capital_inputs(c): + inputs = { + "long_term_capital_loss_carryover": {YEAR: c["carryover"]}, + "non_sch_d_capital_gains": {YEAR: c["distributions"]}, + } + if c["style"] != "net": + inputs["long_term_capital_gains"] = {YEAR: c["long_term"]} + inputs["short_term_capital_gains"] = {YEAR: c["short_term"]} + if c["style"] != "sparse": + inputs["capital_gains"] = {YEAR: c["net"]} + return inputs + + +NO_CAPITAL = dict(style="parts", long_term=0, short_term=0, net=0) | dict( + carryover=0, distributions=0 +) + + +def build_situation(cases): + people, tax_units, marital_units, households = {}, {}, {}, {} + for i, c in enumerate(cases): + for arrangement in "ABC": + key = f"{arrangement}{i}" + claimant = f"claimant_{key}" + people[claimant] = { + "age": {YEAR: 70}, + "taxable_pension_income": {YEAR: PENSION}, + "real_estate_taxes": {YEAR: PROPERTY_TAX}, + # Set for everyone: an input for some people gives the others + # its default, not its formula. + "is_tax_unit_dependent": {YEAR: False}, + **capital_inputs(c if arrangement == "C" else NO_CAPITAL), + } + marital_units[f"marital_unit_{key}"] = {"members": [claimant]} + members = [claimant] + if arrangement == "C": + tax_units[f"tax_unit_{key}"] = {"members": [claimant]} + else: + member = f"member_{key}" + people[member] = { + "age": {YEAR: 30}, + "is_tax_unit_dependent": {YEAR: arrangement == "A"}, + **capital_inputs(c), + } + marital_units[f"marital_unit_{key}_member"] = {"members": [member]} + members.append(member) + if arrangement == "A": + tax_units[f"tax_unit_{key}"] = {"members": members} + else: + tax_units[f"tax_unit_{key}"] = {"members": [claimant]} + tax_units[f"tax_unit_{key}_member"] = {"members": [member]} + households[f"household_{key}"] = { + "members": members, + "state_code": {YEAR: "MT"}, + } + return { + "people": people, + "tax_units": tax_units, + "marital_units": marital_units, + "households": households, + } + + +def gains_without_losses(c): + """Schedule 2EC line 7 (line 17 for another member) plus distributions.""" + if c["style"] == "net": + gains = max(0, c["net"] + c["carryover"]) + else: + gains = max(0, c["long_term"] + c["carryover"]) + max(0, c["short_term"]) + return gains + c["distributions"] + + +@pytest.fixture(scope="module") +def results(): + cases = grid() + simulation = Simulation(situation=build_situation(cases)) + shape = (len(cases), 3) + income = np.asarray( + simulation.calculate( + "mt_elderly_homeowner_or_renter_credit_gross_household_income", + YEAR, + map_to="household", + ) + ).reshape(shape) + credit = np.asarray( + simulation.calculate( + "mt_elderly_homeowner_or_renter_credit", YEAR, map_to="household" + ) + ).reshape(shape) + return cases, income, credit + + +def tolerance(c): + scale = PENSION + sum( + abs(c[k]) for k in ["long_term", "short_term", "net", "carryover"] + ) + scale += c["distributions"] + return max(TOLERANCE, 8 * float(np.spacing(np.float32(scale)))) + + +def test_gains_count_without_losses_on_every_path(results): + cases, income, _ = results + for i, c in enumerate(cases): + expected = PENSION + gains_without_losses(c) + assert income[i] == pytest.approx([expected] * 3, abs=tolerance(c)), c + + +def test_credit_does_not_depend_on_how_the_member_files(results): + cases, _, credit = results + for i, c in enumerate(cases): + assert credit[i, 0] == pytest.approx(credit[i, 1], abs=tolerance(c)), c + + +def test_hand_checked_cases(results): + """Two grid cases are checked by hand. A long-term amount of 11,000 that + already nets a 15,000 carryover, with a 9,000 short-term gain and no net + supplied (sparse): the year's long-term gain is 26,000, so 20,000 + + 26,000 + 9,000 = 55,000, and the credit is 0 (55,000 is at least + 45,000). A net -2,500 supplied + directly with a 2,500 carryover: no gain, so 20,000; (20,000 - 12,600) x + 0.035 = 259, and min(2,000 - 259, 1,150) = 1,150.""" + cases, income, credit = results + + def find(**kw): + return next(i for i, c in enumerate(cases) if kw.items() <= c.items()) + + i = find( + style="sparse", + long_term=11_000, + short_term=9_000, + carryover=15_000, + distributions=0, + ) + assert income[i].tolist() == pytest.approx([55_000] * 3) + assert credit[i].tolist() == pytest.approx([0] * 3) + j = find(style="net", net=-2_500, carryover=2_500, distributions=0) + assert income[j].tolist() == pytest.approx([20_000] * 3) + assert credit[j].tolist() == pytest.approx([1_150] * 3, abs=0.01) diff --git a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.py b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.py index c6f1065035f..4f6ba36ed86 100644 --- a/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.py +++ b/policyengine_us/variables/gov/states/mt/tax/income/credits/mt_elderly_homeowner_or_renter/mt_elderly_homeowner_or_renter_credit_gross_household_income.py @@ -53,16 +53,20 @@ def formula(person, period, parameters): person("dependent_gross_income", period) - own_deductions ) # The income above counts only the taxable portion of Social - # Security: a filer's in federal AGI, a dependent's in their own - # gross income. Add the untaxed portion so all SS is counted per - # § 15-30-2337(9)(a)(viii). + # Security: a filer's in federal AGI (irs_gross_income counts people + # who are not dependents), a dependent's in their own gross income + # (dependent_gross_income counts people who are not the head or + # spouse). Subtract only the part one of them counted, then add the + # rest, so all SS is counted once per § 15-30-2337(9)(a)(viii) even + # when a person is input as both a dependent and the head. social_security = person("social_security", period) - taxable_social_security = add( - person, - period, - ["taxable_social_security", "dependent_taxable_social_security"], + head_or_spouse = person("is_tax_unit_head_or_spouse", period) + counted_social_security = where( + is_dependent, + ~head_or_spouse * person("dependent_taxable_social_security", period), + person("taxable_social_security", period), ) - untaxed_social_security = max_(social_security - taxable_social_security, 0) + untaxed_social_security = max_(social_security - counted_social_security, 0) # Income is federal AGI "without regard to loss" (§ 15-30-2337(9)(a)), # so the business and capital losses deducted in computing federal # AGI are added back (2023 Elderly Homeowner/Renter Credit Schedule, @@ -70,4 +74,59 @@ def formula(person, period, parameters): # losses."). The return's losses are added once, to the head. head = person("is_tax_unit_head", period) losses = head * person.tax_unit("loss_ald", period) - return sources + dependent_income + untaxed_social_security + losses + # Count each person's capital gains without losses, as 2024 Schedule + # 2EC line 7 requires ("do not include any capital losses"). Two + # losses reduce the gains the income paths count without reaching + # loss_ald, which holds only the capital loss deducted in AGI: + # - a short-term loss netted against the same person's long-term gain + # (or the reverse) inside capital_gains; + # - the prior-year long-term capital loss carryover, which + # long_term_capital_gains already includes as a loss, as Schedule D + # line 15 does. Adding it back gives the year's own long-term gain + # (as az_property_tax_credit_capital_gains does); only the gain it + # hid is restored, never the unused loss. + # The long-term and short-term parts, when either is nonzero, decide + # the gains: max(0, long-term + carryover) + max(0, short-term). + # capital_gains can disagree with them: once it is an input for + # anyone in a simulation, everyone without one holds its default of + # zero, not the sum of their parts. Without parts, capital_gains is a + # net amount supplied directly, and the gain is max(0, net + + # carryover); the carryover is never counted as a gain the parts did + # not show. + carryover = max_(0, person("long_term_capital_loss_carryover", period)) + long_term_capital_gains = person("long_term_capital_gains", period) + short_term_capital_gains = person("short_term_capital_gains", period) + short_term_gains = max_(0, short_term_capital_gains) + capital_gains = person("capital_gains", period) + parts_supplied = (long_term_capital_gains != 0) | ( + short_term_capital_gains != 0 + ) + gains_without_losses = where( + parts_supplied, + max_(0, long_term_capital_gains + carryover) + short_term_gains, + max_(0, capital_gains + carryover), + ) + # The gains each income path already counted: irs_gross_income + # floors the net, dependent_gross_income floors each part. The + # difference replaces them with the gains above; it is negative only + # when an inconsistent capital_gains input exceeds the parts. A person + # input as both a dependent and the head is counted by neither path, + # so nothing is restored for them. + counted_gains = where( + is_dependent, + ~head_or_spouse + * max_( + max_(0, long_term_capital_gains) + short_term_gains, + capital_gains, + ), + max_(0, capital_gains), + ) + counted_by_a_path = ~(is_dependent & head_or_spouse) + lost_capital_gains = counted_by_a_path * (gains_without_losses - counted_gains) + return ( + sources + + dependent_income + + untaxed_social_security + + losses + + lost_capital_gains + )