use crate::params::TaxTables;
use crate::plan::{Issue, Plan, Residency, US};
use super::resolve::Resolver;
use super::{horizon_year, plan_inflation};
fn residence_in<'plan>(
plan: &'plan Plan,
resolver: &Resolver,
year: i16,
) -> Option<(usize, &'plan Residency)> {
let began = |residency: &Residency| match &residency.from {
None => Some(i16::MIN),
Some(from) => resolver.trigger_year(plan, from),
};
let lived = plan.residency.iter().enumerate();
lived
.filter_map(|(at, residency)| Some((began(residency)?, at, residency)))
.filter(|(began, ..)| *began <= year)
.max_by_key(|(began, ..)| *began)
.map(|(_, at, residency)| (at, residency))
}
pub(super) fn state_in<'plan>(
plan: &'plan Plan,
resolver: &Resolver,
year: i16,
) -> Option<&'plan str> {
taxing_state(residence_in(plan, resolver, year)?.1)
}
fn taxing_state(residency: &Residency) -> Option<&str> {
residency
.state
.as_deref()
.filter(|_| residency.country == US)
}
#[must_use]
pub(super) fn check_modeled_states(plan: &Plan, tables: &TaxTables) -> Vec<Issue> {
let resolver = Resolver::new(plan);
let inflation = plan_inflation(plan);
let mut issues: Vec<Issue> = Vec::new();
for year in plan.plan.start_year..=horizon_year(plan) {
let Some((at, residency)) = residence_in(plan, &resolver, year) else {
continue;
};
let Some(state) = taxing_state(residency) else {
continue;
};
let params = tables.params_for(year, &inflation);
let path = format!("residency[{at}].state");
if params.states.contains_key(state) || issues.iter().any(|issue| issue.path == path) {
continue;
}
let message = format!(
"state income tax is not modeled for \"{state}\" in {year}; without a residency \
the plan projects federal tax only"
);
issues.push(Issue { path, message });
}
issues
}