use std::collections::BTreeMap;
use std::ops::RangeInclusive;
use crate::params::{BenefitParams, Bracket, TaxParams};
use crate::plan::{AccountKind, Dollars, FilingStatus, Person, PlanDate};
mod payroll;
mod state;
pub use payroll::{Earnings, Payroll, payroll};
pub use state::{PersonIncome, StateIncome, state_tax};
pub const MEDICARE_AGE: u8 = 65;
pub const EARLIEST_CLAIM_AGE: i16 = 62;
const INDEXING_AGE: i16 = 60;
pub const FIRST_WORKING_AGE: i16 = 22;
pub const LATEST_CREDIT_AGE: i16 = 70;
pub const MONTHS_PER_YEAR: i32 = 12;
pub const EARLIEST_CLAIM_MONTHS: i32 = EARLIEST_CLAIM_AGE as i32 * MONTHS_PER_YEAR;
pub const LATEST_CREDIT_MONTHS: i32 = LATEST_CREDIT_AGE as i32 * MONTHS_PER_YEAR;
const COMPUTATION_YEARS: usize = 35;
const COMPUTATION_MONTHS: f64 = (COMPUTATION_YEARS as i32 * MONTHS_PER_YEAR) as f64;
const PIA_RATES: [f64; 3] = [0.90, 0.32, 0.15];
const EARLY_FIRST_MONTHS: i32 = 36;
const EARLY_FIRST_RATE: f64 = 5.0 / 900.0;
const EARLY_BEYOND_RATE: f64 = 5.0 / 1200.0;
const DELAYED_RATE: f64 = 2.0 / 300.0;
const FRA_65_LAST_BIRTH_YEAR: i16 = 1937;
const FRA_66_FIRST_BIRTH_YEAR: i16 = 1943;
const FRA_66_LAST_BIRTH_YEAR: i16 = 1954;
const FRA_67_FIRST_BIRTH_YEAR: i16 = 1960;
const FRA_STEP_MONTHS: i32 = 2;
pub const IRMAA_LOOKBACK_YEARS: i16 = 2;
pub(crate) fn is_age_reached(birth: PlanDate, age: f64, year: i16) -> bool {
let months_per_year = f64::from(MONTHS_PER_YEAR);
let months = (age * months_per_year).round() + f64::from(birth.0.month() - 1);
f64::from(birth.year()) + (months / months_per_year).floor() <= f64::from(year)
}
#[must_use]
pub fn is_medicare_covered(person: &Person, year: i16) -> bool {
person.age_in_year(year) >= i16::from(MEDICARE_AGE)
}
const RMD_AGE_75_BIRTH_YEAR: i16 = 1960;
const RMD_START_AGE_EARLY: u8 = 73;
const RMD_START_AGE_LATE: u8 = 75;
#[must_use]
pub fn ordinary_tax(params: &TaxParams, status: FilingStatus, taxable: Dollars) -> Dollars {
walk_brackets(params.brackets.for_status(status), taxable)
}
pub(super) fn walk_brackets(brackets: &[Bracket], taxable: Dollars) -> Dollars {
let mut tax = 0.0;
for (i, bracket) in brackets.iter().enumerate() {
if taxable <= bracket.over {
break;
}
let top = brackets
.get(i + 1)
.map_or(taxable, |next| taxable.min(next.over));
tax += bracket.rate * (top - bracket.over) as f64;
}
tax.round() as Dollars
}
#[must_use]
pub fn ltcg_tax(
params: &TaxParams,
status: FilingStatus,
ordinary_taxable: Dollars,
gains: Dollars,
) -> Dollars {
if gains <= 0 {
return 0;
}
let stack_bottom = ordinary_taxable.max(0);
let zero_top = params.ltcg.zero_until.get(status);
let fifteen_top = params.ltcg.fifteen_until.get(status);
let in_zero = (zero_top - stack_bottom).clamp(0, gains);
let above_zero = gains - in_zero;
let in_middle = (fifteen_top - stack_bottom.max(zero_top)).clamp(0, above_zero);
let in_top = above_zero - in_middle;
let tax = params.ltcg.middle_rate * in_middle as f64 + params.ltcg.top_rate * in_top as f64;
tax.round() as Dollars
}
#[must_use]
pub fn taxable_social_security(
params: &TaxParams,
status: FilingStatus,
other_income: Dollars,
benefit: Dollars,
) -> Dollars {
if benefit <= 0 {
return 0;
}
let provisional = other_income + benefit / 2;
let base = params.social_security.provisional_base.get(status);
let upper = params.social_security.provisional_upper.get(status);
if provisional <= base {
return 0;
}
if provisional <= upper {
return ((provisional - base) / 2).min(benefit / 2);
}
let from_lower_band = ((upper - base) / 2).min(benefit / 2);
let above_upper = (0.85 * (provisional - upper) as f64).round() as Dollars;
(above_upper + from_lower_band).min((0.85 * benefit as f64).round() as Dollars)
}
#[must_use]
pub fn full_retirement_months(birth_year: i16) -> i32 {
let steps = |from: i16| FRA_STEP_MONTHS * i32::from(birth_year - from);
if birth_year <= FRA_65_LAST_BIRTH_YEAR {
65 * MONTHS_PER_YEAR
} else if birth_year < FRA_66_FIRST_BIRTH_YEAR {
65 * MONTHS_PER_YEAR + steps(FRA_65_LAST_BIRTH_YEAR)
} else if birth_year <= FRA_66_LAST_BIRTH_YEAR {
66 * MONTHS_PER_YEAR
} else if birth_year < FRA_67_FIRST_BIRTH_YEAR {
66 * MONTHS_PER_YEAR + steps(FRA_66_LAST_BIRTH_YEAR)
} else {
67 * MONTHS_PER_YEAR
}
}
#[must_use]
pub fn social_security_benefit(
params: &BenefitParams,
birth_year: i16,
age_months: i32,
earnings: &BTreeMap<i16, Dollars>,
colas: &[f64],
) -> Dollars {
let pia = (colas.iter()).fold(
primary_insurance_amount(params, birth_year, earnings),
|pia, cola| to_the_dime(pia * (1.0 + cola)),
);
let monthly = (pia * claim_factor(birth_year, age_months) * 100.0).round() / 100.0;
monthly.floor() as Dollars * MONTHS_PER_YEAR as Dollars
}
fn to_the_dime(amount: f64) -> f64 {
((amount * 100.0).round() / 10.0).floor() / 10.0
}
#[must_use]
pub fn earnings_at_wage(
params: &BenefitParams,
salary: Dollars,
paid_in: i16,
years: RangeInclusive<i16>,
) -> BTreeMap<i16, Dollars> {
years
.filter_map(|year| {
let scaled = salary as f64 * params.wage_ratio(year, paid_in)?;
Some((year, scaled.round() as Dollars))
})
.collect()
}
#[must_use]
pub fn career_before(
params: &BenefitParams,
birth_year: i16,
salary: Dollars,
start_year: i16,
) -> BTreeMap<i16, Dollars> {
let from = birth_year + FIRST_WORKING_AGE;
earnings_at_wage(params, salary, start_year, from..=start_year - 1)
}
#[must_use]
pub fn claim_year_age(birth_year: i16, age_months: i32, january_months: i32) -> i32 {
let full = full_retirement_months(birth_year);
if age_months > full && age_months < LATEST_CREDIT_MONTHS {
january_months.clamp(full, age_months)
} else {
age_months
}
}
#[must_use]
pub fn month_index(year: i16, month: i8) -> i32 {
i32::from(year) * MONTHS_PER_YEAR + i32::from(month) - 1
}
#[must_use]
pub fn attained_month(birth: PlanDate, age: u8) -> i32 {
month_index(birth.year() + i16::from(age), birth.0.month()) - i32::from(birth.0.day() == 1)
}
#[must_use]
pub fn age_months(birth: PlanDate, month: i32) -> i32 {
month - attained_month(birth, 0)
}
#[must_use]
pub fn first_claim_month(birth: PlanDate) -> i32 {
let attained = attained_month(birth, EARLIEST_CLAIM_AGE as u8);
attained + i32::from(birth.0.day() != 2)
}
#[must_use]
pub fn claim_year_share(first_paid: i32, year: i16) -> f64 {
let months = (month_index(year + 1, 1) - first_paid).clamp(0, MONTHS_PER_YEAR);
f64::from(months) / f64::from(MONTHS_PER_YEAR)
}
fn primary_insurance_amount(
params: &BenefitParams,
birth_year: i16,
earnings: &BTreeMap<i16, Dollars>,
) -> f64 {
let index_year = birth_year + INDEXING_AGE;
let to_index_year = |year| {
if year >= index_year {
1.0
} else {
params.wage_ratio(index_year, year).unwrap_or(1.0)
}
};
let mut indexed: Vec<f64> = earnings
.iter()
.map(|(&year, &amount)| amount.min(params.wage_base(year)) as f64 * to_index_year(year))
.collect();
indexed.sort_by(|a, b| b.total_cmp(a));
let total: f64 = indexed.iter().take(COMPUTATION_YEARS).sum();
let aime = (total / COMPUTATION_MONTHS).floor();
let eligibility_year = birth_year + EARLIEST_CLAIM_AGE;
let [first, second] = params
.bend_points(eligibility_year)
.map(|point| point as f64);
let pia = PIA_RATES[0] * aime.min(first)
+ PIA_RATES[1] * (aime.min(second) - first).max(0.0)
+ PIA_RATES[2] * (aime - second).max(0.0);
to_the_dime(pia)
}
fn claim_factor(birth_year: i16, age_months: i32) -> f64 {
let full = full_retirement_months(birth_year);
let claim = age_months.min(LATEST_CREDIT_MONTHS);
if claim >= full {
return 1.0 + f64::from(claim - full) * DELAYED_RATE;
}
let early = full - claim;
let first = early.min(EARLY_FIRST_MONTHS);
1.0 - f64::from(first) * EARLY_FIRST_RATE - f64::from(early - first) * EARLY_BEYOND_RATE
}
#[must_use]
pub fn irmaa_surcharge(
params: &TaxParams,
status: FilingStatus,
magi: Dollars,
part_d: bool,
) -> Dollars {
params
.irmaa
.iter()
.rev()
.find(|tier| magi > tier.magi_over.get(status))
.map_or(0, |tier| tier.part_b + if part_d { tier.part_d } else { 0 })
}
#[must_use]
pub fn irmaa_threshold(params: &TaxParams, status: FilingStatus, tier: u8) -> Option<Dollars> {
params
.irmaa
.get(usize::from(tier))
.map(|entry| entry.magi_over.get(status))
}
#[must_use]
pub fn rmd(params: &TaxParams, age: u8, balance: Dollars) -> Dollars {
if balance <= 0 {
return 0;
}
let divisors = ¶ms.rmd.divisors;
let Some(first) = divisors.first() else {
return 0;
};
if age < first.age {
return 0;
}
let divisor = divisors
.iter()
.rev()
.find(|row| row.age <= age)
.map_or(first.divisor, |row| row.divisor);
(balance as f64 / divisor).round() as Dollars
}
#[must_use]
pub fn rmd_start_age(birth_year: i16) -> u8 {
if birth_year >= RMD_AGE_75_BIRTH_YEAR {
RMD_START_AGE_LATE
} else {
RMD_START_AGE_EARLY
}
}
#[must_use]
pub fn is_penalty_exempt(kind: AccountKind) -> bool {
matches!(kind, AccountKind::K457b)
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum LimitPool {
EmployerPlan,
Deferred457,
Simple,
Ira,
Hsa,
}
#[must_use]
pub fn limit_pool(kind: AccountKind) -> Option<LimitPool> {
match kind {
AccountKind::K401k | AccountKind::K403b => Some(LimitPool::EmployerPlan),
AccountKind::K457b => Some(LimitPool::Deferred457),
AccountKind::SimpleIra => Some(LimitPool::Simple),
AccountKind::Ira => Some(LimitPool::Ira),
AccountKind::Hsa => Some(LimitPool::Hsa),
AccountKind::K414k | AccountKind::SepIra | AccountKind::Brokerage | AccountKind::Cash => {
None
}
}
}
#[must_use]
pub fn employee_limit(
params: &TaxParams,
status: FilingStatus,
kind: AccountKind,
age: u8,
) -> Option<Dollars> {
let limits = ¶ms.limits;
let plan_catch_up = |base_50: Dollars, base_60: Dollars| match age {
60..=63 => base_60,
50.. => base_50,
_ => 0,
};
let limit = match limit_pool(kind)? {
LimitPool::EmployerPlan | LimitPool::Deferred457 => {
limits.employer_plan
+ plan_catch_up(
limits.employer_plan_catch_up_50,
limits.employer_plan_catch_up_60,
)
}
LimitPool::Simple => {
limits.simple + plan_catch_up(limits.simple_catch_up_50, limits.simple_catch_up_60)
}
LimitPool::Ira => limits.ira + if age >= 50 { limits.ira_catch_up_50 } else { 0 },
LimitPool::Hsa => {
let coverage = match status {
FilingStatus::Single => limits.hsa_self,
FilingStatus::MarriedJoint => limits.hsa_family,
};
coverage + if age >= 55 { limits.hsa_catch_up_55 } else { 0 }
}
};
Some(limit)
}