use std::collections::BTreeSet;
use crate::params::{ContributionLimits, PhaseOut, TaxParams};
use crate::plan::{Account, AccountKind, Dollars, FilingStatus};
use super::contribute::push_unique;
use super::year::{Simulation, YearAcc};
use super::{Action, ContributionNote, Taxes, scale};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum IraBand {
Covered,
SpouseCovered,
}
impl IraBand {
const ALL: [Self; 2] = [Self::Covered, Self::SpouseCovered];
fn phase_out(self, limits: &ContributionLimits, status: FilingStatus) -> Option<PhaseOut> {
match self {
Self::Covered => Some(*limits.ira_deduction_phase_out.for_status(status)),
Self::SpouseCovered => limits.spouse_ira_deduction_phase_out(status),
}
}
fn pending(self, pending: &[(usize, IraBand, Dollars)]) -> Dollars {
pending
.iter()
.filter(|&&(_, band, _)| band == self)
.map(|&(_, _, amount)| amount)
.sum()
}
}
pub(super) fn ira_deducted(
params: &TaxParams,
status: FilingStatus,
pending: &[(usize, IraBand, Dollars)],
magi: Dollars,
) -> [Dollars; 2] {
IraBand::ALL.map(|band| {
let withheld = band
.phase_out(¶ms.limits, status)
.map_or(0.0, |phase_out| phase_out.position(magi));
scale(band.pending(pending), 1.0 - withheld)
})
}
impl Simulation<'_> {
pub(super) fn ira_band(account: &Account, covered: &BTreeSet<&str>) -> Option<IraBand> {
if account.kind != AccountKind::Ira {
return None;
}
if covered.contains(account.owner.as_str()) {
return Some(IraBand::Covered);
}
let is_spouse_covered = covered.iter().any(|&person| person != account.owner);
is_spouse_covered.then_some(IraBand::SpouseCovered)
}
pub(super) fn settle_ira_bands(
&mut self,
params: &TaxParams,
taxes: &Taxes,
acc: &mut YearAcc,
) {
for (band, deducted) in IraBand::ALL.into_iter().zip(taxes.ira_deducted) {
self.keep_not_deducted(band, deducted, acc);
}
let band = params
.limits
.roth_ira_phase_out
.for_status(self.plan.household.filing);
if band.position(taxes.magi) <= 0.0 {
return;
}
for account in &self.plan.accounts {
if account.kind == AccountKind::Ira && account.roth {
note_on(
&mut acc.actions,
&account.id,
ContributionNote::RothIraPhaseOut,
);
}
}
}
fn keep_not_deducted(&mut self, band: IraBand, deducted: Dollars, acc: &mut YearAcc) {
let pending = band.pending(&acc.ira_to_settle);
let withheld = pending - deducted;
if withheld <= 0 {
return;
}
let entries = acc.ira_to_settle.iter().filter(|&&(_, of, _)| of == band);
for &(index, _, amount) in entries {
let part = scale(amount, withheld as f64 / pending as f64);
self.bases[index] += part;
let id = &self.plan.accounts[index].id;
note_on(
&mut acc.actions,
id,
ContributionNote::NotDeducted { amount: part },
);
}
}
}
fn note_on(actions: &mut [Action], id: &str, note: ContributionNote) {
let paid = actions.iter_mut().find_map(|action| match action {
Action::Contribution {
account,
employee,
notes,
..
} if account == id && *employee > 0 => Some(notes),
_ => None,
});
if let Some(notes) = paid {
push_unique(notes, note);
}
}