use crate::conventions::{round_cents, Usd};
use crate::event::IncomeKind;
use crate::state::LedgerState;
use crate::tax::tables::{
se_addl_medicare_threshold, TaxTable, SE_NET_EARNINGS_FACTOR, SE_RATE_ADDL_MEDICARE,
SE_RATE_MEDICARE, SE_RATE_SS,
};
use crate::tax::types::FilingStatus;
use rust_decimal_macros::dec;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SeTaxResult {
pub net_se: Usd,
pub base: Usd,
pub ss: Usd,
pub medicare: Usd,
pub addl: Usd,
pub total: Usd,
pub deductible_half: Usd,
}
pub fn se_net_income(state: &LedgerState, year: i32) -> Usd {
state
.income_recognized
.iter()
.filter(|i| i.business && i.kind != IncomeKind::Interest && i.recognized_at.year() == year)
.map(|i| i.usd_fmv)
.sum()
}
pub fn compute_se_tax(
state: &LedgerState,
year: i32,
status: FilingStatus,
table: &TaxTable,
w2_ss_wages: Usd,
w2_medicare_wages: Usd,
schedule_c_expenses: Usd,
) -> Option<SeTaxResult> {
let gross_se = se_net_income(state, year);
let net_se = {
let n = gross_se - schedule_c_expenses;
if n <= Usd::ZERO {
Usd::ZERO
} else {
n
}
};
if net_se.is_zero() {
return None;
}
let base = round_cents(net_se * SE_NET_EARNINGS_FACTOR);
let ss_cap = {
let c = table.ss_wage_base - w2_ss_wages;
if c < Usd::ZERO {
Usd::ZERO
} else {
c
}
};
let ss_taxable = if base < ss_cap { base } else { ss_cap };
let ss = round_cents(SE_RATE_SS * ss_taxable);
let medicare = round_cents(SE_RATE_MEDICARE * base);
let addl_threshold = {
let t = se_addl_medicare_threshold(status) - w2_medicare_wages;
if t < Usd::ZERO {
Usd::ZERO
} else {
t
}
};
let over = {
let o = base - addl_threshold;
if o < Usd::ZERO {
Usd::ZERO
} else {
o
}
};
let addl = round_cents(SE_RATE_ADDL_MEDICARE * over);
let total = ss + medicare + addl;
let deductible_half = round_cents((ss + medicare) / dec!(2));
Some(SeTaxResult {
net_se,
base,
ss,
medicare,
addl,
total,
deductible_half,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::conventions::TaxDate;
use crate::identity::EventId;
use crate::state::IncomeRecord;
use crate::tax::tables::synthetic_table;
use time::macros::date;
fn income(kind: IncomeKind, business: bool, fmv: Usd, d: TaxDate) -> IncomeRecord {
IncomeRecord {
event: EventId::decision(1),
recognized_at: d,
sat: 100_000_000,
usd_fmv: fmv,
kind,
business,
}
}
fn state_with(income: Vec<IncomeRecord>) -> LedgerState {
LedgerState {
income_recognized: income,
..Default::default()
}
}
fn tbl() -> TaxTable {
synthetic_table(2025)
}
#[test]
fn golden1_single_100k_business_mining() {
let st = state_with(vec![income(
IncomeKind::Mining,
true,
dec!(100000),
date!(2025 - 03 - 01),
)]);
let r = compute_se_tax(
&st,
2025,
FilingStatus::Single,
&tbl(),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
)
.expect("SE tax expected");
assert_eq!(r.net_se, dec!(100000));
assert_eq!(r.base, dec!(92350.00));
assert_eq!(r.ss, dec!(11451.40));
assert_eq!(r.medicare, dec!(2678.15));
assert_eq!(r.addl, dec!(0.00));
assert_eq!(r.total, dec!(14129.55));
assert_eq!(r.deductible_half, dec!(7064.78));
}
#[test]
fn c1_lock_single_300k_addl_medicare_and_deductible_half_excludes_addl() {
let st = state_with(vec![income(
IncomeKind::Mining,
true,
dec!(300000),
date!(2025 - 06 - 15),
)]);
let r = compute_se_tax(
&st,
2025,
FilingStatus::Single,
&tbl(),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
)
.expect("SE tax expected");
assert_eq!(r.base, dec!(277050.00));
assert_eq!(r.ss, dec!(21836.40)); assert_eq!(r.medicare, dec!(8034.45));
assert_eq!(r.addl, dec!(693.45)); assert_eq!(r.total, dec!(30564.30));
assert_eq!(r.deductible_half, dec!(14935.42));
assert_ne!(r.deductible_half, dec!(15282.15));
}
#[test]
fn wage_base_cap_250k_single() {
let st = state_with(vec![income(
IncomeKind::Mining,
true,
dec!(250000),
date!(2025 - 02 - 01),
)]);
let r = compute_se_tax(
&st,
2025,
FilingStatus::Single,
&tbl(),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
)
.expect("SE tax expected");
assert_eq!(r.base, dec!(230875.00));
assert_eq!(r.ss, dec!(21836.40));
}
#[test]
fn mfs_uses_125k_addl_medicare_threshold() {
assert_eq!(se_addl_medicare_threshold(FilingStatus::Mfs), dec!(125000));
let st = state_with(vec![income(
IncomeKind::Mining,
true,
dec!(200000),
date!(2025 - 04 - 01),
)]);
let r = compute_se_tax(
&st,
2025,
FilingStatus::Mfs,
&tbl(),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
)
.expect("SE tax expected");
assert_eq!(r.base, dec!(184700.00));
assert_eq!(r.ss, dec!(21836.40)); assert_eq!(r.medicare, dec!(5356.30));
assert_eq!(r.addl, dec!(537.30));
assert_eq!(r.total, dec!(27730.00));
}
#[test]
fn m2_business_interest_excluded_mining_included() {
let st = state_with(vec![
income(
IncomeKind::Mining,
true,
dec!(100000),
date!(2025 - 03 - 01),
),
income(
IncomeKind::Interest,
true,
dec!(50000),
date!(2025 - 03 - 02),
),
]);
let r = compute_se_tax(
&st,
2025,
FilingStatus::Single,
&tbl(),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
)
.expect("SE tax expected");
assert_eq!(r.net_se, dec!(100000));
assert_eq!(r.base, dec!(92350.00));
assert_eq!(r.total, dec!(14129.55));
}
#[test]
fn m3_fractional_base_rounds_at_cent() {
let st = state_with(vec![income(
IncomeKind::Mining,
true,
dec!(12345.67),
date!(2025 - 05 - 05),
)]);
let r = compute_se_tax(
&st,
2025,
FilingStatus::Single,
&tbl(),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
)
.expect("SE tax expected");
assert_eq!(r.base, dec!(11401.23)); assert_eq!(r.ss, dec!(1413.75)); assert_eq!(r.medicare, dec!(330.64)); assert_eq!(r.addl, dec!(0.00));
assert_eq!(r.total, dec!(1744.39));
assert_eq!(r.deductible_half, dec!(872.20)); }
#[test]
fn no_business_income_is_none() {
let st = state_with(vec![income(
IncomeKind::Mining,
false,
dec!(100000),
date!(2025 - 03 - 01),
)]);
assert!(compute_se_tax(
&st,
2025,
FilingStatus::Single,
&tbl(),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
)
.is_none());
}
#[test]
fn hobby_mining_excluded_even_with_business_interest() {
let st = state_with(vec![
income(
IncomeKind::Mining,
false,
dec!(100000),
date!(2025 - 03 - 01),
),
income(
IncomeKind::Interest,
true,
dec!(50000),
date!(2025 - 03 - 02),
),
]);
assert!(compute_se_tax(
&st,
2025,
FilingStatus::Single,
&tbl(),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
)
.is_none());
}
#[test]
fn only_this_years_business_income_counts() {
let st = state_with(vec![
income(
IncomeKind::Mining,
true,
dec!(100000),
date!(2025 - 03 - 01),
),
income(
IncomeKind::Mining,
true,
dec!(999999),
date!(2024 - 03 - 01),
),
]);
let r = compute_se_tax(
&st,
2025,
FilingStatus::Single,
&tbl(),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
)
.expect("SE tax expected");
assert_eq!(r.net_se, dec!(100000)); }
#[test]
fn w2_both_directions_headline_150k_ss_150k_medicare() {
let st = state_with(vec![income(
IncomeKind::Mining,
true,
dec!(100000),
date!(2025 - 03 - 01),
)]);
let r = compute_se_tax(
&st,
2025,
FilingStatus::Single,
&tbl(),
dec!(150000),
dec!(150000),
Usd::ZERO,
)
.expect("SE tax expected");
assert_eq!(r.base, dec!(92350.00));
assert_eq!(r.ss, dec!(3236.40));
assert_eq!(r.medicare, dec!(2678.15));
assert_eq!(r.addl, dec!(381.15));
assert_eq!(r.total, dec!(6295.70));
assert_eq!(r.deductible_half, dec!(2957.28));
}
#[test]
fn w2_ss_above_wage_base_180k() {
let st = state_with(vec![income(
IncomeKind::Mining,
true,
dec!(100000),
date!(2025 - 03 - 01),
)]);
let r = compute_se_tax(
&st,
2025,
FilingStatus::Single,
&tbl(),
dec!(180000),
Usd::ZERO,
Usd::ZERO,
)
.expect("SE tax expected");
assert_eq!(r.base, dec!(92350.00));
assert_eq!(r.ss, dec!(0.00));
assert_eq!(r.medicare, dec!(2678.15));
assert_eq!(r.addl, dec!(0.00));
assert_eq!(r.total, dec!(2678.15));
assert_eq!(r.deductible_half, dec!(1339.08));
}
#[test]
fn w2_medicare_above_threshold_isolated_250k() {
let st = state_with(vec![income(
IncomeKind::Mining,
true,
dec!(100000),
date!(2025 - 03 - 01),
)]);
let r = compute_se_tax(
&st,
2025,
FilingStatus::Single,
&tbl(),
Usd::ZERO,
dec!(250000),
Usd::ZERO,
)
.expect("SE tax expected");
assert_eq!(r.base, dec!(92350.00));
assert_eq!(r.ss, dec!(11451.40));
assert_eq!(r.medicare, dec!(2678.15));
assert_eq!(r.addl, dec!(831.15));
assert_eq!(r.total, dec!(14960.70));
assert_eq!(r.deductible_half, dec!(7064.78));
}
#[test]
fn w2_asymmetric_transposition_guard_150k_ss_0_medicare() {
let st = state_with(vec![income(
IncomeKind::Mining,
true,
dec!(100000),
date!(2025 - 03 - 01),
)]);
let r = compute_se_tax(
&st,
2025,
FilingStatus::Single,
&tbl(),
dec!(150000),
Usd::ZERO,
Usd::ZERO,
)
.expect("SE tax expected");
assert_eq!(
r.ss,
dec!(3236.40),
"ss must be reduced (not 11451.40 — transposition check)"
);
assert_eq!(
r.addl,
dec!(0.00),
"addl must be 0 (not 381.15 — transposition check)"
);
}
#[test]
fn chunkb_headline_expenses_20k_no_w2() {
let st = state_with(vec![income(
IncomeKind::Mining,
true,
dec!(100000),
date!(2025 - 03 - 01),
)]);
let r = compute_se_tax(
&st,
2025,
FilingStatus::Single,
&tbl(),
Usd::ZERO,
Usd::ZERO,
dec!(20000),
)
.expect("SE tax expected");
assert_eq!(r.net_se, dec!(80000));
assert_eq!(r.base, dec!(73880.00));
assert_eq!(r.ss, dec!(9161.12));
assert_eq!(r.medicare, dec!(2142.52));
assert_eq!(r.addl, dec!(0.00));
assert_eq!(r.total, dec!(11303.64));
assert_eq!(r.deductible_half, dec!(5651.82));
}
#[test]
fn chunkb_fully_expensed_mining_10k_expenses_15k_is_none() {
let st = state_with(vec![income(
IncomeKind::Mining,
true,
dec!(10000),
date!(2025 - 06 - 01),
)]);
assert!(
compute_se_tax(
&st,
2025,
FilingStatus::Single,
&tbl(),
Usd::ZERO,
Usd::ZERO,
dec!(15000),
)
.is_none(),
"fully expensed (expenses ≥ gross) must return None"
);
}
#[test]
fn chunkb_expenses_w2_combined() {
let st = state_with(vec![income(
IncomeKind::Mining,
true,
dec!(100000),
date!(2025 - 03 - 01),
)]);
let r = compute_se_tax(
&st,
2025,
FilingStatus::Single,
&tbl(),
dec!(150000),
dec!(150000),
dec!(20000),
)
.expect("SE tax expected");
assert_eq!(r.net_se, dec!(80000));
assert_eq!(r.base, dec!(73880.00));
assert_eq!(r.ss, dec!(3236.40));
assert_eq!(r.medicare, dec!(2142.52));
assert_eq!(r.addl, dec!(214.92));
assert_eq!(r.total, dec!(5593.84));
assert_eq!(r.deductible_half, dec!(2689.46));
}
#[test]
fn chunkb_regression_zero_expenses_byte_identical_to_golden1() {
let st = state_with(vec![income(
IncomeKind::Mining,
true,
dec!(100000),
date!(2025 - 03 - 01),
)]);
let r = compute_se_tax(
&st,
2025,
FilingStatus::Single,
&tbl(),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
)
.expect("SE tax expected");
assert_eq!(r.net_se, dec!(100000));
assert_eq!(r.base, dec!(92350.00));
assert_eq!(r.ss, dec!(11451.40));
assert_eq!(r.medicare, dec!(2678.15));
assert_eq!(r.addl, dec!(0.00));
assert_eq!(r.total, dec!(14129.55));
assert_eq!(r.deductible_half, dec!(7064.78));
}
#[test]
fn chunkb_expenses_equal_to_gross_is_none() {
let st = state_with(vec![income(
IncomeKind::Mining,
true,
dec!(50000),
date!(2025 - 01 - 15),
)]);
assert!(
compute_se_tax(
&st,
2025,
FilingStatus::Single,
&tbl(),
Usd::ZERO,
Usd::ZERO,
dec!(50000),
)
.is_none(),
"expenses == gross must return None (net_se = 0)"
);
}
}