use super::{is_age_reached, walk_brackets};
use crate::params::{FederalTaxSubtraction, PerStatus, Source, StateParams};
use crate::plan::{Dollars, FilingStatus, PlanDate};
const SOURCES: usize = Source::ALL.len();
const ADDITION_AGE: f64 = 65.0;
#[derive(Debug, Clone, PartialEq)]
pub struct PersonIncome {
birth: PlanDate,
by_source: [Dollars; SOURCES],
}
impl PersonIncome {
#[must_use]
pub fn new(birth: PlanDate) -> Self {
Self {
birth,
by_source: [0; SOURCES],
}
}
pub fn add(&mut self, source: Source, amount: Dollars) {
self.by_source[source as usize] += amount;
}
#[must_use]
pub fn total(&self) -> Dollars {
self.by_source.iter().sum()
}
}
#[derive(Debug)]
pub struct StateIncome<'a> {
pub year: i16,
pub people: &'a [PersonIncome],
pub gains: Dollars,
pub taxable_social_security: Dollars,
pub deferred: Dollars,
pub federal_tax: Dollars,
pub agi: Dollars,
}
#[must_use]
pub fn state_tax(state: &StateParams, status: FilingStatus, income: &StateIncome) -> Dollars {
let people = income.people.iter();
let ordinary: Dollars = people
.map(|person| taxed_of(state, person, income.year))
.sum();
let deferred = if state.taxes_deferrals {
0
} else {
income.deferred
};
let benefits = if state.taxes_social_security {
income.taxable_social_security
} else {
0
};
let taxable = ordinary + income.gains - deferred + benefits
- deduction_of(state, status, income)
- subtracted_of(state, status, income);
let walked = walk_brackets(state.brackets.for_status(status), taxable);
(walked - credit_of(state, status, income)).max(0) + excise_of(state, income.gains)
}
fn deduction_of(state: &StateParams, status: FilingStatus, income: &StateIncome) -> Dollars {
if is_over(state.deduction_until_agi, status, income.agi) {
return 0;
}
state.deduction.get(status) + added_at_65(state.deduction_at_65.get(status), income)
}
fn added_at_65(each: Dollars, income: &StateIncome) -> Dollars {
if each == 0 {
return 0;
}
let people = income.people.iter();
let aged = people.filter(|person| is_age_reached(person.birth, ADDITION_AGE, income.year));
each * aged.count() as Dollars
}
fn is_over(until_agi: Option<PerStatus<Dollars>>, status: FilingStatus, agi: Dollars) -> bool {
until_agi.is_some_and(|limit| agi > limit.get(status))
}
fn subtracted_of(state: &StateParams, status: FilingStatus, income: &StateIncome) -> Dollars {
let subtraction = state.federal_tax_subtraction.as_ref();
subtraction.map_or(0, |subtraction| {
income
.federal_tax
.min(cap_at(subtraction, status, income.agi))
})
}
fn cap_at(subtraction: &FederalTaxSubtraction, status: FilingStatus, agi: Dollars) -> Dollars {
let band = subtraction.phase_out.get(status);
if agi < band.from {
return subtraction.cap;
}
let steps = Dollars::from(subtraction.steps.get());
let width = (band.to - band.from).max(1);
let lost = (1 + (agi - band.from) * (steps - 1) / width).min(steps);
subtraction.cap * (steps - lost) / steps
}
fn credit_of(state: &StateParams, status: FilingStatus, income: &StateIncome) -> Dollars {
let Some(credit) = &state.exemption_credit else {
return 0;
};
if is_over(credit.until_agi, status, income.agi) {
return 0;
}
credit.per_person * income.people.len() as Dollars + added_at_65(credit.at_65, income)
}
fn excise_of(state: &StateParams, gains: Dollars) -> Dollars {
let excise = state.gains_excise.as_ref();
excise.map_or(0, |excise| {
walk_brackets(&excise.brackets, gains - excise.deduction)
})
}
fn taxed_of(state: &StateParams, person: &PersonIncome, year: i16) -> Dollars {
let mut by_source = person.by_source;
for row in &state.exclusions {
if !row
.from_age
.is_none_or(|age| is_age_reached(person.birth, age, year))
{
continue;
}
for &source in &row.sources {
let held = &mut by_source[source as usize];
*held = (*held).min(0);
}
}
by_source.iter().sum()
}