mod benefit;
mod collect;
mod contribute;
mod flows;
mod invest;
mod ira;
mod path;
mod residence;
mod resolve;
mod roth;
mod settle;
mod year;
pub use benefit::benefit_params;
pub use path::MarketPath;
use residence::check_modeled_states;
pub use residence::state_lived_in;
pub(crate) use resolve::trigger_month;
pub use resolve::{Timeline, Window};
use std::collections::BTreeMap;
use serde::Serialize;
pub(crate) use crate::params::scale;
use crate::params::{Inflation, TaxTables};
use crate::plan::{Dollars, Issue, Plan, TreatmentClass};
#[derive(Debug, Clone, PartialEq, Serialize)]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct Projection {
pub years: Vec<YearRow>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct Taxes {
pub ordinary: Dollars,
pub ltcg: Dollars,
pub penalty: Dollars,
pub state: Dollars,
pub payroll: Dollars,
pub taxable_social_security: Dollars,
pub ordinary_taxable: Dollars,
pub gains: Dollars,
pub magi: Dollars,
pub total: Dollars,
#[serde(skip)]
pub(crate) ira_deducted: [Dollars; 2],
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Serialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct ClassTotals {
pub taxable: Dollars,
pub deferred: Dollars,
pub roth: Dollars,
pub hsa: Dollars,
}
impl ClassTotals {
#[must_use]
pub fn get(&self, class: TreatmentClass) -> Dollars {
match class {
TreatmentClass::Taxable => self.taxable,
TreatmentClass::Deferred => self.deferred,
TreatmentClass::Roth => self.roth,
TreatmentClass::Hsa => self.hsa,
}
}
pub(crate) fn add(&mut self, class: TreatmentClass, amount: Dollars) {
match class {
TreatmentClass::Taxable => self.taxable += amount,
TreatmentClass::Deferred => self.deferred += amount,
TreatmentClass::Roth => self.roth += amount,
TreatmentClass::Hsa => self.hsa += amount,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum ContributionNote {
Share {
rate: f64,
of: String,
},
Maximum,
Match {
rate: f64,
up_to: f64,
of: String,
},
AfterTax {
amount: Dollars,
},
HeldToLimit,
HeldToOverall,
RothIraPhaseOut,
NotDeducted {
amount: Dollars,
},
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum Action {
Transfer {
from: String,
to: String,
amount: Dollars,
},
Rmd {
account: String,
amount: Dollars,
},
Contribution {
account: String,
employee: Dollars,
employer: Dollars,
notes: Vec<ContributionNote>,
},
Conversion {
from: String,
to: String,
amount: Dollars,
},
Withdrawal {
account: String,
amount: Dollars,
},
Surplus {
account: String,
amount: Dollars,
},
}
#[derive(Debug, Clone, PartialEq, Serialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct YearRow {
pub year: i16,
pub ages: BTreeMap<String, u8>,
pub deflator: f64,
pub income: BTreeMap<String, Dollars>,
pub total_income: Dollars,
pub pretax: Dollars,
pub covered: BTreeMap<String, Dollars>,
pub expenses: Dollars,
pub spending: BTreeMap<String, Dollars>,
pub expenses_essential: Dollars,
pub expenses_flexible: Dollars,
pub medicare: Dollars,
pub contributions_employee: Dollars,
pub contributions_employer: Dollars,
pub rmds: Dollars,
pub conversions: Dollars,
pub withdrawals: BTreeMap<String, Dollars>,
pub growth: BTreeMap<String, Dollars>,
pub actions: Vec<Action>,
pub taxes: Taxes,
pub surplus: Dollars,
pub unfunded: Dollars,
pub balances: BTreeMap<String, Dollars>,
pub class_totals: ClassTotals,
pub net_worth: Dollars,
}
impl YearRow {
#[must_use]
pub const fn expenses_once(&self) -> Dollars {
self.expenses - self.expenses_essential - self.expenses_flexible
}
#[must_use]
pub fn total_withdrawals(&self) -> Dollars {
self.withdrawals.values().sum()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
pub struct Summary {
pub final_net_worth: Dollars,
pub peak_net_worth: Dollars,
pub peak_year: i16,
pub lifetime_taxes: Dollars,
pub lifetime_conversions: Dollars,
pub lifetime_unfunded: Dollars,
pub lifetime_medicare: Dollars,
pub first_unfunded_year: Option<i16>,
pub final_deferred: Dollars,
}
impl Projection {
#[must_use]
pub fn summary(&self, deflated: bool) -> Summary {
let money = |amount: Dollars, deflator: f64| {
if deflated {
deflate(amount, deflator)
} else {
amount
}
};
let mut peak: Option<(Dollars, i16)> = None;
let mut taxes = 0;
let mut conversions = 0;
let mut unfunded = 0;
let mut medicare = 0;
let mut first_unfunded_year = None;
for row in &self.years {
let net_worth = money(row.net_worth, row.deflator);
if peak.is_none_or(|(value, _)| net_worth > value) {
peak = Some((net_worth, row.year));
}
taxes += money(row.taxes.total, row.deflator);
conversions += money(row.conversions, row.deflator);
unfunded += money(row.unfunded, row.deflator);
medicare += money(row.medicare, row.deflator);
if row.unfunded > 0 && first_unfunded_year.is_none() {
first_unfunded_year = Some(row.year);
}
}
let (peak_net_worth, peak_year) = peak.unwrap_or((0, 0));
let last = self.years.last();
Summary {
final_net_worth: last.map_or(0, |row| money(row.net_worth, row.deflator)),
peak_net_worth,
peak_year,
lifetime_taxes: taxes,
lifetime_conversions: conversions,
lifetime_unfunded: unfunded,
lifetime_medicare: medicare,
first_unfunded_year,
final_deferred: last.map_or(0, |row| money(row.class_totals.deferred, row.deflator)),
}
}
}
pub(crate) fn min_active_cliff_threshold(plan: &Plan, year: i16) -> Option<Dollars> {
if plan.cliffs.is_empty() {
return None;
}
let resolver = resolve::Resolver::new(plan);
let default_end = collect::default_cliff_end(plan);
plan.cliffs
.iter()
.filter(|cliff| collect::is_cliff_active(plan, &resolver, default_end, cliff, year))
.map(|cliff| {
let years = i32::from(year - plan.plan.start_year);
scale(
cliff.magi_over,
cliff.cola.factor(inflation_factor(plan, year), years),
)
})
.min()
}
#[must_use]
pub fn irmaa_purchase(
plan: &Plan,
tables: &crate::params::TaxTables,
year: i16,
magi: Dollars,
) -> Dollars {
let Some(medicare) = &plan.medicare else {
return 0;
};
let premium_year = year + crate::tax::IRMAA_LOOKBACK_YEARS;
if premium_year > horizon_year(plan) {
return 0;
}
let covered = covered_count(plan, premium_year);
if covered == 0 {
return 0;
}
let params = tables.params_in(premium_year, &plan_inflation(plan), None);
let per_person =
crate::tax::irmaa_surcharge(¶ms, plan.household.filing, magi, medicare.part_d);
per_person * covered as Dollars
}
#[must_use]
pub fn deflate(amount: Dollars, deflator: f64) -> Dollars {
(amount as f64 / deflator).round() as Dollars
}
impl Projection {
#[must_use]
pub fn row(&self, year: i16) -> Option<&YearRow> {
self.years.iter().find(|row| row.year == year)
}
#[must_use]
pub fn deflate_in(&self, year: i16, amount: Dollars) -> Dollars {
self.row(year)
.map_or(amount, |row| deflate(amount, row.deflator))
}
}
#[must_use]
pub fn validate_plan(plan: &Plan, tables: &TaxTables) -> Vec<Issue> {
let issues = plan.validate();
if !issues.is_empty() {
return issues;
}
let mut issues = check_modeled_states(plan, tables);
issues.extend(benefit::check_derived_claims(plan, tables));
issues
}
#[must_use]
pub fn project(plan: &Plan, tables: &TaxTables) -> Projection {
project_on(plan, tables, &MarketPath::expected(plan))
}
#[must_use]
pub fn project_on(plan: &Plan, tables: &TaxTables, path: &MarketPath) -> Projection {
year::Simulation::new(plan, tables, path).run()
}
pub(crate) fn covered_count(plan: &Plan, year: i16) -> usize {
plan.household
.people
.iter()
.filter(|person| crate::tax::is_medicare_covered(person, year))
.count()
}
pub(crate) fn horizon_year(plan: &Plan) -> i16 {
latest_birthday_year(plan, plan.plan.horizon_age)
}
pub(crate) fn latest_birthday_year(plan: &Plan, age: u8) -> i16 {
plan.household
.people
.iter()
.map(|person| person.birth.year() + i16::from(age))
.max()
.unwrap_or(plan.plan.start_year)
}
pub(crate) fn plan_inflation(plan: &Plan) -> Inflation {
Inflation::constant(plan.plan.inflation)
}
pub(crate) fn inflation_factor(plan: &Plan, year: i16) -> f64 {
plan_inflation(plan).factor(plan.plan.start_year, year)
}
pub(crate) fn horizon_len(plan: &Plan) -> usize {
usize::try_from(horizon_year(plan) - plan.plan.start_year + 1).unwrap_or_default()
}