use crate::params::{Source, StateParams, TaxParams};
use crate::plan::{Account, Dollars, FilingStatus, Person, TreatmentClass};
use crate::tax::{self, Payroll, StateIncome};
use super::Taxes;
use super::flows::distribution;
use super::ira::ira_deducted;
use super::residence;
use super::scale;
use super::year::{Simulation, YearAcc};
const MAX_TAX_ITERATIONS: usize = 30;
const PENALTY_FREE_AGE: f64 = 59.5;
const SEPARATION_AGE: i16 = 55;
const PUBLIC_SAFETY_SEPARATION_AGE: i16 = 50;
impl Simulation<'_> {
pub(super) fn settle_cash(
&mut self,
year: i16,
params: &TaxParams,
acc: &mut YearAcc,
) -> Taxes {
let state = residence::state_in(self.plan, &self.resolver, year)
.and_then(|code| params.states.get(code));
let status = self.plan.household.filing;
let payroll = tax::payroll(params, status, &acc.payroll);
let mut taxes = compute_taxes(params, state, status, payroll, acc);
acc.cliffs = self.cliff_costs(year, &taxes);
let order = self.drain_candidates(year);
for _ in 0..MAX_TAX_ITERATIONS {
let need = acc.expenses + acc.employee + taxes.total + acc.medicare + acc.cliffs;
let available = acc.cash + acc.drained_cash;
let shortfall = need - available;
if shortfall <= 0 {
acc.surplus = -shortfall;
return taxes;
}
if self.drain(&order, year, shortfall, acc) == 0 {
acc.unfunded = shortfall;
return taxes;
}
taxes = compute_taxes(params, state, status, payroll, acc);
acc.cliffs = self.cliff_costs(year, &taxes);
}
let residual = acc.expenses + acc.employee + taxes.total + acc.medicare + acc.cliffs
- acc.cash
- acc.drained_cash;
acc.unfunded = residual.max(0);
acc.surplus = (-residual).max(0);
taxes
}
fn drain(&mut self, order: &[usize], year: i16, want: Dollars, acc: &mut YearAcc) -> Dollars {
let mut remaining = want;
for is_free_pass in [true, false] {
for &index in order {
if remaining <= 0 {
break;
}
if self.balances[index] <= 0 {
continue;
}
let limit = if is_free_pass {
remaining.min(self.penalty_free_room(index, year, acc))
} else {
remaining
};
remaining -= self.withdraw(index, limit, year, acc);
}
}
want - remaining
}
fn penalty_free_room(&self, index: usize, year: i16, acc: &YearAcc) -> Dollars {
let account = &self.plan.accounts[index];
match account.treatment() {
TreatmentClass::Roth => self.roth_room(index, year),
TreatmentClass::Hsa if self.is_under_medicare_age(account, year) => acc.medical,
TreatmentClass::Deferred if self.pays_penalty(account, year) => 0,
TreatmentClass::Deferred | TreatmentClass::Taxable | TreatmentClass::Hsa => {
Dollars::MAX
}
}
}
fn is_under_medicare_age(&self, account: &Account, year: i16) -> bool {
self.plan
.person(&account.owner)
.is_none_or(|owner| !tax::is_medicare_covered(owner, year))
}
fn drain_candidates(&self, year: i16) -> Vec<usize> {
let unlocked = |index: &usize| self.is_unlocked(*index, year);
let mut prioritized: Vec<usize> = (0..self.plan.accounts.len())
.filter(|&index| self.plan.accounts[index].drain_priority.is_some())
.filter(unlocked)
.collect();
prioritized.sort_by_key(|&index| self.plan.accounts[index].drain_priority);
for class in &self.plan.plan.withdrawal_order {
for (index, account) in self.plan.accounts.iter().enumerate() {
if account.drain_priority.is_none()
&& account.treatment() == *class
&& self.is_unlocked(index, year)
{
prioritized.push(index);
}
}
}
prioritized
}
pub(super) fn is_unlocked(&self, index: usize, year: i16) -> bool {
match &self.plan.accounts[index].locked_until {
None => true,
Some(trigger) => self
.resolver
.trigger_year(self.plan, trigger)
.is_some_and(|unlock| year >= unlock),
}
}
pub(super) fn pays_penalty(&self, account: &Account, year: i16) -> bool {
if tax::is_penalty_exempt(account.kind) {
return false;
}
self.plan.person(&account.owner).is_none_or(|owner| {
!is_penalty_free_age(owner, year) && !self.is_freed_by_separation(account, owner, year)
})
}
pub(super) fn is_under_penalty_age(&self, account: &Account, year: i16) -> bool {
self.plan
.person(&account.owner)
.is_none_or(|owner| !is_penalty_free_age(owner, year))
}
fn is_freed_by_separation(&self, account: &Account, owner: &Person, year: i16) -> bool {
let Some(trigger) = &account.separated else {
return false;
};
let freeing_age = if account.public_safety {
PUBLIC_SAFETY_SEPARATION_AGE
} else {
SEPARATION_AGE
};
self.resolver
.trigger_year(self.plan, trigger)
.is_some_and(|left| left <= year && owner.age_in_year(left) >= freeing_age)
}
fn withdraw(&mut self, index: usize, want: Dollars, year: i16, acc: &mut YearAcc) -> Dollars {
let take = want.min(self.balances[index]);
if take <= 0 {
return 0;
}
let account = &self.plan.accounts[index];
match account.treatment() {
TreatmentClass::Deferred => {
let taxed = take - self.remove_basis(index, take);
self.tax_distribution(index, taxed, year, acc);
}
TreatmentClass::Taxable if account.kind.tracks_basis() => {
acc.gains += take - self.remove_basis(index, take);
}
TreatmentClass::Roth => {
let draw = self.draw_roth(index, take, year);
let source = distribution(draw.is_early);
acc.tax(self.person_at(&account.owner), source, draw.taxed);
acc.penalty_base += draw.penalized;
}
TreatmentClass::Hsa => {
let medical = take.min(acc.medical);
acc.medical -= medical;
acc.tax(
self.person_at(&account.owner),
Source::Other,
take - medical,
);
if self.is_under_medicare_age(account, year) {
acc.hsa_penalty_base += take - medical;
}
}
TreatmentClass::Taxable => {}
}
self.balances[index] -= take;
acc.drained_cash += take;
*acc.withdrawals.entry(account.id.clone()).or_default() += take;
match acc
.funding
.iter_mut()
.find(|(drained, _)| *drained == index)
{
Some((_, total)) => *total += take,
None => acc.funding.push((index, take)),
}
take
}
}
fn compute_taxes(
params: &TaxParams,
state: Option<&StateParams>,
status: FilingStatus,
payroll: Payroll,
acc: &YearAcc,
) -> Taxes {
let ordinary_income = acc.ordinary() - payroll.deduction;
let before = ordinary_income + acc.gains;
let ss_before = tax::taxable_social_security(params, status, before, acc.ss_gross);
let by_band = ira_deducted(params, status, &acc.ira_to_settle, before + ss_before);
let deducted: Dollars = by_band.iter().sum();
let other_income = before - deducted;
let taxable_ss = tax::taxable_social_security(params, status, other_income, acc.ss_gross);
let deduction = params.deductions.standard.get(status);
let ordinary_taxable = ordinary_income - deducted + taxable_ss - deduction;
let ordinary = tax::ordinary_tax(params, status, ordinary_taxable);
let ltcg = tax::ltcg_tax(params, status, ordinary_taxable, acc.gains);
let early = params.early_withdrawal;
let penalty =
scale(acc.penalty_base, early.penalty) + scale(acc.hsa_penalty_base, early.hsa_penalty);
let magi = (other_income + taxable_ss).max(0);
let federal_tax = ordinary + ltcg + penalty;
let income = StateIncome {
year: params.year,
people: &acc.taxed,
gains: acc.gains,
taxable_social_security: taxable_ss,
deferred: acc.deferred + deducted,
federal_tax,
agi: magi,
};
let state = state.map_or(0, |state| tax::state_tax(state, status, &income));
Taxes {
ordinary,
ltcg,
penalty,
state,
payroll: payroll.total,
taxable_social_security: taxable_ss,
ordinary_taxable: ordinary_taxable.max(0),
gains: acc.gains,
magi,
total: federal_tax + state + payroll.total,
ira_deducted: by_band,
}
}
fn is_penalty_free_age(person: &Person, year: i16) -> bool {
tax::is_age_reached(person.birth, PENALTY_FREE_AGE, year)
}