use crate::params::{TaxParams, TaxTables};
use crate::plan::{Dollars, FilingStatus, Plan};
use crate::project::{
covered_count, horizon_year, inflation_factor, min_active_cliff_threshold, plan_inflation,
scale,
};
use crate::tax;
use super::{GainsRate, OptimizeOptions, RATE_EPSILON};
pub(super) fn conversion_window(
plan: &Plan,
options: &OptimizeOptions,
) -> std::ops::RangeInclusive<i16> {
let start = options
.start_year
.unwrap_or(plan.plan.start_year)
.max(plan.plan.start_year);
let end = options
.end_year
.unwrap_or_else(|| default_end_year(plan, options))
.min(horizon_year(plan));
start..=end
}
fn default_end_year(plan: &Plan, options: &OptimizeOptions) -> i16 {
plan.account(&options.destination)
.and_then(|account| plan.person(&account.owner))
.map_or(plan.plan.start_year, |owner| {
let birth = owner.birth.year();
birth + i16::from(tax::rmd_start_age(birth)) - 1
})
}
#[derive(Clone, Copy)]
pub(super) struct YearCeilings {
pub target: Dollars,
pub magi: Option<Dollars>,
pub gains: Option<Dollars>,
}
pub(super) fn year_ceilings(
plan: &Plan,
tables: &TaxTables,
year: i16,
bracket_rate: f64,
options: &OptimizeOptions,
) -> Option<YearCeilings> {
let params = tables.params_for(year, &plan_inflation(plan));
let filing = plan.household.filing;
let target = bracket_target(¶ms, filing, bracket_rate, options.headroom)?;
Some(YearCeilings {
target,
magi: magi_ceiling(plan, ¶ms, year, options),
gains: options.gains_rate.map(|rate| {
match rate {
GainsRate::Zero => params.ltcg.zero_until,
GainsRate::Fifteen => params.ltcg.fifteen_until,
}
.get(filing)
}),
})
}
fn magi_ceiling(
plan: &Plan,
params: &TaxParams,
year: i16,
options: &OptimizeOptions,
) -> Option<Dollars> {
let tier = options
.irmaa_tier
.filter(|_| irmaa_lookback_matters(plan, year))
.and_then(|tier| tax::irmaa_threshold(params, plan.household.filing, tier));
let explicit = options
.max_magi
.map(|max| scale(max, inflation_factor(plan, year)));
let cliff = min_active_cliff_threshold(plan, year);
[tier, explicit, cliff].into_iter().flatten().min()
}
fn irmaa_lookback_matters(plan: &Plan, year: i16) -> bool {
let premium_year = year + tax::IRMAA_LOOKBACK_YEARS;
premium_year <= horizon_year(plan) && covered_count(plan, premium_year) > 0
}
fn bracket_target(
params: &TaxParams,
filing: FilingStatus,
rate: f64,
headroom: Dollars,
) -> Option<Dollars> {
let brackets = params.brackets.for_status(filing);
let index = brackets
.iter()
.position(|bracket| (bracket.rate - rate).abs() < RATE_EPSILON)?;
let top = brackets.get(index + 1)?.over;
Some(top - headroom)
}