diff --git a/changelog.d/fix-vt-renter-credit-franklin-fmr.fixed.md b/changelog.d/fix-vt-renter-credit-franklin-fmr.fixed.md new file mode 100644 index 00000000000..02a957b058d --- /dev/null +++ b/changelog.d/fix-vt-renter-credit-franklin-fmr.fixed.md @@ -0,0 +1 @@ +Match the Vermont Department of Taxes' published renter credit base amounts for Franklin and Grand Isle in 2022, 2023 and 2025, and round both the base amount before the income phase-out and the final credit half up to whole dollars, including exact 50-cent ties. diff --git a/policyengine_us/parameters/gov/states/vt/tax/income/credits/renter/fair_market_rent.yaml b/policyengine_us/parameters/gov/states/vt/tax/income/credits/renter/fair_market_rent.yaml index c7509113b5c..6ddecf03a2c 100644 --- a/policyengine_us/parameters/gov/states/vt/tax/income/credits/renter/fair_market_rent.yaml +++ b/policyengine_us/parameters/gov/states/vt/tax/income/credits/renter/fair_market_rent.yaml @@ -9,10 +9,12 @@ metadata: - Household size label: Vermont fair market rent amount reference: - # The values are derived from the renter credit computation tables which represent 10% of the + # The values are derived from the renter credit computation tables which represent 10% of the # yearly fair market rent - title: 32 V.S.A. §6066. Homestead Property Tax Credit and Renter Credit (b)(A) href: https://law.justia.com/codes/vermont/2022/title-32/chapter-154/section-6066/ + - title: 32 V.S.A. § 6061(7)(C) + href: https://legislature.vermont.gov/statutes/section/32/154/06061 - title: Vermont Credit Amounts Based on HUD Data Based on Number of Bedrooms Website href: https://tax.vermont.gov/individuals/renter-credit/calculator-and-credit-amounts - title: 2025 Renter Credit Website Calculator @@ -21,10 +23,47 @@ metadata: href: https://tax.vermont.gov/sites/tax/files/documents/2024%20Renter%20Credit%20Website%20Calculator.xlsx - title: 2023 Renter Credit Website Calculator href: https://web.archive.org/web/20250526044955/https://tax.vermont.gov/document/2023-renter-credit-website-calculator + - title: 2023 Vermont Renter Credit Report, 2023 Credit Amounts (Based on HUD Fair Market Rents) + href: https://tax.vermont.gov/sites/tax/files/documents/renter_credit_2023.pdf#page=10 - title: 2022 Renter Credit Website Calculator - href: https://tax.vermont.gov/document/2022-property-tax-credit-calculator + href: https://tax.vermont.gov/sites/tax/files/documents/2022%20Renter%20Credit%20Website%20Calculator_1.xlsx + - title: 2022 Vermont Renter Credit Report, 2022 Credit Amounts (Based on HUD Fair Market Rents) + href: https://tax.vermont.gov/sites/tax/files/documents/renter_credit_2022.pdf#page=10 + - title: 2021 Renter Credit Website Calculator + href: https://tax.vermont.gov/sites/tax/files/documents/2021%20Renter%20Credit%20Website%20Calculator_8.xlsx - title: 2021 Vermont Credit Amounts Based on HUD Data Based on Number of Bedrooms href: https://tax.vermont.gov/sites/tax/files/documents/renter_credit_2021.pdf#page=10 + - title: HUD FY2025 Fair Market Rents + href: https://www.huduser.gov/portal/datasets/fmr/fmr2025/FY25_FMRs.xlsx + - title: HUD FY2024 Fair Market Rents + href: https://www.huduser.gov/portal/datasets/fmr/fmr2024/FY24_FMRs.xlsx + - title: HUD FY2023 Fair Market Rents + href: https://www.huduser.gov/portal/datasets/fmr/fmr2023/FY23_FMRs.xlsx + - title: HUD FY2022 Fair Market Rents + href: https://www.huduser.gov/portal/datasets/fmr/fmr2022/FY22_FMRs.xlsx + - title: HUD FY2021 Fair Market Rents + href: https://www.huduser.gov/portal/datasets/fmr/fmr2021/FY21_4050_FMRs_rev.xlsx +# Each county's value is HUD's fair market rent for the fiscal year that includes +# June 30 of the taxable year (FY2025 for 2025), for the county's HUD area, with +# household size n using the min(n, 4)-bedroom rent. Chittenden's 2,083 is the +# rent at which 10% x 12 reaches the $2,500 maximum the department applies to +# the credit amount (2,500 / 1.2 = 2,083.33). +# +# Franklin and Grand Isle use the average of the fair market rents for the State +# (32 V.S.A. § 6061(7)(C)). The following historical reconstructions use Vermont's +# 12 HUD fair market rent areas (the 11 nonmetropolitan counties and the +# Burlington-South Burlington MSA, which also holds Franklin and Grand Isle). +# They reproduce the published credit amounts; they are not department rules. +# The 2022-2025 calculators hard-code their credit tables, so they do not establish +# the averaging methods used to obtain those amounts: +# - 2021 and 2024: the average rounded to whole dollars (the 2021 calculator's +# fair market rent table holds 810, 1,000, 1,291 and 1,419). +# - 2022 reconstruction: the average of 11 areas, without Addison County +# (all 12 give 1,010, 1,251, 1,611 and 1,777, not the published 1,006, +# 1,242, 1,602 and 1,773). +# - 2023 reconstruction: the average with the Burlington four-bedroom rent +# (2,170) held to 2,083.33 (all 12 unadjusted give 1,913, not the published 1,904). +# - 2025: the unrounded average (1,057.75 x 1.2 = 1,269.3, published 1,269). 1: ADDISON_COUNTY_VT: 2021-01-01: 846 @@ -58,16 +97,16 @@ metadata: 2025-01-01: 854 FRANKLIN_COUNTY_VT: 2021-01-01: 810 - 2022-01-01: 838 - 2023-01-01: 919 + 2022-01-01: 838.36 + 2023-01-01: 919.5 2024-01-01: 1_045 - 2025-01-01: 1_058 + 2025-01-01: 1_057.75 GRAND_ISLE_COUNTY_VT: 2021-01-01: 810 - 2022-01-01: 838 - 2023-01-01: 919 + 2022-01-01: 838.36 + 2023-01-01: 919.5 2024-01-01: 1_045 - 2025-01-01: 1_058 + 2025-01-01: 1_057.75 LAMOILLE_COUNTY_VT: 2021-01-01: 793 2022-01-01: 806 @@ -143,14 +182,14 @@ metadata: 2025-01-01: 946 FRANKLIN_COUNTY_VT: 2021-01-01: 1_000 - 2022-01-01: 1_035 - 2023-01-01: 1_132 + 2022-01-01: 1_035.36 + 2023-01-01: 1_131.83 2024-01-01: 1_283 2025-01-01: 1_298 GRAND_ISLE_COUNTY_VT: 2021-01-01: 1_000 - 2022-01-01: 1_035 - 2023-01-01: 1_132 + 2022-01-01: 1_035.36 + 2023-01-01: 1_131.83 2024-01-01: 1_283 2025-01-01: 1_298 LAMOILLE_COUNTY_VT: @@ -228,16 +267,16 @@ metadata: 2025-01-01: 1_326 FRANKLIN_COUNTY_VT: 2021-01-01: 1_291 - 2022-01-01: 1_335 - 2023-01-01: 1_458 + 2022-01-01: 1_334.91 + 2023-01-01: 1_457.83 2024-01-01: 1_663 - 2025-01-01: 1_688 + 2025-01-01: 1_687.17 GRAND_ISLE_COUNTY_VT: 2021-01-01: 1_291 - 2022-01-01: 1_335 - 2023-01-01: 1_458 + 2022-01-01: 1_334.91 + 2023-01-01: 1_457.83 2024-01-01: 1_663 - 2025-01-01: 1_688 + 2025-01-01: 1_687.17 LAMOILLE_COUNTY_VT: 2021-01-01: 1_264 2022-01-01: 1_343 @@ -313,16 +352,16 @@ metadata: 2025-01-01: 1_351 FRANKLIN_COUNTY_VT: 2021-01-01: 1_419 - 2022-01-01: 1_478 - 2023-01-01: 1_587 + 2022-01-01: 1_477.91 + 2023-01-01: 1_586.94 2024-01-01: 1_800 - 2025-01-01: 1_829 + 2025-01-01: 1_828.75 GRAND_ISLE_COUNTY_VT: 2021-01-01: 1_419 - 2022-01-01: 1_478 - 2023-01-01: 1_587 + 2022-01-01: 1_477.91 + 2023-01-01: 1_586.94 2024-01-01: 1_800 - 2025-01-01: 1_829 + 2025-01-01: 1_828.75 LAMOILLE_COUNTY_VT: 2021-01-01: 1_378 2022-01-01: 1_439 @@ -398,16 +437,16 @@ metadata: 2025-01-01: 1_351 FRANKLIN_COUNTY_VT: 2021-01-01: 1_419 - 2022-01-01: 1_478 - 2023-01-01: 1_587 + 2022-01-01: 1_477.91 + 2023-01-01: 1_586.94 2024-01-01: 1_800 - 2025-01-01: 1_829 + 2025-01-01: 1_828.75 GRAND_ISLE_COUNTY_VT: 2021-01-01: 1_419 - 2022-01-01: 1_478 - 2023-01-01: 1_587 + 2022-01-01: 1_477.91 + 2023-01-01: 1_586.94 2024-01-01: 1_800 - 2025-01-01: 1_829 + 2025-01-01: 1_828.75 LAMOILLE_COUNTY_VT: 2021-01-01: 1_378 2022-01-01: 1_439 @@ -483,16 +522,16 @@ metadata: 2025-01-01: 1_351 FRANKLIN_COUNTY_VT: 2021-01-01: 1_419 - 2022-01-01: 1_478 - 2023-01-01: 1_587 + 2022-01-01: 1_477.91 + 2023-01-01: 1_586.94 2024-01-01: 1_800 - 2025-01-01: 1_829 + 2025-01-01: 1_828.75 GRAND_ISLE_COUNTY_VT: 2021-01-01: 1_419 - 2022-01-01: 1_478 - 2023-01-01: 1_587 + 2022-01-01: 1_477.91 + 2023-01-01: 1_586.94 2024-01-01: 1_800 - 2025-01-01: 1_829 + 2025-01-01: 1_828.75 LAMOILLE_COUNTY_VT: 2021-01-01: 1_378 2022-01-01: 1_439 @@ -568,16 +607,16 @@ metadata: 2025-01-01: 1_351 FRANKLIN_COUNTY_VT: 2021-01-01: 1_419 - 2022-01-01: 1_478 - 2023-01-01: 1_587 + 2022-01-01: 1_477.91 + 2023-01-01: 1_586.94 2024-01-01: 1_800 - 2025-01-01: 1_829 + 2025-01-01: 1_828.75 GRAND_ISLE_COUNTY_VT: 2021-01-01: 1_419 - 2022-01-01: 1_478 - 2023-01-01: 1_587 + 2022-01-01: 1_477.91 + 2023-01-01: 1_586.94 2024-01-01: 1_800 - 2025-01-01: 1_829 + 2025-01-01: 1_828.75 LAMOILLE_COUNTY_VT: 2021-01-01: 1_378 2022-01-01: 1_439 @@ -653,16 +692,16 @@ metadata: 2025-01-01: 1_351 FRANKLIN_COUNTY_VT: 2021-01-01: 1_419 - 2022-01-01: 1_478 - 2023-01-01: 1_587 + 2022-01-01: 1_477.91 + 2023-01-01: 1_586.94 2024-01-01: 1_800 - 2025-01-01: 1_829 + 2025-01-01: 1_828.75 GRAND_ISLE_COUNTY_VT: 2021-01-01: 1_419 - 2022-01-01: 1_478 - 2023-01-01: 1_587 + 2022-01-01: 1_477.91 + 2023-01-01: 1_586.94 2024-01-01: 1_800 - 2025-01-01: 1_829 + 2025-01-01: 1_828.75 LAMOILLE_COUNTY_VT: 2021-01-01: 1_378 2022-01-01: 1_439 diff --git a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/rent/test_vt_renter_credit_amounts.py b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/rent/test_vt_renter_credit_amounts.py new file mode 100644 index 00000000000..dd1b27f4aa4 --- /dev/null +++ b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/rent/test_vt_renter_credit_amounts.py @@ -0,0 +1,263 @@ +"""Check Vermont's renter credit against the Department of Taxes' credit tables. + +32 V.S.A. § 6066(b)(1) bases the renter credit on 10% of fair market rent, which +§ 6061(7)(C) defines as HUD's fair market rent for the claimant's area, or for +Franklin and Grand Isle the average of the fair market rents for the State. The +department publishes the whole-dollar credit amounts by county and household +size in each year's Renter Credit Website Calculator ("Base Credit Amounts" +sheet; the 2021-2023 renter credit reports print the same tables), and its +calculator applies the income phase-out to that whole-dollar amount: +ROUND(Base_Rebate_Amount * Percent_Rebate_Claimable, 0). + +The tests compute vt_renter_credit for every county, household size 1-8 and +year 2021-2025 (12 months rented, not shared, not subsidized) and check: +- below the extremely low-income limit, the credit equals the table; +- at every other income, the credit equals the department's formula applied + to the table, and it never rises with income. + +Final credits round half up, including exact 50-cent ties: Calculator!G16 in +2021-2022 and Calculator!G17 in 2023-2025 use ROUND(..., 0). +""" + +from fractions import Fraction + +import numpy as np +import pytest + +from policyengine_us import Simulation +from policyengine_us.system import system + +# Hypothesis is a dev extra; skip rather than fail collection without it. +hypothesis = pytest.importorskip("hypothesis") +st = pytest.importorskip("hypothesis.strategies") + +YEARS = range(2021, 2026) +SIZES = range(1, 9) +LIMITS = system.parameters.gov.states.vt.tax.income.credits.renter.income_limit_ami + +# Department credit amounts for household sizes 1-4; sizes 5-8 repeat size 4. +# 2021: 2021 Renter Credit Website Calculator (_8); 2022: 2022 calculator (_1); +# 2023-2025: each year's calculator. Read from the workbooks, not retyped. +DEPARTMENT_CREDIT_AMOUNTS = { + 2021: { + "ADDISON_COUNTY_VT": (1_015, 1_270, 1_586, 1_722), + "BENNINGTON_COUNTY_VT": (983, 1_128, 1_524, 1_530), + "CALEDONIA_COUNTY_VT": (887, 1_010, 1_256, 1_717), + "CHITTENDEN_COUNTY_VT": (1_518, 1_954, 2_430, 2_500), + "ESSEX_COUNTY_VT": (700, 889, 1_182, 1_231), + "FRANKLIN_COUNTY_VT": (972, 1_200, 1_549, 1_703), + "GRAND_ISLE_COUNTY_VT": (972, 1_200, 1_549, 1_703), + "LAMOILLE_COUNTY_VT": (952, 1_219, 1_517, 1_654), + "ORANGE_COUNTY_VT": (894, 1_178, 1_500, 1_622), + "ORLEANS_COUNTY_VT": (826, 941, 1_279, 1_466), + "RUTLAND_COUNTY_VT": (935, 1_102, 1_502, 1_654), + "WASHINGTON_COUNTY_VT": (989, 1_303, 1_630, 1_838), + "WINDHAM_COUNTY_VT": (958, 1_192, 1_482, 1_616), + "WINDSOR_COUNTY_VT": (1_004, 1_208, 1_699, 1_729), + }, + 2022: { + "ADDISON_COUNTY_VT": (1_054, 1_340, 1_715, 1_818), + "BENNINGTON_COUNTY_VT": (986, 1_222, 1_604, 1_656), + "CALEDONIA_COUNTY_VT": (924, 1_052, 1_301, 1_801), + "CHITTENDEN_COUNTY_VT": (1_396, 1_800, 2_225, 2_441), + "ESSEX_COUNTY_VT": (785, 937, 1_238, 1_302), + "FRANKLIN_COUNTY_VT": (1_006, 1_242, 1_602, 1_773), + "GRAND_ISLE_COUNTY_VT": (1_006, 1_242, 1_602, 1_773), + "LAMOILLE_COUNTY_VT": (967, 1_273, 1_612, 1_727), + "ORANGE_COUNTY_VT": (952, 1_252, 1_565, 1_723), + "ORLEANS_COUNTY_VT": (896, 1_021, 1_358, 1_493), + "RUTLAND_COUNTY_VT": (1_001, 1_177, 1_613, 1_944), + "WASHINGTON_COUNTY_VT": (1_043, 1_373, 1_745, 1_861), + "WINDHAM_COUNTY_VT": (1_048, 1_307, 1_615, 1_772), + "WINDSOR_COUNTY_VT": (1_069, 1_253, 1_745, 1_788), + }, + 2023: { + "ADDISON_COUNTY_VT": (1_136, 1_430, 1_859, 1_921), + "BENNINGTON_COUNTY_VT": (1_063, 1_344, 1_746, 1_894), + "CALEDONIA_COUNTY_VT": (1_014, 1_145, 1_400, 1_950), + "CHITTENDEN_COUNTY_VT": (1_486, 1_938, 2_378, 2_500), + "ESSEX_COUNTY_VT": (895, 1_012, 1_372, 1_403), + "FRANKLIN_COUNTY_VT": (1_103, 1_358, 1_749, 1_904), + "GRAND_ISLE_COUNTY_VT": (1_103, 1_358, 1_749, 1_904), + "LAMOILLE_COUNTY_VT": (1_056, 1_390, 1_853, 1_867), + "ORANGE_COUNTY_VT": (1_028, 1_350, 1_712, 1_813), + "ORLEANS_COUNTY_VT": (1_013, 1_144, 1_489, 1_582), + "RUTLAND_COUNTY_VT": (1_093, 1_306, 1_728, 2_190), + "WASHINGTON_COUNTY_VT": (1_144, 1_506, 1_901, 2_028), + "WINDHAM_COUNTY_VT": (1_134, 1_380, 1_711, 1_854), + "WINDSOR_COUNTY_VT": (1_178, 1_355, 1_843, 1_850), + }, + 2024: { + "ADDISON_COUNTY_VT": (1_274, 1_598, 2_089, 2_129), + "BENNINGTON_COUNTY_VT": (1_207, 1_489, 1_970, 2_315), + "CALEDONIA_COUNTY_VT": (1_106, 1_282, 1_668, 1_988), + "CHITTENDEN_COUNTY_VT": (1_729, 2_264, 2_500, 2_500), + "ESSEX_COUNTY_VT": (1_001, 1_123, 1_561, 1_583), + "FRANKLIN_COUNTY_VT": (1_254, 1_540, 1_996, 2_160), + "GRAND_ISLE_COUNTY_VT": (1_254, 1_540, 1_996, 2_160), + "LAMOILLE_COUNTY_VT": (1_180, 1_549, 2_056, 2_063), + "ORANGE_COUNTY_VT": (1_204, 1_493, 1_921, 2_005), + "ORLEANS_COUNTY_VT": (1_156, 1_297, 1_681, 1_829), + "RUTLAND_COUNTY_VT": (1_196, 1_469, 1_873, 2_405), + "WASHINGTON_COUNTY_VT": (1_327, 1_744, 2_198, 2_370), + "WINDHAM_COUNTY_VT": (1_309, 1_608, 1_986, 2_142), + "WINDSOR_COUNTY_VT": (1_363, 1_560, 2_070, 2_077), + }, + 2025: { + "ADDISON_COUNTY_VT": (1_324, 1_692, 2_112, 2_394), + "BENNINGTON_COUNTY_VT": (1_283, 1_470, 2_051, 2_383), + "CALEDONIA_COUNTY_VT": (1_091, 1_310, 1_765, 1_873), + "CHITTENDEN_COUNTY_VT": (1_771, 2_323, 2_500, 2_500), + "ESSEX_COUNTY_VT": (1_025, 1_135, 1_591, 1_621), + "FRANKLIN_COUNTY_VT": (1_269, 1_558, 2_025, 2_195), + "GRAND_ISLE_COUNTY_VT": (1_269, 1_558, 2_025, 2_195), + "LAMOILLE_COUNTY_VT": (1_169, 1_534, 2_064, 2_071), + "ORANGE_COUNTY_VT": (1_241, 1_511, 1_976, 2_072), + "ORLEANS_COUNTY_VT": (1_146, 1_319, 1_717, 1_894), + "RUTLAND_COUNTY_VT": (1_198, 1_508, 1_861, 2_330), + "WASHINGTON_COUNTY_VT": (1_298, 1_703, 2_116, 2_372), + "WINDHAM_COUNTY_VT": (1_334, 1_625, 2_022, 2_154), + "WINDSOR_COUNTY_VT": (1_352, 1_561, 2_030, 2_088), + }, +} +COUNTIES = sorted(DEPARTMENT_CREDIT_AMOUNTS[2025]) + + +def _department_amount(year, county, size): + return DEPARTMENT_CREDIT_AMOUNTS[year][county][min(size, 4) - 1] + + +def _limits(year, county, size): + instant = f"{year}-01-01" + full = LIMITS.thirty_percent.children[str(size)].children[county](instant) + partial = LIMITS.fifty_percent.children[str(size)].children[county](instant) + return int(full), int(partial) + + +def _department_credit(amount, income, full, partial): + """The calculator's credit, in exact arithmetic, before its final ROUND.""" + if income > partial: + return Fraction(0) + if income <= full: + return Fraction(amount) + return Fraction(amount) * Fraction(partial - income, partial - full) + + +def _calculate(year, households): + """vt_renter_credit for single-person households given as + (county, tax unit size, renter credit income).""" + people, tax_units, spm_units, households_out = {}, {}, {}, {} + for i, (county, size, income) in enumerate(households): + people[f"person_{i}"] = {"age": {year: 40}, "rent": {year: 0}} + tax_units[f"tax_unit_{i}"] = { + "members": [f"person_{i}"], + "tax_unit_size": {year: size}, + "vt_renter_credit_income": {year: income}, + "rent_is_shared_with_another_tax_unit": {year: False}, + } + spm_units[f"spm_unit_{i}"] = { + "members": [f"person_{i}"], + "housing_assistance": {year: 0}, + } + households_out[f"household_{i}"] = { + "members": [f"person_{i}"], + "state_code": {year: "VT"}, + "county": {year: county}, + } + simulation = Simulation( + situation={ + "people": people, + "tax_units": tax_units, + "spm_units": spm_units, + "households": households_out, + } + ) + return np.asarray(simulation.calculate("vt_renter_credit", year)) + + +def test_table_covers_every_county_size_and_year(): + assert sorted(DEPARTMENT_CREDIT_AMOUNTS) == list(YEARS) + for year in YEARS: + assert sorted(DEPARTMENT_CREDIT_AMOUNTS[year]) == COUNTIES + assert len(COUNTIES) == 14 + # Franklin and Grand Isle share the state average. + assert ( + DEPARTMENT_CREDIT_AMOUNTS[year]["FRANKLIN_COUNTY_VT"] + == DEPARTMENT_CREDIT_AMOUNTS[year]["GRAND_ISLE_COUNTY_VT"] + ) + + +@pytest.mark.parametrize("year", YEARS) +def test_full_credit_equals_department_table(year): + households = [(county, size, 0) for county in COUNTIES for size in SIZES] + credits = _calculate(year, households) + mismatches = [ + (county, size, credit, _department_amount(year, county, size)) + for (county, size, _), credit in zip(households, credits) + if credit != _department_amount(year, county, size) + ] + assert mismatches == [] + + +def _incomes(full, partial): + """Both limits, a dollar either side, and 19 incomes in between. + + The $37 offset varies the fractional phase-out amounts sampled by the grid. + Explicit tie examples are covered in vt_renter_credit.yaml. + """ + step = Fraction(partial - full, 20) + inside = [int(full + k * step) + 37 for k in range(1, 20)] + return [full - 1, full, full + 1, *inside, partial - 1, partial, partial + 1] + + +@pytest.mark.parametrize("year", YEARS) +def test_partial_credit_follows_department_formula(year): + households, expected = [], [] + for county in COUNTIES: + for size in SIZES: + amount = _department_amount(year, county, size) + full, partial = _limits(year, county, size) + for income in _incomes(full, partial): + credit = _department_credit(amount, income, full, partial) + households.append((county, size, income)) + expected.append(int(credit + Fraction(1, 2))) + credits = _calculate(year, households) + mismatches = [ + (household, credit, want) + for household, credit, want in zip(households, credits, expected) + if credit != want + ] + assert mismatches == [] + # The credit never rises with income for a given county and size. + by_unit = {} + for (county, size, income), credit in zip(households, credits): + by_unit.setdefault((county, size), []).append((income, credit)) + for points in by_unit.values(): + amounts = [credit for _, credit in sorted(points)] + assert all(a >= b for a, b in zip(amounts, amounts[1:])) + + +@hypothesis.settings(max_examples=20, deadline=None) +@hypothesis.given( + year=st.sampled_from(list(YEARS)), + draws=st.lists( + st.tuples( + st.sampled_from(COUNTIES), + st.integers(min_value=1, max_value=8), + st.floats(min_value=0, max_value=1.2), + ), + min_size=1, + max_size=40, + ), +) +def test_random_incomes_follow_department_formula(year, draws): + households, expected = [], [] + for county, size, share in draws: + full, partial = _limits(year, county, size) + income = int(share * partial) + credit = _department_credit( + _department_amount(year, county, size), income, full, partial + ) + households.append((county, size, income)) + expected.append(int(credit + Fraction(1, 2))) + assert list(_calculate(year, households)) == expected diff --git a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/rent/vt_renter_credit.yaml b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/rent/vt_renter_credit.yaml index bb540781e29..1e8a1245a2a 100644 --- a/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/rent/vt_renter_credit.yaml +++ b/policyengine_us/tests/policy/baseline/gov/states/vt/tax/income/credits/rent/vt_renter_credit.yaml @@ -145,3 +145,210 @@ state_code: VT output: vt_renter_credit: 1_692 + +# Franklin and Grand Isle use the state-average fair market rent (32 V.S.A. +# 6061(7)(C)). Expected values are the department's Renter Credit Website +# Calculators (2022, 2023, 2025), recalculated in LibreOffice for each household: +# 12 months rented, not shared, not subsidized. +- name: Franklin, 4 people, 2022, full credit at the state-average fair market rent + period: 2022 + input: + vt_renter_credit_income: 5_000 + county: FRANKLIN_COUNTY_VT + tax_unit_size: 4 + rent_is_shared_with_another_tax_unit: false + housing_assistance: 0 + rent: 0 + state_code: VT + output: + # 10% x 12 x 1,477.91 = 1,773.49; the whole-dollar 1,478 gave 1,774. + vt_renter_credit: 1_773 + +- name: Grand Isle, 3 people, 2023, full credit at the state-average fair market rent + period: 2023 + input: + vt_renter_credit_income: 5_000 + county: GRAND_ISLE_COUNTY_VT + tax_unit_size: 3 + rent_is_shared_with_another_tax_unit: false + housing_assistance: 0 + rent: 0 + state_code: VT + output: + # 10% x 12 x 1,457.83 = 1,749.40; the whole-dollar 1,458 gave 1,750. + vt_renter_credit: 1_749 + +- name: Franklin, 1 person, 2025, full credit at the state-average fair market rent + period: 2025 + input: + vt_renter_credit_income: 5_000 + county: FRANKLIN_COUNTY_VT + tax_unit_size: 1 + rent_is_shared_with_another_tax_unit: false + housing_assistance: 0 + rent: 0 + state_code: VT + output: + # 10% x 12 x 1,057.75 = 1,269.30; the whole-dollar 1,058 gave 1,270. + vt_renter_credit: 1_269 + +- name: Grand Isle, 3 people, 2025, full credit at the state-average fair market rent + period: 2025 + input: + vt_renter_credit_income: 5_000 + county: GRAND_ISLE_COUNTY_VT + tax_unit_size: 3 + rent_is_shared_with_another_tax_unit: false + housing_assistance: 0 + rent: 0 + state_code: VT + output: + # 10% x 12 x 1,687.17 = 2,024.60; the whole-dollar 1,688 gave 2,026. + vt_renter_credit: 2_025 + +- name: Franklin, 6 people, 2025, full credit with the credit amount rounded half up + period: 2025 + input: + vt_renter_credit_income: 5_000 + county: FRANKLIN_COUNTY_VT + tax_unit_size: 6 + rent_is_shared_with_another_tax_unit: false + housing_assistance: 0 + rent: 0 + state_code: VT + output: + # 10% x 12 x 1,828.75 = 2,194.50 exactly, which the department rounds up. + vt_renter_credit: 2_195 + +- name: Franklin, 1 person, 2025, partial credit from the whole-dollar credit amount + period: 2025 + input: + vt_renter_credit_income: 31_090 + county: FRANKLIN_COUNTY_VT + tax_unit_size: 1 + rent_is_shared_with_another_tax_unit: false + housing_assistance: 0 + rent: 0 + state_code: VT + output: + # (49,340 - 31,090) / (49,340 - 22,790) x 1,269 = 872.29. + vt_renter_credit: 872 + +- name: Grand Isle, 3 people, 2025, partial credit from the whole-dollar credit amount + period: 2025 + input: + vt_renter_credit_income: 39_980 + county: GRAND_ISLE_COUNTY_VT + tax_unit_size: 3 + rent_is_shared_with_another_tax_unit: false + housing_assistance: 0 + rent: 0 + state_code: VT + output: + # (63,410 - 39,980) / (63,410 - 29,280) x 2,025 = 1,390.15. + vt_renter_credit: 1_390 + +- name: Grand Isle, 3 people, 2023, partial credit from the whole-dollar credit amount + period: 2023 + input: + vt_renter_credit_income: 30_740 + county: GRAND_ISLE_COUNTY_VT + tax_unit_size: 3 + rent_is_shared_with_another_tax_unit: false + housing_assistance: 0 + rent: 0 + state_code: VT + output: + # (42,350 - 30,740) / (42,350 - 25,640) x 1,749 = 1,215.19. + vt_renter_credit: 1_215 + +- name: Franklin, 5 people, 2022, partial credit from the whole-dollar credit amount + period: 2022 + input: + vt_renter_credit_income: 37_560 + county: FRANKLIN_COUNTY_VT + tax_unit_size: 5 + rent_is_shared_with_another_tax_unit: false + housing_assistance: 0 + rent: 0 + state_code: VT + output: + # (47,990 - 37,560) / (47,990 - 32,660) x 1,773 = 1,206.29. + vt_renter_credit: 1_206 + +- name: Addison, 1 person, 2025, partial credit from the whole-dollar credit amount + period: 2025 + input: + vt_renter_credit_income: 33_000 + county: ADDISON_COUNTY_VT + tax_unit_size: 1 + rent_is_shared_with_another_tax_unit: false + housing_assistance: 0 + rent: 0 + state_code: VT + output: + # 10% x 12 x 1,103 = 1,323.60, which the department rounds to 1,324 before + # the phase-out: (52,850 - 33,000) / (52,850 - 24,400) x 1,324 = 923.78. + # Applying the phase-out to the unrounded 1,323.60 gave 923. + vt_renter_credit: 924 + +# The 2025 department calculator's Calculator!G17 uses ROUND(..., 0). +# Exact 50-cent ties round up for these full-year, unshared, unsubsidized claims: +# https://tax.vermont.gov/sites/tax/files/documents/2025%20Renter%20Credit%20Website%20Calculator.xlsx +- name: Franklin, 1 person, 2025, final partial-credit tie rounds half up + period: 2025 + input: + vt_renter_credit_income: 36_065 + county: FRANKLIN_COUNTY_VT + tax_unit_size: 1 + rent_is_shared_with_another_tax_unit: false + housing_assistance: 0 + rent: 0 + state_code: VT + output: + # (49,340 - 36,065) / (49,340 - 22,790) x 1,269 = 634.50. + vt_renter_credit: 635 + +- name: Lamoille, 1 person, 2025, final partial-credit tie rounds half up + period: 2025 + input: + vt_renter_credit_income: 34_495 + county: LAMOILLE_COUNTY_VT + tax_unit_size: 1 + rent_is_shared_with_another_tax_unit: false + housing_assistance: 0 + rent: 0 + state_code: VT + output: + # (47,190 - 34,495) / (47,190 - 21,800) x 1,169 = 584.50. + vt_renter_credit: 585 + +- name: Addison, 1 person, 2027, full-credit base tie rounds half up + period: 2027 + input: + vt_renter_credit_income: 5_000 + county: ADDISON_COUNTY_VT + tax_unit_size: 1 + rent_is_shared_with_another_tax_unit: false + housing_assistance: 0 + rent: 0 + state_code: VT + output: + # 1,103 x 12 x the temporary 12.5% rate = 1,654.50. + vt_renter_credit: 1_655 + +# The 2022 calculator's Calculator!G16 rounds this non-midpoint tie up: +# https://tax.vermont.gov/sites/tax/files/documents/2022%20Renter%20Credit%20Website%20Calculator_1.xlsx +- name: Caledonia, 1 person, 2022, final tie preserves precision during phase-out + period: 2022 + input: + vt_renter_credit_income: 23_500 + county: CALEDONIA_COUNTY_VT + tax_unit_size: 1 + rent_is_shared_with_another_tax_unit: false + housing_assistance: 0 + rent: 0 + state_code: VT + output: + # 924 x (30,000 - 23,500) / (30,000 - 18,000) = 500.50. + vt_renter_credit: 501 diff --git a/policyengine_us/variables/gov/states/vt/tax/income/credits/renter/vt_renter_credit.py b/policyengine_us/variables/gov/states/vt/tax/income/credits/renter/vt_renter_credit.py index c8fd1ec7368..6284d0a2187 100644 --- a/policyengine_us/variables/gov/states/vt/tax/income/credits/renter/vt_renter_credit.py +++ b/policyengine_us/variables/gov/states/vt/tax/income/credits/renter/vt_renter_credit.py @@ -28,13 +28,21 @@ def formula(tax_unit, period, parameters): county ] fmr = p.fair_market_rent[tax_unit_size][county] - base_credit_amount = fmr * MONTHS_IN_YEAR * p.fmr_rate + # The department rounds the credit amount to whole dollars, half up, + # before applying the income phase-out: its 2021 calculator computes + # ROUND(FMR * 12 * 10%, 0), and its published tables are whole dollars. + base_credit_amount = np.floor(fmr * MONTHS_IN_YEAR * p.fmr_rate + 0.5) # Compute what the spreadsheet calls the "percent rebate claimable" vt_renter_credit_income = tax_unit("vt_renter_credit_income", period) income_diff = partial_credit_income_limit - vt_renter_credit_income income_threshold_diff = partial_credit_income_limit - full_credit_income_limit percent_rebate_claimable = income_diff / income_threshold_diff + # Multiply before dividing so whole-dollar inputs preserve exact + # half-dollar credits instead of floating-point values just below them. + partial_credit_unsubsidized = ( + income_diff * base_credit_amount / income_threshold_diff + ) # If shared residence, reduce by a given fraction. shared_rent = tax_unit("rent_is_shared_with_another_tax_unit", period) shared_residence_reduction = shared_rent * p.shared_residence_reduction @@ -62,7 +70,7 @@ def formula(tax_unit, period, parameters): [ base_credit_amount, base_credit_subsidized, - percent_rebate_claimable * base_credit_amount, + partial_credit_unsubsidized, percent_rebate_claimable * base_credit_subsidized, ], default=0, @@ -71,4 +79,7 @@ def formula(tax_unit, period, parameters): # 6066(b)(1)); 2026 Vt. Act 169 raises this for claim year 2027 only. capped_credit = min_(credit_value, p.max_credit) unrounded = capped_credit * (1 - shared_residence_reduction) - return np.round(unrounded) + # The department calculators use ROUND(..., 0): Calculator!G16 in + # 2021-2022 and Calculator!G17 in 2023-2025. Credits are nonnegative, + # so floor(x + 0.5) matches their half-up rounding at exact ties. + return np.floor(unrounded + 0.5)