use crate::conventions::{round_cents, round_dollar, Usd};
use crate::tax::se::SeTaxResult;
use crate::tax::tables::{
niit_threshold, se_addl_medicare_threshold, NIIT_RATE, SE_RATE_ADDL_MEDICARE,
};
use crate::tax::types::FilingStatus;
use rust_decimal_macros::dec;
const MEDICARE_EMPLOYEE_RATE: Usd = dec!(0.0145);
pub fn sch2_line4_se(se: Option<&SeTaxResult>) -> Usd {
se.map_or(Usd::ZERO, |s| s.ss + s.medicare)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Form8959 {
pub part1_wages: Usd,
pub part2_se: Usd,
pub additional_medicare_tax: Usd,
pub part5_withholding: Usd,
}
pub fn form_8959(
status: FilingStatus,
medicare_wages: Usd,
medicare_withheld: Usd,
se: Option<&SeTaxResult>,
) -> Form8959 {
let thr = se_addl_medicare_threshold(status);
let part1 = round_cents(SE_RATE_ADDL_MEDICARE * (medicare_wages - thr).max(Usd::ZERO));
let part2 = se.map_or(Usd::ZERO, |s| s.addl);
let regular_medicare = round_cents(MEDICARE_EMPLOYEE_RATE * medicare_wages);
let part5 = (medicare_withheld - regular_medicare).max(Usd::ZERO);
Form8959 {
part1_wages: part1,
part2_se: part2,
additional_medicare_tax: part1 + part2,
part5_withholding: part5,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Form8959Lines {
pub line1: Usd,
pub line4: Usd,
pub line5: Usd,
pub line6: Usd,
pub line7: Usd,
pub line8: Usd,
pub line9: Usd,
pub line10: Usd,
pub line11: Usd,
pub line12: Usd,
pub line13: Usd,
pub line18: Usd,
pub line19: Usd,
pub line20: Usd,
pub line21: Usd,
pub line22: Usd,
pub line24: Usd,
}
impl Form8959Lines {
pub fn must_file(&self) -> bool {
self.line18 != Usd::ZERO || self.line24 != Usd::ZERO
}
}
pub fn form_8959_lines(
status: FilingStatus,
medicare_wages: Usd,
medicare_withheld: Usd,
se: Option<&SeTaxResult>,
) -> Form8959Lines {
let thr = se_addl_medicare_threshold(status);
let line1 = round_dollar(medicare_wages);
let line4 = line1; let line6 = (line4 - thr).max(Usd::ZERO);
let line7 = round_dollar(SE_RATE_ADDL_MEDICARE * line6);
let line8 = round_dollar(se.map_or(Usd::ZERO, |s| s.base)); let line10 = line4;
let line11 = (thr - line10).max(Usd::ZERO);
let line12 = (line8 - line11).max(Usd::ZERO);
let line13 = round_dollar(SE_RATE_ADDL_MEDICARE * line12);
let line18 = line7 + line13;
let line19 = round_dollar(medicare_withheld);
let line20 = line1;
let line21 = round_dollar(MEDICARE_EMPLOYEE_RATE * line20);
let line22 = (line19 - line21).max(Usd::ZERO);
let line24 = line22;
Form8959Lines {
line1,
line4,
line5: thr,
line6,
line7,
line8,
line9: thr,
line10,
line11,
line12,
line13,
line18,
line19,
line20,
line21,
line22,
line24,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Form8960 {
pub nii: Usd,
pub magi: Usd,
pub tax: Usd,
}
pub fn form_8960(
status: FilingStatus,
taxable_interest: Usd,
ordinary_dividends: Usd,
net_capital_gain: Usd,
crypto_lending_interest: Usd,
agi: Usd,
) -> Form8960 {
let nii = taxable_interest + ordinary_dividends + net_capital_gain + crypto_lending_interest;
let magi = agi;
let thr = niit_threshold(status);
let over = (magi - thr).max(Usd::ZERO);
let base = nii.max(Usd::ZERO).min(over);
Form8960 {
nii,
magi,
tax: round_cents(NIIT_RATE * base),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Form8960Lines {
pub line1: Usd,
pub line2: Usd,
pub line5a: Usd,
pub line5d: Usd,
pub line7: Usd,
pub line8: Usd,
pub line9d: Usd,
pub line11: Usd,
pub line12: Usd,
pub line13: Usd,
pub line14: Usd,
pub line15: Usd,
pub line16: Usd,
pub line17: Usd,
}
pub fn form_8960_lines(
status: FilingStatus,
taxable_interest: Usd,
ordinary_dividends: Usd,
net_capital_gain: Usd,
crypto_lending_interest: Usd,
agi: Usd,
) -> Option<Form8960Lines> {
let line1 = round_dollar(taxable_interest);
let line2 = round_dollar(ordinary_dividends);
let line5a = round_dollar(net_capital_gain); let line5d = line5a; let line7 = round_dollar(crypto_lending_interest);
let line8 = line1 + line2 + line5d + line7; let line9d = Usd::ZERO; let line11 = line9d; let line12 = line8 - line11;
let line13 = round_dollar(agi);
let line14 = niit_threshold(status);
let line15 = (line13 - line14).max(Usd::ZERO);
let line16 = line12.min(line15);
let line17 = round_dollar(NIIT_RATE * line16.max(Usd::ZERO));
if line17 <= Usd::ZERO {
return None; }
Some(Form8960Lines {
line1,
line2,
line5a,
line5d,
line7,
line8,
line9d,
line11,
line12,
line13,
line14,
line15,
line16,
line17,
})
}
#[cfg(test)]
mod tests {
use super::*;
fn se_300k_single() -> SeTaxResult {
SeTaxResult {
net_se: dec!(300000),
base: dec!(277050.00),
ss: dec!(21836.40),
medicare: dec!(8034.45),
addl: dec!(693.45),
total: dec!(30564.30),
deductible_half: dec!(14935.42),
}
}
#[test]
fn kat6_sch2_l4_unbundles_the_addl_medicare() {
let se = se_300k_single();
assert_eq!(sch2_line4_se(Some(&se)), dec!(29870.85)); assert_ne!(sch2_line4_se(Some(&se)), se.total);
let f = form_8959(FilingStatus::Single, Usd::ZERO, Usd::ZERO, Some(&se));
assert_eq!(f.part2_se, dec!(693.45)); assert_eq!(f.additional_medicare_tax, dec!(693.45)); assert_eq!(sch2_line4_se(None), Usd::ZERO);
}
#[test]
fn form_8959_qss_uses_200k_threshold_not_250k() {
let f = form_8959(FilingStatus::Qss, dec!(240000), Usd::ZERO, None);
assert_eq!(f.part1_wages, dec!(360.00));
}
#[test]
fn form_8959_part1_wages_over_threshold() {
let over = form_8959(FilingStatus::Single, dec!(250000), Usd::ZERO, None);
assert_eq!(over.part1_wages, dec!(450.00));
assert_eq!(over.additional_medicare_tax, dec!(450.00)); let under = form_8959(FilingStatus::Single, dec!(200000), Usd::ZERO, None);
assert_eq!(under.part1_wages, Usd::ZERO);
let mfj = form_8959(FilingStatus::Mfj, dec!(260000), Usd::ZERO, None);
assert_eq!(mfj.part1_wages, dec!(90.00));
}
#[test]
fn form_8959_part1_and_part2_compose() {
let se = se_300k_single();
let f = form_8959(FilingStatus::Single, dec!(210000), Usd::ZERO, Some(&se));
assert_eq!(f.part1_wages, dec!(90.00));
assert_eq!(f.part2_se, dec!(693.45));
assert_eq!(f.additional_medicare_tax, dec!(783.45)); }
#[test]
fn form_8959_part5_withholding_reconciliation() {
let f = form_8959(FilingStatus::Single, dec!(250000), dec!(4000), None);
assert_eq!(f.part5_withholding, dec!(375.00));
let none = form_8959(FilingStatus::Single, dec!(250000), dec!(3000), None);
assert_eq!(none.part5_withholding, Usd::ZERO);
}
fn se_mining_60k_mfj_with_280k_wages() -> SeTaxResult {
SeTaxResult {
net_se: dec!(60000),
base: dec!(55410.00), ss: dec!(0.00), medicare: dec!(1606.89), addl: dec!(498.69), total: dec!(2105.58), deductible_half: dec!(803.44), }
}
#[test]
fn form_8959_lines_deep02_example2_printed_chain() {
let se = se_mining_60k_mfj_with_280k_wages();
let l = form_8959_lines(FilingStatus::Mfj, dec!(280000), dec!(4240), Some(&se));
assert_eq!(l.line1, dec!(280000));
assert_eq!(l.line4, dec!(280000)); assert_eq!(l.line5, dec!(250000)); assert_eq!(l.line6, dec!(30000));
assert_eq!(l.line7, dec!(270)); assert_eq!(l.line8, dec!(55410)); assert_eq!(l.line9, dec!(250000));
assert_eq!(l.line10, dec!(280000)); assert_eq!(l.line11, Usd::ZERO); assert_eq!(l.line12, dec!(55410)); assert_eq!(l.line13, dec!(499)); assert_eq!(l.line18, dec!(769)); assert_eq!(l.line19, dec!(4240));
assert_eq!(l.line20, dec!(280000)); assert_eq!(l.line21, dec!(4060)); assert_eq!(l.line22, dec!(180)); assert_eq!(l.line24, dec!(180)); }
#[test]
fn kat9_printed_lines_round_then_cross_foot() {
let se = SeTaxResult {
net_se: dec!(60097.46),
base: dec!(55500.00),
ss: dec!(0.00),
medicare: dec!(1609.50),
addl: dec!(499.50), total: dec!(2109.00),
deductible_half: dec!(804.75),
};
let (status, wages) = (FilingStatus::Mfj, dec!(280500));
let f = form_8959(status, wages, Usd::ZERO, Some(&se));
let l = form_8959_lines(status, wages, Usd::ZERO, Some(&se));
assert_eq!(f.part1_wages, dec!(274.50));
assert_eq!(f.part2_se, dec!(499.50));
assert_eq!(f.additional_medicare_tax, dec!(774.00));
assert_eq!(l.line7, dec!(275));
assert_eq!(l.line13, dec!(500));
assert_eq!(l.line18, dec!(775));
assert_eq!(l.line7 + l.line13, l.line18);
assert_eq!(round_dollar(f.additional_medicare_tax), dec!(774));
assert_ne!(l.line18, round_dollar(f.additional_medicare_tax));
}
#[test]
fn form_8959_printed_lines_cross_foot() {
for (status, wages, withheld, se) in [
(
FilingStatus::Mfj,
dec!(280000),
dec!(4240),
Some(se_mining_60k_mfj_with_280k_wages()),
),
(FilingStatus::Single, dec!(250000.49), dec!(4000), None), (FilingStatus::Qss, dec!(240000), dec!(3000), None), (FilingStatus::Mfs, dec!(50000), dec!(725), None), ] {
let l = form_8959_lines(status, wages, withheld, se.as_ref());
assert_eq!(l.line4, l.line1, "L4 = 1 + 2 + 3, with 2/3 blank");
assert_eq!(
l.line6,
(l.line4 - l.line5).max(Usd::ZERO),
"L6 = 4 − 5, floored"
);
assert_eq!(
l.line7,
round_dollar(SE_RATE_ADDL_MEDICARE * l.line6),
"L7 = 0.9% × 6"
);
assert_eq!(l.line9, l.line5, "the two threshold cells agree");
assert_eq!(l.line10, l.line4, "L10 = line 4");
assert_eq!(
l.line11,
(l.line9 - l.line10).max(Usd::ZERO),
"L11 = 9 − 10, floored"
);
assert_eq!(
l.line12,
(l.line8 - l.line11).max(Usd::ZERO),
"L12 = 8 − 11, floored"
);
assert_eq!(
l.line13,
round_dollar(SE_RATE_ADDL_MEDICARE * l.line12),
"L13 = 0.9% × 12"
);
assert_eq!(l.line18, l.line7 + l.line13, "L18 = 7 + 13 (+17, blank)");
assert_eq!(l.line20, l.line1, "L20 = line 1");
assert_eq!(
l.line21,
round_dollar(MEDICARE_EMPLOYEE_RATE * l.line20),
"L21 = 1.45% × 20"
);
assert_eq!(
l.line22,
(l.line19 - l.line21).max(Usd::ZERO),
"L22 = 19 − 21, floored"
);
assert_eq!(l.line24, l.line22, "L24 = 22 + 23 (23 blank)");
for cell in [
l.line1, l.line4, l.line5, l.line6, l.line7, l.line8, l.line9, l.line10, l.line11,
l.line12, l.line13, l.line18, l.line19, l.line20, l.line21, l.line22, l.line24,
] {
assert_eq!(
cell.fract(),
Usd::ZERO,
"printed cells are whole dollars: {cell}"
);
}
}
}
#[test]
fn form_8960_nii_binding() {
let f = form_8960(
FilingStatus::Single,
dec!(5000),
dec!(10000),
dec!(20000),
dec!(2000),
dec!(300000),
);
assert_eq!(f.nii, dec!(37000));
assert_eq!(f.tax, dec!(1406.00)); }
#[test]
fn form_8960_magi_binding() {
let f = form_8960(
FilingStatus::Single,
dec!(5000),
dec!(10000),
dec!(20000),
dec!(2000),
dec!(210000),
);
assert_eq!(f.tax, dec!(380.00)); }
#[test]
fn form_8960_below_threshold_is_zero() {
let f = form_8960(
FilingStatus::Single,
dec!(50000),
dec!(50000),
dec!(50000),
Usd::ZERO,
dec!(150000),
);
assert_eq!(f.tax, Usd::ZERO);
}
#[test]
fn form_8960_capital_loss_reduces_nii_and_floors_at_zero() {
let f = form_8960(
FilingStatus::Single,
dec!(5000),
Usd::ZERO,
dec!(-3000),
Usd::ZERO,
dec!(300000),
);
assert_eq!(f.nii, dec!(2000));
assert_eq!(f.tax, dec!(76.00));
let neg = form_8960(
FilingStatus::Single,
dec!(5000),
Usd::ZERO,
dec!(-10000),
Usd::ZERO,
dec!(300000),
);
assert_eq!(neg.nii, dec!(-5000));
assert_eq!(neg.tax, Usd::ZERO);
}
#[test]
fn form_8960_printed_chain_nii_binding() {
let l = form_8960_lines(
FilingStatus::Single,
dec!(5000),
dec!(10000),
dec!(20000),
dec!(2000),
dec!(300000),
)
.unwrap();
assert_eq!(l.line1, dec!(5000));
assert_eq!(l.line2, dec!(10000));
assert_eq!(l.line5a, dec!(20000));
assert_eq!(l.line5d, dec!(20000));
assert_eq!(l.line7, dec!(2000));
assert_eq!(l.line8, dec!(37000));
assert_eq!(l.line9d, Usd::ZERO); assert_eq!(l.line11, Usd::ZERO);
assert_eq!(l.line12, dec!(37000));
assert_eq!(l.line13, dec!(300000));
assert_eq!(l.line14, dec!(200000));
assert_eq!(l.line15, dec!(100000));
assert_eq!(l.line16, dec!(37000)); assert_eq!(l.line17, dec!(1406)); }
#[test]
fn form_8960_printed_chain_magi_binding() {
let l = form_8960_lines(
FilingStatus::Single,
dec!(50000),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
dec!(210000),
)
.unwrap();
assert_eq!(l.line12, dec!(50000));
assert_eq!(l.line15, dec!(10000));
assert_eq!(l.line16, dec!(10000)); assert_eq!(l.line17, dec!(380)); }
#[test]
fn form_8960_qss_threshold_is_250k_unlike_form_8959() {
let l = form_8960_lines(
FilingStatus::Qss,
dec!(60000),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
dec!(300000),
)
.unwrap();
assert_eq!(l.line14, dec!(250000), "NIIT: QSS gets the JOINT threshold");
let f = form_8959_lines(FilingStatus::Qss, dec!(300000), Usd::ZERO, None);
assert_eq!(
f.line5,
dec!(200000),
"Add'l Medicare: QSS gets the UNMARRIED threshold"
);
assert_ne!(l.line14, f.line5, "the asymmetry is real and deliberate");
}
#[test]
fn form_8960_absent_when_no_niit_is_owed() {
assert!(form_8960_lines(
FilingStatus::Single,
dec!(50000),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
dec!(150000)
)
.is_none());
assert!(form_8960_lines(
FilingStatus::Single,
dec!(1000),
Usd::ZERO,
dec!(-3000),
Usd::ZERO,
dec!(300000)
)
.is_none());
}
#[test]
fn form_8960_printed_lines_cross_foot() {
for (status, int_, div, ncg, lend, agi) in [
(
FilingStatus::Single,
dec!(5000),
dec!(10000),
dec!(20000),
dec!(2000),
dec!(300000),
),
(
FilingStatus::Mfj,
dec!(50000),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
dec!(300000),
),
(
FilingStatus::Qss,
dec!(60000),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
dec!(300000),
),
(
FilingStatus::Single,
dec!(40000.49),
dec!(0.50),
dec!(-3000),
Usd::ZERO,
dec!(300000.51),
),
] {
let l = form_8960_lines(status, int_, div, ncg, lend, agi).unwrap();
assert_eq!(l.line5d, l.line5a, "L5d = 5a + 5b + 5c (5b/5c blank)");
assert_eq!(
l.line8,
l.line1 + l.line2 + l.line5d + l.line7,
"L8 = 1+2+5d+7"
);
assert_eq!(l.line11, l.line9d, "L11 = 9d + 10 (10 blank)");
assert_eq!(l.line12, l.line8 - l.line11, "L12 = 8 − 11");
assert_eq!(
l.line15,
(l.line13 - l.line14).max(Usd::ZERO),
"L15 = 13 − 14, floored"
);
assert_eq!(
l.line16,
l.line12.min(l.line15),
"L16 = smaller of 12 or 15"
);
assert_eq!(
l.line17,
round_dollar(NIIT_RATE * l.line16.max(Usd::ZERO)),
"L17 = 3.8% × 16"
);
for cell in [
l.line1, l.line2, l.line5a, l.line5d, l.line7, l.line8, l.line9d, l.line11,
l.line12, l.line13, l.line14, l.line15, l.line16, l.line17,
] {
assert_eq!(
cell.fract(),
Usd::ZERO,
"printed cells are whole dollars: {cell}"
);
}
}
}
}