use crate::conventions::Usd;
use crate::tax::types::FilingStatus;
use rust_decimal_macros::dec;
use std::collections::BTreeMap;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OrdinaryBracket {
pub lower: Usd, pub rate: Usd, }
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct OrdinarySchedule {
pub brackets: Vec<OrdinaryBracket>, }
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct LtcgBreakpoints {
pub max_zero: Usd, pub max_fifteen: Usd, }
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TaxTable {
pub year: i32,
pub source: &'static str, pub ordinary: BTreeMap<FilingStatus, OrdinarySchedule>,
pub ltcg: BTreeMap<FilingStatus, LtcgBreakpoints>,
pub gift_annual_exclusion: Usd,
pub ss_wage_base: Usd,
pub gift_lifetime_exclusion: Usd,
}
impl TaxTable {
fn key(status: FilingStatus) -> FilingStatus {
match status {
FilingStatus::Qss => FilingStatus::Mfj,
s => s,
}
}
pub fn ordinary_for(&self, status: FilingStatus) -> &OrdinarySchedule {
&self.ordinary[&Self::key(status)]
}
pub fn ltcg_for(&self, status: FilingStatus) -> &LtcgBreakpoints {
&self.ltcg[&Self::key(status)]
}
}
pub trait TaxTables {
fn table_for(&self, year: i32) -> Option<&TaxTable>;
}
impl TaxTables for BTreeMap<i32, TaxTable> {
fn table_for(&self, year: i32) -> Option<&TaxTable> {
self.get(&year)
}
}
pub const NIIT_RATE: Usd = dec!(0.038);
pub const EMPLOYEE_OASDI_RATE: Usd = dec!(0.062);
pub const SE_RATE_SS: Usd = dec!(0.124);
pub const SE_RATE_MEDICARE: Usd = dec!(0.029);
pub const SE_RATE_ADDL_MEDICARE: Usd = dec!(0.009);
pub const SE_NET_EARNINGS_FACTOR: Usd = dec!(0.9235);
pub fn se_addl_medicare_threshold(status: FilingStatus) -> Usd {
match status {
FilingStatus::Mfj => dec!(250000),
FilingStatus::Mfs => dec!(125000),
FilingStatus::Single | FilingStatus::HoH | FilingStatus::Qss => dec!(200000),
}
}
pub const QUALIFIED_APPRAISAL_THRESHOLD: Usd = dec!(5000);
pub fn niit_threshold(status: FilingStatus) -> Usd {
match status {
FilingStatus::Mfj | FilingStatus::Qss => dec!(250000),
FilingStatus::Single | FilingStatus::HoH => dec!(200000),
FilingStatus::Mfs => dec!(125000),
}
}
pub fn loss_limit(status: FilingStatus) -> Usd {
match status {
FilingStatus::Mfs => dec!(1500),
_ => dec!(3000),
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AmtParams {
pub exemption_single_hoh: Usd,
pub exemption_mfj_qss: Usd,
pub exemption_mfs: Usd,
pub phaseout_start_single_hoh_mfs: Usd,
pub phaseout_start_mfj_qss: Usd,
pub breakpoint_28pct: Usd,
pub breakpoint_28pct_mfs: Usd,
pub mfs_kicker_start: Usd,
pub mfs_kicker_max: Usd,
pub phaseout_rate: Usd,
pub rate_26: Usd,
pub rate_28: Usd,
pub rate_28_subtrahend: Usd,
pub rate_28_subtrahend_mfs: Usd,
}
impl AmtParams {
pub fn exemption(&self, status: FilingStatus) -> Usd {
match status {
FilingStatus::Mfj | FilingStatus::Qss => self.exemption_mfj_qss,
FilingStatus::Mfs => self.exemption_mfs,
FilingStatus::Single | FilingStatus::HoH => self.exemption_single_hoh,
}
}
pub fn phaseout_start(&self, status: FilingStatus) -> Usd {
match status {
FilingStatus::Mfj | FilingStatus::Qss => self.phaseout_start_mfj_qss,
_ => self.phaseout_start_single_hoh_mfs,
}
}
pub fn breakpoint_28pct(&self, status: FilingStatus) -> Usd {
match status {
FilingStatus::Mfs => self.breakpoint_28pct_mfs,
_ => self.breakpoint_28pct,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FullReturnParams {
pub year: i32,
pub std_deduction: BTreeMap<FilingStatus, Usd>,
pub std_aged_blind_married: Usd,
pub std_aged_blind_unmarried: Usd,
pub dependent_std_floor: Usd,
pub dependent_std_earned_addon: Usd,
pub salt_cap: Usd,
pub kiddie_unearned_threshold: Usd,
pub elective_deferral_limit: Usd,
pub ftc_ceiling: Usd,
pub qbi_ti_threshold_unmarried: Usd,
pub qbi_ti_threshold_married: Usd,
pub student_loan_phaseout_unmarried: (Usd, Usd),
pub student_loan_phaseout_married: (Usd, Usd),
pub amt: AmtParams,
}
impl FullReturnParams {
pub fn std_deduction_for(&self, status: FilingStatus) -> Usd {
self.std_deduction[&TaxTable::key(status)]
}
pub fn qbi_ti_threshold(&self, status: FilingStatus) -> Usd {
match status {
FilingStatus::Mfj => self.qbi_ti_threshold_married,
_ => self.qbi_ti_threshold_unmarried,
}
}
pub fn student_loan_phaseout(&self, status: FilingStatus) -> Option<(Usd, Usd)> {
match status {
FilingStatus::Mfs => None,
FilingStatus::Mfj => Some(self.student_loan_phaseout_married),
FilingStatus::Single | FilingStatus::HoH | FilingStatus::Qss => {
Some(self.student_loan_phaseout_unmarried)
}
}
}
}
pub trait FullReturnTables {
fn full_return_for(&self, year: i32) -> Option<&FullReturnParams>;
}
impl FullReturnTables for BTreeMap<i32, FullReturnParams> {
fn full_return_for(&self, year: i32) -> Option<&FullReturnParams> {
self.get(&year)
}
}
#[cfg(test)]
pub(crate) fn synthetic_table(year: i32) -> TaxTable {
let mut ordinary = BTreeMap::new();
ordinary.insert(
FilingStatus::Single,
OrdinarySchedule {
brackets: vec![
OrdinaryBracket {
lower: dec!(0),
rate: dec!(0.10),
},
OrdinaryBracket {
lower: dec!(10000),
rate: dec!(0.22),
},
OrdinaryBracket {
lower: dec!(100000),
rate: dec!(0.32),
},
],
},
);
let mut ltcg = BTreeMap::new();
ltcg.insert(
FilingStatus::Single,
LtcgBreakpoints {
max_zero: dec!(40000),
max_fifteen: dec!(400000),
},
);
TaxTable {
year,
source: "SYNTHETIC",
ordinary,
ltcg,
gift_annual_exclusion: dec!(19000),
ss_wage_base: dec!(176100),
gift_lifetime_exclusion: dec!(13_990_000),
}
}
#[cfg(test)]
mod tests {
use super::*;
use rust_decimal_macros::dec;
#[test]
fn statutory_values_are_constant_across_years() {
for status in [
FilingStatus::Single,
FilingStatus::Mfj,
FilingStatus::Mfs,
FilingStatus::HoH,
FilingStatus::Qss,
] {
assert_eq!(niit_threshold(status), niit_threshold(status));
}
assert_eq!(niit_threshold(FilingStatus::Mfj), dec!(250000));
assert_eq!(niit_threshold(FilingStatus::Qss), dec!(250000));
assert_eq!(niit_threshold(FilingStatus::Single), dec!(200000));
assert_eq!(niit_threshold(FilingStatus::HoH), dec!(200000));
assert_eq!(niit_threshold(FilingStatus::Mfs), dec!(125000));
assert_eq!(NIIT_RATE, dec!(0.038));
assert_eq!(se_addl_medicare_threshold(FilingStatus::Mfj), dec!(250000));
assert_eq!(se_addl_medicare_threshold(FilingStatus::Qss), dec!(200000)); assert_eq!(
se_addl_medicare_threshold(FilingStatus::Single),
dec!(200000)
);
assert_eq!(se_addl_medicare_threshold(FilingStatus::HoH), dec!(200000));
assert_eq!(se_addl_medicare_threshold(FilingStatus::Mfs), dec!(125000));
assert_eq!(QUALIFIED_APPRAISAL_THRESHOLD, dec!(5000));
assert_eq!(loss_limit(FilingStatus::Mfs), dec!(1500));
assert_eq!(loss_limit(FilingStatus::Single), dec!(3000));
assert_eq!(loss_limit(FilingStatus::Mfj), dec!(3000));
assert_eq!(loss_limit(FilingStatus::HoH), dec!(3000));
assert_eq!(loss_limit(FilingStatus::Qss), dec!(3000));
}
#[test]
fn qss_uses_mfj_schedule() {
let mut t = synthetic_table(2025);
t.ordinary.insert(
FilingStatus::Mfj,
OrdinarySchedule {
brackets: vec![
OrdinaryBracket {
lower: dec!(0),
rate: dec!(0.10),
},
OrdinaryBracket {
lower: dec!(50000),
rate: dec!(0.22),
},
],
},
);
t.ltcg.insert(
FilingStatus::Mfj,
LtcgBreakpoints {
max_zero: dec!(80000),
max_fifteen: dec!(500000),
},
);
assert_eq!(
t.ordinary_for(FilingStatus::Qss).brackets,
t.ordinary_for(FilingStatus::Mfj).brackets
);
assert_eq!(
*t.ltcg_for(FilingStatus::Qss),
*t.ltcg_for(FilingStatus::Mfj)
);
}
}