use btctax_core::tax::tables::{
AmtParams, FullReturnParams, FullReturnTables, LtcgBreakpoints, OrdinaryBracket,
OrdinarySchedule, TaxTable, TaxTables,
};
use btctax_core::{FilingStatus, Usd};
use rust_decimal_macros::dec;
use std::collections::BTreeMap;
#[derive(Debug, Clone)]
pub struct BundledTaxTables {
by_year: BTreeMap<i32, TaxTable>,
}
impl BundledTaxTables {
pub fn load() -> Self {
let mut by_year = BTreeMap::new();
by_year.insert(2017, ty2017());
by_year.insert(2024, ty2024());
by_year.insert(2025, ty2025());
by_year.insert(2026, ty2026());
Self { by_year }
}
}
impl TaxTables for BundledTaxTables {
fn table_for(&self, year: i32) -> Option<&TaxTable> {
self.by_year.get(&year)
}
}
#[derive(Debug, Clone)]
pub struct BundledFullReturnTables {
by_year: BTreeMap<i32, FullReturnParams>,
}
impl BundledFullReturnTables {
pub fn load() -> Self {
let mut by_year = BTreeMap::new();
by_year.insert(2024, ty2024_full_return());
Self { by_year }
}
}
impl FullReturnTables for BundledFullReturnTables {
fn full_return_for(&self, year: i32) -> Option<&FullReturnParams> {
self.by_year.get(&year)
}
}
fn ty2024_full_return() -> FullReturnParams {
let mut std_deduction = BTreeMap::new();
std_deduction.insert(FilingStatus::Single, dec!(14600));
std_deduction.insert(FilingStatus::Mfj, dec!(29200));
std_deduction.insert(FilingStatus::Mfs, dec!(14600));
std_deduction.insert(FilingStatus::HoH, dec!(21900));
FullReturnParams {
year: 2024,
std_deduction,
std_aged_blind_married: dec!(1550), std_aged_blind_unmarried: dec!(1950),
dependent_std_floor: dec!(1300), dependent_std_earned_addon: dec!(450),
salt_cap: dec!(10000), kiddie_unearned_threshold: dec!(2600), elective_deferral_limit: dec!(23000), ftc_ceiling: dec!(300), qbi_ti_threshold_unmarried: dec!(191950),
qbi_ti_threshold_married: dec!(383900),
student_loan_phaseout_unmarried: (dec!(80000), dec!(95000)),
student_loan_phaseout_married: (dec!(165000), dec!(195000)),
amt: AmtParams {
exemption_single_hoh: dec!(85700),
exemption_mfj_qss: dec!(133300),
exemption_mfs: dec!(66650),
phaseout_start_single_hoh_mfs: dec!(609350),
phaseout_start_mfj_qss: dec!(1218700),
breakpoint_28pct: dec!(232600),
breakpoint_28pct_mfs: dec!(116300),
mfs_kicker_start: dec!(875950),
mfs_kicker_max: dec!(66650),
phaseout_rate: dec!(0.25),
rate_26: dec!(0.26),
rate_28: dec!(0.28),
rate_28_subtrahend: dec!(4652),
rate_28_subtrahend_mfs: dec!(2326),
},
}
}
fn br(lower: Usd, rate: Usd) -> OrdinaryBracket {
OrdinaryBracket { lower, rate }
}
fn ty2017() -> TaxTable {
let mut ordinary = BTreeMap::new();
ordinary.insert(
FilingStatus::Single,
OrdinarySchedule {
brackets: vec![
br(dec!(0), dec!(0.10)),
br(dec!(9325), dec!(0.15)),
br(dec!(37950), dec!(0.25)),
br(dec!(91900), dec!(0.28)),
br(dec!(191650), dec!(0.33)),
br(dec!(416700), dec!(0.35)),
br(dec!(418400), dec!(0.396)),
],
},
);
ordinary.insert(
FilingStatus::Mfj,
OrdinarySchedule {
brackets: vec![
br(dec!(0), dec!(0.10)),
br(dec!(18650), dec!(0.15)),
br(dec!(75900), dec!(0.25)),
br(dec!(153100), dec!(0.28)),
br(dec!(233350), dec!(0.33)),
br(dec!(416700), dec!(0.35)),
br(dec!(470700), dec!(0.396)),
],
},
);
ordinary.insert(
FilingStatus::HoH,
OrdinarySchedule {
brackets: vec![
br(dec!(0), dec!(0.10)),
br(dec!(13350), dec!(0.15)),
br(dec!(50800), dec!(0.25)),
br(dec!(131200), dec!(0.28)),
br(dec!(212500), dec!(0.33)),
br(dec!(416700), dec!(0.35)),
br(dec!(444550), dec!(0.396)),
],
},
);
ordinary.insert(
FilingStatus::Mfs,
OrdinarySchedule {
brackets: vec![
br(dec!(0), dec!(0.10)),
br(dec!(9325), dec!(0.15)),
br(dec!(37950), dec!(0.25)),
br(dec!(76550), dec!(0.28)),
br(dec!(116675), dec!(0.33)),
br(dec!(208350), dec!(0.35)),
br(dec!(235350), dec!(0.396)),
],
},
);
let mut ltcg = BTreeMap::new();
ltcg.insert(
FilingStatus::Single,
LtcgBreakpoints {
max_zero: dec!(37950),
max_fifteen: dec!(418400),
},
);
ltcg.insert(
FilingStatus::Mfj,
LtcgBreakpoints {
max_zero: dec!(75900),
max_fifteen: dec!(470700),
},
);
ltcg.insert(
FilingStatus::HoH,
LtcgBreakpoints {
max_zero: dec!(50800),
max_fifteen: dec!(444550),
},
);
ltcg.insert(
FilingStatus::Mfs,
LtcgBreakpoints {
max_zero: dec!(37950),
max_fifteen: dec!(235350),
},
);
TaxTable {
year: 2017,
source: "Rev. Proc. 2016-55 §2.01/§2.03 (TY2017, pre-TCJA 10/15/25/28/33/35/39.6%); \
SSA 2016-10-18 (ss_wage_base $127,200)",
ordinary,
ltcg,
gift_annual_exclusion: dec!(14000),
ss_wage_base: dec!(127200),
gift_lifetime_exclusion: dec!(5_490_000),
}
}
fn ty2024() -> TaxTable {
let mut ordinary = BTreeMap::new();
ordinary.insert(
FilingStatus::Single,
OrdinarySchedule {
brackets: vec![
br(dec!(0), dec!(0.10)),
br(dec!(11600), dec!(0.12)),
br(dec!(47150), dec!(0.22)),
br(dec!(100525), dec!(0.24)),
br(dec!(191950), dec!(0.32)),
br(dec!(243725), dec!(0.35)),
br(dec!(609350), dec!(0.37)),
],
},
);
ordinary.insert(
FilingStatus::Mfj,
OrdinarySchedule {
brackets: vec![
br(dec!(0), dec!(0.10)),
br(dec!(23200), dec!(0.12)),
br(dec!(94300), dec!(0.22)),
br(dec!(201050), dec!(0.24)),
br(dec!(383900), dec!(0.32)),
br(dec!(487450), dec!(0.35)),
br(dec!(731200), dec!(0.37)),
],
},
);
ordinary.insert(
FilingStatus::HoH,
OrdinarySchedule {
brackets: vec![
br(dec!(0), dec!(0.10)),
br(dec!(16550), dec!(0.12)),
br(dec!(63100), dec!(0.22)),
br(dec!(100500), dec!(0.24)),
br(dec!(191950), dec!(0.32)),
br(dec!(243700), dec!(0.35)),
br(dec!(609350), dec!(0.37)),
],
},
);
ordinary.insert(
FilingStatus::Mfs,
OrdinarySchedule {
brackets: vec![
br(dec!(0), dec!(0.10)),
br(dec!(11600), dec!(0.12)),
br(dec!(47150), dec!(0.22)),
br(dec!(100525), dec!(0.24)),
br(dec!(191950), dec!(0.32)),
br(dec!(243725), dec!(0.35)),
br(dec!(365600), dec!(0.37)),
],
},
);
let mut ltcg = BTreeMap::new();
ltcg.insert(
FilingStatus::Single,
LtcgBreakpoints {
max_zero: dec!(47025),
max_fifteen: dec!(518900),
},
);
ltcg.insert(
FilingStatus::Mfj,
LtcgBreakpoints {
max_zero: dec!(94050),
max_fifteen: dec!(583750),
},
);
ltcg.insert(
FilingStatus::HoH,
LtcgBreakpoints {
max_zero: dec!(63000),
max_fifteen: dec!(551350),
},
);
ltcg.insert(
FilingStatus::Mfs,
LtcgBreakpoints {
max_zero: dec!(47025),
max_fifteen: dec!(291850),
},
);
TaxTable {
year: 2024,
source: "Rev. Proc. 2023-34 §3.01/§3.03 + §3.43 + §3.41 (TY2024); \
SSA 2023-10-12 (ss_wage_base $168,600)",
ordinary,
ltcg,
gift_annual_exclusion: dec!(18000),
ss_wage_base: dec!(168600),
gift_lifetime_exclusion: dec!(13_610_000),
}
}
fn ty2025() -> TaxTable {
let mut ordinary = BTreeMap::new();
ordinary.insert(
FilingStatus::Single,
OrdinarySchedule {
brackets: vec![
br(dec!(0), dec!(0.10)),
br(dec!(11925), dec!(0.12)),
br(dec!(48475), dec!(0.22)),
br(dec!(103350), dec!(0.24)),
br(dec!(197300), dec!(0.32)),
br(dec!(250525), dec!(0.35)),
br(dec!(626350), dec!(0.37)),
],
},
);
ordinary.insert(
FilingStatus::Mfj,
OrdinarySchedule {
brackets: vec![
br(dec!(0), dec!(0.10)),
br(dec!(23850), dec!(0.12)),
br(dec!(96950), dec!(0.22)),
br(dec!(206700), dec!(0.24)),
br(dec!(394600), dec!(0.32)),
br(dec!(501050), dec!(0.35)),
br(dec!(751600), dec!(0.37)),
],
},
);
ordinary.insert(
FilingStatus::HoH,
OrdinarySchedule {
brackets: vec![
br(dec!(0), dec!(0.10)),
br(dec!(17000), dec!(0.12)),
br(dec!(64850), dec!(0.22)),
br(dec!(103350), dec!(0.24)),
br(dec!(197300), dec!(0.32)),
br(dec!(250500), dec!(0.35)),
br(dec!(626350), dec!(0.37)),
],
},
);
ordinary.insert(
FilingStatus::Mfs,
OrdinarySchedule {
brackets: vec![
br(dec!(0), dec!(0.10)),
br(dec!(11925), dec!(0.12)),
br(dec!(48475), dec!(0.22)),
br(dec!(103350), dec!(0.24)),
br(dec!(197300), dec!(0.32)),
br(dec!(250525), dec!(0.35)),
br(dec!(375800), dec!(0.37)),
],
},
);
let mut ltcg = BTreeMap::new();
ltcg.insert(
FilingStatus::Single,
LtcgBreakpoints {
max_zero: dec!(48350),
max_fifteen: dec!(533400),
},
);
ltcg.insert(
FilingStatus::Mfj,
LtcgBreakpoints {
max_zero: dec!(96700),
max_fifteen: dec!(600050),
},
);
ltcg.insert(
FilingStatus::HoH,
LtcgBreakpoints {
max_zero: dec!(64750),
max_fifteen: dec!(566700),
},
);
ltcg.insert(
FilingStatus::Mfs,
LtcgBreakpoints {
max_zero: dec!(48350),
max_fifteen: dec!(300000),
},
);
TaxTable {
year: 2025,
source: "Rev. Proc. 2024-40 §2.01/§2.03 + §2.43 + §2.41 (TY2025); OBBBA Pub. L. 119-21 \
left 2025 brackets/breakpoints unchanged",
ordinary,
ltcg,
gift_annual_exclusion: dec!(19000),
ss_wage_base: dec!(176100),
gift_lifetime_exclusion: dec!(13_990_000),
}
}
fn ty2026() -> TaxTable {
let mut ordinary = BTreeMap::new();
ordinary.insert(
FilingStatus::Single,
OrdinarySchedule {
brackets: vec![
br(dec!(0), dec!(0.10)),
br(dec!(12400), dec!(0.12)),
br(dec!(50400), dec!(0.22)),
br(dec!(105700), dec!(0.24)),
br(dec!(201775), dec!(0.32)),
br(dec!(256225), dec!(0.35)),
br(dec!(640600), dec!(0.37)),
],
},
);
ordinary.insert(
FilingStatus::Mfj,
OrdinarySchedule {
brackets: vec![
br(dec!(0), dec!(0.10)),
br(dec!(24800), dec!(0.12)),
br(dec!(100800), dec!(0.22)),
br(dec!(211400), dec!(0.24)),
br(dec!(403550), dec!(0.32)),
br(dec!(512450), dec!(0.35)),
br(dec!(768700), dec!(0.37)),
],
},
);
ordinary.insert(
FilingStatus::HoH,
OrdinarySchedule {
brackets: vec![
br(dec!(0), dec!(0.10)),
br(dec!(17700), dec!(0.12)),
br(dec!(67450), dec!(0.22)),
br(dec!(105700), dec!(0.24)),
br(dec!(201750), dec!(0.32)),
br(dec!(256200), dec!(0.35)),
br(dec!(640600), dec!(0.37)),
],
},
);
ordinary.insert(
FilingStatus::Mfs,
OrdinarySchedule {
brackets: vec![
br(dec!(0), dec!(0.10)),
br(dec!(12400), dec!(0.12)),
br(dec!(50400), dec!(0.22)),
br(dec!(105700), dec!(0.24)),
br(dec!(201775), dec!(0.32)),
br(dec!(256225), dec!(0.35)),
br(dec!(384350), dec!(0.37)),
],
},
);
let mut ltcg = BTreeMap::new();
ltcg.insert(
FilingStatus::Single,
LtcgBreakpoints {
max_zero: dec!(49450),
max_fifteen: dec!(545500),
},
);
ltcg.insert(
FilingStatus::Mfj,
LtcgBreakpoints {
max_zero: dec!(98900),
max_fifteen: dec!(613700),
},
);
ltcg.insert(
FilingStatus::HoH,
LtcgBreakpoints {
max_zero: dec!(66200),
max_fifteen: dec!(579600),
},
);
ltcg.insert(
FilingStatus::Mfs,
LtcgBreakpoints {
max_zero: dec!(49450),
max_fifteen: dec!(306850),
},
);
TaxTable {
year: 2026,
source: "Rev. Proc. 2025-32 §4.01/§4.03 + §4.42 (TY2026); §2010(c)(3) basic exclusion \
$15,000,000 per OBBBA Pub. L. 119-21 §70106; SS wage base $184,500 per SSA \
(Fed. Reg. 2025-11-03)",
ordinary,
ltcg,
gift_annual_exclusion: dec!(19000),
ss_wage_base: dec!(184500),
gift_lifetime_exclusion: dec!(15_000_000),
}
}
#[cfg(test)]
mod tests {
use super::*;
use btctax_core::tax::tables::{loss_limit, niit_threshold, NIIT_RATE};
use btctax_core::{
compute_tax_year, BasisSource, Disposal, DisposalLeg, DisposeKind, EventId, LedgerState,
LotId, Source, SourceRef, TaxOutcome, TaxProfile, Term, WalletId,
};
use rust_decimal_macros::dec;
use time::macros::date;
#[test]
fn all_bundled_years_are_tax_table_binnable() {
use btctax_core::tax::method::first_unbinnable_edge;
let t = BundledTaxTables::load();
let mut checked = 0;
for year in 2000..=2100 {
let Some(tbl) = t.table_for(year) else {
continue;
};
for status in [
FilingStatus::Single,
FilingStatus::Mfj,
FilingStatus::Mfs,
FilingStatus::HoH,
] {
assert_eq!(
first_unbinnable_edge(tbl.ordinary_for(status)),
None,
"year {year} {status:?}: a sub-$100k bracket edge is not a $25 multiple"
);
}
checked += 1;
}
assert!(
checked >= 4,
"expected every bundled year swept; checked {checked}"
);
}
#[test]
fn ty2024_full_return_params_bundled() {
let t = BundledFullReturnTables::load();
let p = t.full_return_for(2024).unwrap();
assert_eq!(p.std_deduction_for(FilingStatus::Single), dec!(14600));
assert_eq!(p.std_deduction_for(FilingStatus::Mfj), dec!(29200));
assert_eq!(p.std_deduction_for(FilingStatus::HoH), dec!(21900));
assert_eq!(p.std_deduction_for(FilingStatus::Qss), dec!(29200)); assert_eq!(p.std_aged_blind_married, dec!(1550));
assert_eq!(p.std_aged_blind_unmarried, dec!(1950));
assert_eq!(p.dependent_std_floor, dec!(1300));
assert_eq!(p.dependent_std_earned_addon, dec!(450));
assert_eq!(p.salt_cap, dec!(10000));
assert_eq!(p.kiddie_unearned_threshold, dec!(2600));
assert_eq!(p.elective_deferral_limit, dec!(23000));
assert_eq!(p.ftc_ceiling, dec!(300));
assert_eq!(p.qbi_ti_threshold_unmarried, dec!(191950)); assert_eq!(p.qbi_ti_threshold_married, dec!(383900));
assert_eq!(p.qbi_ti_threshold(FilingStatus::Qss), dec!(191950)); assert_eq!(p.amt.exemption(FilingStatus::Single), dec!(85700));
assert_eq!(p.amt.exemption(FilingStatus::Mfj), dec!(133300));
assert_eq!(p.amt.exemption(FilingStatus::Mfs), dec!(66650));
assert_eq!(p.amt.phaseout_start(FilingStatus::Single), dec!(609350));
assert_eq!(p.amt.phaseout_start(FilingStatus::Mfj), dec!(1218700));
assert_eq!(p.amt.breakpoint_28pct(FilingStatus::Single), dec!(232600));
assert_eq!(p.amt.breakpoint_28pct(FilingStatus::Mfs), dec!(116300));
assert!(t.full_return_for(2025).is_none()); assert!(t.full_return_for(2017).is_none());
}
#[test]
fn ty2025_single_ordinary_brackets_match_rev_proc_2024_40() {
let t = BundledTaxTables::load();
let s = t
.table_for(2025)
.unwrap()
.ordinary_for(FilingStatus::Single);
assert_eq!(s.brackets[1].lower, dec!(11925)); assert_eq!(s.brackets[2].lower, dec!(48475)); assert_eq!(s.brackets[6].lower, dec!(626350)); assert_eq!(s.brackets[6].rate, dec!(0.37));
}
#[test]
fn ty2025_ltcg_breakpoints_all_statuses() {
let t = BundledTaxTables::load();
let tt = t.table_for(2025).unwrap();
assert_eq!(
*tt.ltcg_for(FilingStatus::Single),
LtcgBreakpoints {
max_zero: dec!(48350),
max_fifteen: dec!(533400)
}
);
assert_eq!(
*tt.ltcg_for(FilingStatus::Mfj),
LtcgBreakpoints {
max_zero: dec!(96700),
max_fifteen: dec!(600050)
}
);
assert_eq!(
*tt.ltcg_for(FilingStatus::Qss),
LtcgBreakpoints {
max_zero: dec!(96700),
max_fifteen: dec!(600050)
}
);
assert_eq!(
*tt.ltcg_for(FilingStatus::HoH),
LtcgBreakpoints {
max_zero: dec!(64750),
max_fifteen: dec!(566700)
}
);
assert_eq!(
*tt.ltcg_for(FilingStatus::Mfs),
LtcgBreakpoints {
max_zero: dec!(48350),
max_fifteen: dec!(300000)
}
);
}
#[test]
fn mfs_37_pct_starts_at_375800_and_mfj_at_751600() {
let t = BundledTaxTables::load();
let tt = t.table_for(2025).unwrap();
assert_eq!(
tt.ordinary_for(FilingStatus::Mfs)
.brackets
.last()
.unwrap()
.lower,
dec!(375800)
);
assert_eq!(
tt.ordinary_for(FilingStatus::Mfj)
.brackets
.last()
.unwrap()
.lower,
dec!(751600)
);
}
#[test]
fn missing_year_returns_none() {
assert!(BundledTaxTables::load().table_for(2099).is_none());
}
#[test]
fn ty2025_gift_annual_exclusion_is_19000() {
let t = BundledTaxTables::load();
assert_eq!(
t.table_for(2025).unwrap().gift_annual_exclusion,
dec!(19000)
);
}
#[test]
fn statutory_values_are_not_in_the_table_and_constant_across_years() {
assert_eq!(niit_threshold(FilingStatus::Mfj), dec!(250000));
assert_eq!(loss_limit(FilingStatus::Mfs), dec!(1500));
assert_eq!(NIIT_RATE, dec!(0.038));
let t = BundledTaxTables::load();
let s25 = t
.table_for(2025)
.unwrap()
.ordinary_for(FilingStatus::Single);
let s26 = t
.table_for(2026)
.unwrap()
.ordinary_for(FilingStatus::Single);
assert_ne!(s25.brackets[1].lower, s26.brackets[1].lower); assert_ne!(
t.table_for(2025)
.unwrap()
.ltcg_for(FilingStatus::Single)
.max_zero,
t.table_for(2026)
.unwrap()
.ltcg_for(FilingStatus::Single)
.max_zero
);
assert_eq!(niit_threshold(FilingStatus::Mfj), dec!(250000));
assert_eq!(NIIT_RATE, dec!(0.038));
}
#[test]
fn ty2026_single_ordinary_brackets_match_rev_proc_2025_32() {
let t = BundledTaxTables::load();
let s = t
.table_for(2026)
.unwrap()
.ordinary_for(FilingStatus::Single);
assert_eq!(s.brackets[1].lower, dec!(12400)); assert_eq!(s.brackets[2].lower, dec!(50400)); assert_eq!(s.brackets[3].lower, dec!(105700)); assert_eq!(s.brackets[4].lower, dec!(201775)); assert_eq!(s.brackets[5].lower, dec!(256225)); assert_eq!(s.brackets[6].lower, dec!(640600)); assert_eq!(s.brackets[6].rate, dec!(0.37));
}
#[test]
fn ty2026_mfj_ordinary_brackets_match_rev_proc_2025_32() {
let t = BundledTaxTables::load();
let s = t.table_for(2026).unwrap().ordinary_for(FilingStatus::Mfj);
assert_eq!(s.brackets[1].lower, dec!(24800)); assert_eq!(s.brackets[2].lower, dec!(100800)); assert_eq!(s.brackets[3].lower, dec!(211400)); assert_eq!(s.brackets[4].lower, dec!(403550)); assert_eq!(s.brackets[5].lower, dec!(512450)); assert_eq!(s.brackets[6].lower, dec!(768700)); assert_eq!(s.brackets[6].rate, dec!(0.37));
}
#[test]
fn ty2026_hoh_ordinary_brackets_match_rev_proc_2025_32() {
let t = BundledTaxTables::load();
let s = t.table_for(2026).unwrap().ordinary_for(FilingStatus::HoH);
assert_eq!(s.brackets[1].lower, dec!(17700)); assert_eq!(s.brackets[2].lower, dec!(67450)); assert_eq!(s.brackets[3].lower, dec!(105700)); assert_eq!(s.brackets[4].lower, dec!(201750)); assert_eq!(s.brackets[5].lower, dec!(256200)); assert_eq!(s.brackets[6].lower, dec!(640600)); assert_eq!(s.brackets[6].rate, dec!(0.37));
}
#[test]
fn ty2026_mfs_37_pct_starts_at_384350() {
let t = BundledTaxTables::load();
let s = t.table_for(2026).unwrap().ordinary_for(FilingStatus::Mfs);
assert_eq!(s.brackets[1].lower, dec!(12400)); assert_eq!(s.brackets[4].lower, dec!(201775)); assert_eq!(s.brackets[5].lower, dec!(256225)); assert_eq!(s.brackets.last().unwrap().lower, dec!(384350)); assert_eq!(s.brackets.last().unwrap().rate, dec!(0.37));
}
#[test]
fn ty2026_ltcg_breakpoints_all_statuses() {
let t = BundledTaxTables::load();
let tt = t.table_for(2026).unwrap();
assert_eq!(
*tt.ltcg_for(FilingStatus::Single),
LtcgBreakpoints {
max_zero: dec!(49450),
max_fifteen: dec!(545500)
}
);
assert_eq!(
*tt.ltcg_for(FilingStatus::Mfj),
LtcgBreakpoints {
max_zero: dec!(98900),
max_fifteen: dec!(613700)
}
);
assert_eq!(
*tt.ltcg_for(FilingStatus::Qss),
LtcgBreakpoints {
max_zero: dec!(98900),
max_fifteen: dec!(613700)
}
);
assert_eq!(
*tt.ltcg_for(FilingStatus::HoH),
LtcgBreakpoints {
max_zero: dec!(66200),
max_fifteen: dec!(579600)
}
);
assert_eq!(
*tt.ltcg_for(FilingStatus::Mfs),
LtcgBreakpoints {
max_zero: dec!(49450),
max_fifteen: dec!(306850)
}
);
}
#[test]
fn ty2026_gift_annual_exclusion_is_19000() {
let t = BundledTaxTables::load();
assert_eq!(
t.table_for(2026).unwrap().gift_annual_exclusion,
dec!(19000)
);
}
#[test]
fn ty2026_ss_wage_base_is_184500() {
let t = BundledTaxTables::load();
assert_eq!(t.table_for(2026).unwrap().ss_wage_base, dec!(184500));
}
#[test]
fn ty2026_lifetime_exclusion_is_15_million() {
let t = BundledTaxTables::load();
assert_eq!(
t.table_for(2026).unwrap().gift_lifetime_exclusion,
dec!(15_000_000)
);
}
#[test]
fn ty2026_table_is_available() {
assert!(BundledTaxTables::load().table_for(2026).is_some());
}
#[test]
fn ty2024_and_2025_tables_unchanged() {
let t = BundledTaxTables::load();
let t24 = t.table_for(2024).unwrap();
assert_eq!(
t24.ordinary_for(FilingStatus::Single).brackets[6].lower,
dec!(609350)
);
assert_eq!(
*t24.ltcg_for(FilingStatus::Single),
LtcgBreakpoints {
max_zero: dec!(47025),
max_fifteen: dec!(518900)
}
);
assert_eq!(t24.gift_annual_exclusion, dec!(18000));
assert_eq!(t24.ss_wage_base, dec!(168600));
assert_eq!(t24.gift_lifetime_exclusion, dec!(13_610_000));
let t25 = t.table_for(2025).unwrap();
assert_eq!(
t25.ordinary_for(FilingStatus::Single).brackets[6].lower,
dec!(626350)
);
assert_eq!(
*t25.ltcg_for(FilingStatus::Single),
LtcgBreakpoints {
max_zero: dec!(48350),
max_fifteen: dec!(533400)
}
);
assert_eq!(t25.gift_annual_exclusion, dec!(19000));
assert_eq!(t25.ss_wage_base, dec!(176100));
assert_eq!(t25.gift_lifetime_exclusion, dec!(13_990_000));
}
#[test]
fn ty2017_table_matches_rev_proc_2016_55() {
let t = BundledTaxTables::load();
let tt = t.table_for(2017).unwrap();
let single: [(Usd, Usd); 7] = [
(dec!(0), dec!(0.10)),
(dec!(9325), dec!(0.15)),
(dec!(37950), dec!(0.25)),
(dec!(91900), dec!(0.28)),
(dec!(191650), dec!(0.33)),
(dec!(416700), dec!(0.35)),
(dec!(418400), dec!(0.396)),
];
let mfj: [(Usd, Usd); 7] = [
(dec!(0), dec!(0.10)),
(dec!(18650), dec!(0.15)),
(dec!(75900), dec!(0.25)),
(dec!(153100), dec!(0.28)),
(dec!(233350), dec!(0.33)),
(dec!(416700), dec!(0.35)),
(dec!(470700), dec!(0.396)),
];
let hoh: [(Usd, Usd); 7] = [
(dec!(0), dec!(0.10)),
(dec!(13350), dec!(0.15)),
(dec!(50800), dec!(0.25)),
(dec!(131200), dec!(0.28)),
(dec!(212500), dec!(0.33)),
(dec!(416700), dec!(0.35)),
(dec!(444550), dec!(0.396)),
];
let mfs: [(Usd, Usd); 7] = [
(dec!(0), dec!(0.10)),
(dec!(9325), dec!(0.15)),
(dec!(37950), dec!(0.25)),
(dec!(76550), dec!(0.28)),
(dec!(116675), dec!(0.33)),
(dec!(208350), dec!(0.35)),
(dec!(235350), dec!(0.396)),
];
for (status, expected) in [
(FilingStatus::Single, &single),
(FilingStatus::Mfj, &mfj),
(FilingStatus::HoH, &hoh),
(FilingStatus::Mfs, &mfs),
] {
let sched = tt.ordinary_for(status);
assert_eq!(
sched.brackets.len(),
7,
"{status:?}: TY2017 must have exactly 7 ordinary brackets"
);
for (i, (lower, rate)) in expected.iter().enumerate() {
assert_eq!(
sched.brackets[i].lower, *lower,
"{status:?} bracket[{i}] lower must match Rev. Proc. 2016-55 §2.01 verbatim"
);
assert_eq!(
sched.brackets[i].rate, *rate,
"{status:?} bracket[{i}] rate must match Rev. Proc. 2016-55 §2.01 verbatim"
);
}
}
for (status, max_zero, max_fifteen) in [
(FilingStatus::Single, dec!(37950), dec!(418400)),
(FilingStatus::Mfj, dec!(75900), dec!(470700)),
(FilingStatus::HoH, dec!(50800), dec!(444550)),
(FilingStatus::Mfs, dec!(37950), dec!(235350)),
] {
assert_eq!(
*tt.ltcg_for(status),
LtcgBreakpoints {
max_zero,
max_fifteen
},
"{status:?} LTCG pair must match Rev. Proc. 2016-55 §2.03 verbatim"
);
}
assert_eq!(
tt.ss_wage_base,
dec!(127200),
"TY2017 SS wage base must be $127,200 (SSA 2016-10-18)"
);
assert!(!tt.ordinary.contains_key(&FilingStatus::Qss));
assert_eq!(
tt.ordinary_for(FilingStatus::Qss),
tt.ordinary_for(FilingStatus::Mfj)
);
}
#[test]
fn ty2017_ancillary_fields() {
let t = BundledTaxTables::load();
let tt = t.table_for(2017).unwrap();
assert_eq!(tt.gift_annual_exclusion, dec!(14000));
assert_eq!(tt.gift_lifetime_exclusion, dec!(5_490_000));
assert_eq!(tt.ss_wage_base, dec!(127200));
}
#[test]
fn ty2017_table_is_available() {
assert!(BundledTaxTables::load().table_for(2017).is_some());
}
fn kat24_eid(n: u64) -> EventId {
EventId::import(Source::Coinbase, SourceRef::new(format!("kat24-{n}")))
}
fn kat24_lot(n: u64) -> LotId {
LotId {
origin_event_id: kat24_eid(n),
split_sequence: 0,
}
}
fn leg24(gain: Usd, term: Term) -> DisposalLeg {
let proceeds = if gain >= dec!(0) { gain } else { dec!(0) };
let basis = proceeds - gain;
DisposalLeg {
lot_id: kat24_lot(1),
sat: 1,
proceeds,
basis,
gain,
term,
basis_source: BasisSource::ExchangeProvided,
gift_zone: None,
acquired_at: date!(2024 - 01 - 01),
wallet: WalletId::Exchange {
provider: "cb".into(),
account: "m".into(),
},
pseudo: false,
}
}
fn state24_with_legs(legs: Vec<DisposalLeg>) -> LedgerState {
LedgerState {
disposals: vec![Disposal {
event: kat24_eid(0),
kind: DisposeKind::Sell,
disposed_at: date!(2024 - 06 - 15),
legs,
fee_mini_disposition: false,
}],
..LedgerState::default()
}
}
fn state24_st(gain: Usd) -> LedgerState {
state24_with_legs(vec![leg24(gain, Term::ShortTerm)])
}
fn state24_lt(gain: Usd) -> LedgerState {
state24_with_legs(vec![leg24(gain, Term::LongTerm)])
}
fn p24_single(ord: Usd, magi: Usd) -> TaxProfile {
TaxProfile {
filing_status: FilingStatus::Single,
ordinary_taxable_income: ord,
magi_excluding_crypto: magi,
qualified_dividends_and_other_pref_income: dec!(0),
other_net_capital_gain: dec!(0),
capital_loss_carryforward_in: Default::default(),
w2_ss_wages: dec!(0),
w2_medicare_wages: dec!(0),
schedule_c_expenses: Usd::ZERO,
}
}
fn p24_mfj(ord: Usd, magi: Usd) -> TaxProfile {
TaxProfile {
filing_status: FilingStatus::Mfj,
ordinary_taxable_income: ord,
magi_excluding_crypto: magi,
qualified_dividends_and_other_pref_income: dec!(0),
other_net_capital_gain: dec!(0),
capital_loss_carryforward_in: Default::default(),
w2_ss_wages: dec!(0),
w2_medicare_wages: dec!(0),
schedule_c_expenses: Usd::ZERO,
}
}
fn p24_hoh(ord: Usd, magi: Usd) -> TaxProfile {
TaxProfile {
filing_status: FilingStatus::HoH,
ordinary_taxable_income: ord,
magi_excluding_crypto: magi,
qualified_dividends_and_other_pref_income: dec!(0),
other_net_capital_gain: dec!(0),
capital_loss_carryforward_in: Default::default(),
w2_ss_wages: dec!(0),
w2_medicare_wages: dec!(0),
schedule_c_expenses: Usd::ZERO,
}
}
fn p24_mfs(ord: Usd, magi: Usd) -> TaxProfile {
TaxProfile {
filing_status: FilingStatus::Mfs,
ordinary_taxable_income: ord,
magi_excluding_crypto: magi,
qualified_dividends_and_other_pref_income: dec!(0),
other_net_capital_gain: dec!(0),
capital_loss_carryforward_in: Default::default(),
w2_ss_wages: dec!(0),
w2_medicare_wages: dec!(0),
schedule_c_expenses: Usd::ZERO,
}
}
fn computed24(state: LedgerState, profile: TaxProfile) -> btctax_core::TaxResult {
match compute_tax_year(&[], &state, 2024, Some(&profile), &BundledTaxTables::load()) {
TaxOutcome::Computed(r) => r,
TaxOutcome::NotComputable(b) => panic!("unexpected not-computable: {:?}", b),
}
}
#[test]
fn ty2024_single_ordinary_brackets_match_rev_proc_2023_34() {
let t = BundledTaxTables::load();
let s = t
.table_for(2024)
.unwrap()
.ordinary_for(FilingStatus::Single);
assert_eq!(s.brackets[1].lower, dec!(11600)); assert_eq!(s.brackets[2].lower, dec!(47150)); assert_eq!(s.brackets[6].lower, dec!(609350)); assert_eq!(s.brackets[6].rate, dec!(0.37));
}
#[test]
fn ty2024_mfs_37_pct_starts_at_365600_and_mfj_at_731200() {
let t = BundledTaxTables::load();
let tt = t.table_for(2024).unwrap();
assert_eq!(
tt.ordinary_for(FilingStatus::Mfs)
.brackets
.last()
.unwrap()
.lower,
dec!(365600)
);
assert_eq!(
tt.ordinary_for(FilingStatus::Mfj)
.brackets
.last()
.unwrap()
.lower,
dec!(731200)
);
}
#[test]
fn ty2024_ltcg_breakpoints_all_statuses() {
let t = BundledTaxTables::load();
let tt = t.table_for(2024).unwrap();
assert_eq!(
*tt.ltcg_for(FilingStatus::Single),
LtcgBreakpoints {
max_zero: dec!(47025),
max_fifteen: dec!(518900)
}
);
assert_eq!(
*tt.ltcg_for(FilingStatus::Mfj),
LtcgBreakpoints {
max_zero: dec!(94050),
max_fifteen: dec!(583750)
}
);
assert_eq!(
*tt.ltcg_for(FilingStatus::Qss),
LtcgBreakpoints {
max_zero: dec!(94050),
max_fifteen: dec!(583750)
}
);
assert_eq!(
*tt.ltcg_for(FilingStatus::HoH),
LtcgBreakpoints {
max_zero: dec!(63000),
max_fifteen: dec!(551350)
}
);
assert_eq!(
*tt.ltcg_for(FilingStatus::Mfs),
LtcgBreakpoints {
max_zero: dec!(47025),
max_fifteen: dec!(291850)
}
);
}
#[test]
fn ty2024_ancillary_fields() {
let t = BundledTaxTables::load();
let tt = t.table_for(2024).unwrap();
assert_eq!(tt.gift_annual_exclusion, dec!(18000));
assert_eq!(tt.ss_wage_base, dec!(168600));
assert_eq!(tt.gift_lifetime_exclusion, dec!(13_610_000));
}
#[test]
fn ty2024_table_is_available() {
assert!(BundledTaxTables::load().table_for(2024).is_some());
}
#[test]
fn ty2024_a6a_single_22pct_bracket_entry() {
let r = computed24(state24_st(dec!(1000)), p24_single(dec!(47150), dec!(47150)));
assert_eq!(r.total_federal_tax_attributable, dec!(220.00));
assert_eq!(r.niit, dec!(0));
}
#[test]
fn ty2024_a6b_mfj_22_24_boundary() {
let r = computed24(state24_st(dec!(2000)), p24_mfj(dec!(200000), dec!(200000)));
assert_eq!(r.total_federal_tax_attributable, dec!(459.00));
assert_eq!(r.niit, dec!(0));
}
#[test]
fn ty2024_a6c_hoh_12_22_boundary() {
let r = computed24(state24_st(dec!(500)), p24_hoh(dec!(63000), dec!(63000)));
assert_eq!(r.total_federal_tax_attributable, dec!(100.00));
assert_eq!(r.niit, dec!(0));
}
#[test]
fn ty2024_a6d_mfs_35_37_boundary_with_niit() {
let r = computed24(state24_st(dec!(1000)), p24_mfs(dec!(365000), dec!(365000)));
assert_eq!(r.total_federal_tax_attributable, dec!(396.00));
assert_eq!(r.niit, dec!(38.00));
}
#[test]
fn ty2024_a7_single_ltcg_0_to_15_threshold() {
let r = computed24(
state24_lt(dec!(10000)),
p24_single(dec!(40000), dec!(40000)),
);
assert_eq!(r.ltcg_tax, dec!(446.25));
assert_eq!(r.total_federal_tax_attributable, dec!(446.25));
assert_eq!(r.niit, dec!(0));
}
#[test]
fn ty2024_full_schedule_equality_all_28_edges_and_ltcg() {
let t = BundledTaxTables::load();
let tt = t.table_for(2024).unwrap();
let single: [(Usd, Usd); 7] = [
(dec!(0), dec!(0.10)),
(dec!(11600), dec!(0.12)),
(dec!(47150), dec!(0.22)),
(dec!(100525), dec!(0.24)),
(dec!(191950), dec!(0.32)),
(dec!(243725), dec!(0.35)),
(dec!(609350), dec!(0.37)),
];
let mfj: [(Usd, Usd); 7] = [
(dec!(0), dec!(0.10)),
(dec!(23200), dec!(0.12)),
(dec!(94300), dec!(0.22)),
(dec!(201050), dec!(0.24)),
(dec!(383900), dec!(0.32)),
(dec!(487450), dec!(0.35)),
(dec!(731200), dec!(0.37)),
];
let hoh: [(Usd, Usd); 7] = [
(dec!(0), dec!(0.10)),
(dec!(16550), dec!(0.12)),
(dec!(63100), dec!(0.22)),
(dec!(100500), dec!(0.24)),
(dec!(191950), dec!(0.32)),
(dec!(243700), dec!(0.35)),
(dec!(609350), dec!(0.37)),
];
let mfs: [(Usd, Usd); 7] = [
(dec!(0), dec!(0.10)),
(dec!(11600), dec!(0.12)),
(dec!(47150), dec!(0.22)),
(dec!(100525), dec!(0.24)),
(dec!(191950), dec!(0.32)),
(dec!(243725), dec!(0.35)),
(dec!(365600), dec!(0.37)),
];
for (status, expected) in [
(FilingStatus::Single, &single),
(FilingStatus::Mfj, &mfj),
(FilingStatus::HoH, &hoh),
(FilingStatus::Mfs, &mfs),
] {
let sched = tt.ordinary_for(status);
assert_eq!(
sched.brackets.len(),
7,
"{status:?}: TY2024 must have exactly 7 ordinary brackets"
);
for (i, (lower, rate)) in expected.iter().enumerate() {
assert_eq!(
sched.brackets[i].lower, *lower,
"{status:?} bracket[{i}] lower must match Rev. Proc. 2023-34 §3.01 verbatim"
);
assert_eq!(
sched.brackets[i].rate, *rate,
"{status:?} bracket[{i}] rate must match Rev. Proc. 2023-34 §3.01 verbatim"
);
}
}
for (status, max_zero, max_fifteen) in [
(FilingStatus::Single, dec!(47025), dec!(518900)),
(FilingStatus::Mfj, dec!(94050), dec!(583750)),
(FilingStatus::HoH, dec!(63000), dec!(551350)),
(FilingStatus::Mfs, dec!(47025), dec!(291850)),
] {
assert_eq!(
*tt.ltcg_for(status),
LtcgBreakpoints {
max_zero,
max_fifteen
},
"{status:?} LTCG pair must match Rev. Proc. 2023-34 §3.03 verbatim"
);
}
assert!(
!tt.ordinary.contains_key(&FilingStatus::Qss),
"Qss must not be a stored ordinary key (aliases Mfj at lookup)"
);
assert!(
!tt.ltcg.contains_key(&FilingStatus::Qss),
"Qss must not be a stored ltcg key (aliases Mfj at lookup)"
);
assert_eq!(
tt.ordinary_for(FilingStatus::Qss),
tt.ordinary_for(FilingStatus::Mfj),
"Qss ordinary lookup must alias the MFJ schedule"
);
}
}