use btctax_core::tax::tables::{
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(2024, ty2024());
by_year.insert(2025, ty2025());
Self { by_year }
}
}
impl TaxTables for BundledTaxTables {
fn table_for(&self, year: i32) -> Option<&TaxTable> {
self.by_year.get(&year)
}
}
fn br(lower: Usd, rate: Usd) -> OrdinaryBracket {
OrdinaryBracket { lower, rate }
}
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),
}
}
#[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 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));
}
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(),
},
}
}
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"
);
}
}