use crate::conventions::{round_dollar, Usd};
use crate::tax::tables::FullReturnParams;
use crate::tax::types::FilingStatus;
use rust_decimal_macros::dec;
const QBI_RATE: Usd = dec!(0.20);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Qbi8995 {
pub deduction: Usd,
pub reit_ptp_carryforward_out: Usd,
}
pub fn has_qbi(business_qbi: Usd, reit_dividends: Usd, reit_ptp_carryforward_in: Usd) -> bool {
business_qbi > Usd::ZERO || reit_dividends > Usd::ZERO || reit_ptp_carryforward_in > Usd::ZERO
}
pub fn compute_8995(
business_qbi: Usd,
reit_dividends: Usd,
reit_ptp_carryforward_in: Usd,
ti_before_qbi: Usd,
net_capital_gain: Usd,
) -> Qbi8995 {
let line5 = round_dollar(QBI_RATE * business_qbi.max(Usd::ZERO));
let line8 = (reit_dividends - reit_ptp_carryforward_in).max(Usd::ZERO);
let line9 = round_dollar(QBI_RATE * line8);
let component = line5 + line9;
let line13 = (ti_before_qbi - net_capital_gain).max(Usd::ZERO);
let income_limit = round_dollar(QBI_RATE * line13);
let deduction = component.min(income_limit);
let reit_ptp_carryforward_out = (reit_ptp_carryforward_in - reit_dividends).max(Usd::ZERO);
Qbi8995 {
deduction,
reit_ptp_carryforward_out,
}
}
pub fn qbi_over_threshold(
business_qbi: Usd,
reit_dividends: Usd,
reit_ptp_carryforward_in: Usd,
ti_before_qbi: Usd,
status: FilingStatus,
params: &FullReturnParams,
) -> bool {
has_qbi(business_qbi, reit_dividends, reit_ptp_carryforward_in)
&& ti_before_qbi > params.qbi_ti_threshold(status)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Form8995Lines {
pub business_name: String,
pub line2: 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 line14: Usd,
pub line15: Usd,
pub line16: Usd,
pub line17: Usd,
}
pub fn form_8995_lines(
business_name: &str,
business_qbi: Usd,
reit_dividends: Usd,
reit_ptp_carryforward_in: Usd,
ti_before_qbi: Usd,
net_capital_gain: Usd,
) -> Option<Form8995Lines> {
if !has_qbi(business_qbi, reit_dividends, reit_ptp_carryforward_in) {
return None;
}
let line2 = round_dollar(business_qbi.max(Usd::ZERO));
let line4 = line2;
let line5 = round_dollar(QBI_RATE * line4);
let line6 = round_dollar(reit_dividends);
let line7 = round_dollar(reit_ptp_carryforward_in);
let line8 = (line6 - line7).max(Usd::ZERO);
let line9 = round_dollar(QBI_RATE * line8);
let line10 = line5 + line9;
let line11 = round_dollar(ti_before_qbi);
let line12 = round_dollar(net_capital_gain);
let line13 = (line11 - line12).max(Usd::ZERO);
let line14 = round_dollar(QBI_RATE * line13);
let line15 = line10.min(line14);
let line16 = Usd::ZERO; let line17 = (line7 - line6).max(Usd::ZERO);
Some(Form8995Lines {
business_name: if business_qbi > Usd::ZERO {
business_name.to_string()
} else {
String::new()
},
line2,
line4,
line5,
line6,
line7,
line8,
line9,
line10,
line11,
line12,
line13,
line14,
line15,
line16,
line17,
})
}
#[cfg(test)]
mod tests {
#[test]
fn schedule_c_business_income_earns_the_199a_deduction_net_of_the_half_se_deduction() {
let qbi = dec!(60000) - dec!(4239); let r = compute_8995(qbi, Usd::ZERO, Usd::ZERO, dec!(95761), Usd::ZERO);
assert_eq!(
r.deduction,
dec!(11152),
"20% × $55,761 of QBI — the figure the independent oracle computes"
);
}
#[test]
fn the_199a_deduction_is_capped_by_the_income_limitation() {
let r = compute_8995(dec!(50000), Usd::ZERO, Usd::ZERO, dec!(60000), dec!(48000));
assert_eq!(
r.deduction,
dec!(2400),
"the income limitation binds — the deduction cannot exceed 20% of NON-preferential income"
);
}
#[test]
fn the_business_and_reit_components_add() {
let r = compute_8995(dec!(50000), dec!(10000), Usd::ZERO, dec!(200000), Usd::ZERO);
assert_eq!(
r.deduction,
dec!(12000),
"20% × 50,000 (business) + 20% × 10,000 (REIT) = 10,000 + 2,000"
);
}
use super::*;
use std::collections::BTreeMap;
fn params() -> FullReturnParams {
let mut std_deduction = BTreeMap::new();
for s in [
FilingStatus::Single,
FilingStatus::Mfj,
FilingStatus::Mfs,
FilingStatus::HoH,
] {
std_deduction.insert(s, dec!(14600));
}
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: crate::tax::tables::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),
},
}
}
#[test]
fn reit_component_when_income_limit_slack() {
let r = compute_8995(
Usd::ZERO,
dec!(10000),
Usd::ZERO,
dec!(100000),
dec!(5000),
);
assert_eq!(r.deduction, dec!(2000));
assert_eq!(r.reit_ptp_carryforward_out, Usd::ZERO);
}
#[test]
fn income_limit_binds_below_component() {
let r = compute_8995(
Usd::ZERO,
dec!(10000),
Usd::ZERO,
dec!(6000),
Usd::ZERO,
);
assert_eq!(r.deduction, dec!(1200));
}
#[test]
fn net_capital_gain_reduces_the_income_limit() {
let r = compute_8995(
Usd::ZERO,
dec!(10000),
Usd::ZERO,
dec!(50000),
dec!(50000),
);
assert_eq!(r.deduction, Usd::ZERO);
}
#[test]
fn loss_carryforward_in_reduces_income_and_carries_out() {
let r = compute_8995(
Usd::ZERO,
dec!(4000),
dec!(10000),
dec!(100000),
Usd::ZERO,
);
assert_eq!(r.deduction, Usd::ZERO);
assert_eq!(r.reit_ptp_carryforward_out, dec!(6000));
}
#[test]
fn no_qbi_when_no_reit() {
assert!(!has_qbi(Usd::ZERO, Usd::ZERO, Usd::ZERO));
let r = compute_8995(
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
dec!(500000),
Usd::ZERO,
);
assert_eq!(r.deduction, Usd::ZERO);
assert_eq!(r.reit_ptp_carryforward_out, Usd::ZERO);
assert!(!qbi_over_threshold(
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
dec!(500000),
FilingStatus::Single,
¶ms()
));
}
#[test]
fn over_threshold_refuse_boundary() {
let p = params();
assert!(qbi_over_threshold(
Usd::ZERO,
dec!(1000),
Usd::ZERO,
dec!(191951),
FilingStatus::Single,
&p
));
assert!(!qbi_over_threshold(
Usd::ZERO,
dec!(1000),
Usd::ZERO,
dec!(191950),
FilingStatus::Single,
&p
));
assert!(!qbi_over_threshold(
Usd::ZERO,
dec!(1000),
Usd::ZERO,
dec!(300000),
FilingStatus::Mfj,
&p
));
assert!(qbi_over_threshold(
Usd::ZERO,
dec!(1000),
Usd::ZERO,
dec!(400000),
FilingStatus::Mfj,
&p
));
assert!(qbi_over_threshold(
Usd::ZERO,
dec!(1000),
Usd::ZERO,
dec!(300000),
FilingStatus::Qss,
&p
));
assert!(qbi_over_threshold(
Usd::ZERO,
Usd::ZERO,
dec!(5000),
dec!(200000),
FilingStatus::Single,
&p
));
}
#[test]
fn component_line_rounds_half_up() {
let r = compute_8995(
Usd::ZERO,
dec!(2502.50),
Usd::ZERO,
dec!(100000),
Usd::ZERO,
);
assert_eq!(r.deduction, dec!(501));
}
#[test]
fn form_8995_printed_chain_reit_only() {
let l =
form_8995_lines("", Usd::ZERO, dec!(10000), Usd::ZERO, dec!(100000), dec!(20000)).unwrap();
assert_eq!(l.line2, Usd::ZERO);
assert_eq!(l.line4, Usd::ZERO);
assert_eq!(l.line5, Usd::ZERO);
assert_eq!(l.line6, dec!(10000));
assert_eq!(l.line7, Usd::ZERO);
assert_eq!(l.line8, dec!(10000));
assert_eq!(l.line9, dec!(2000));
assert_eq!(l.line10, dec!(2000));
assert_eq!(l.line11, dec!(100000));
assert_eq!(l.line12, dec!(20000));
assert_eq!(l.line13, dec!(80000));
assert_eq!(l.line14, dec!(16000));
assert_eq!(l.line15, dec!(2000)); assert_eq!(l.line16, Usd::ZERO);
assert_eq!(l.line17, Usd::ZERO);
}
#[test]
fn form_8995_printed_chain_income_limit_binds() {
let l =
form_8995_lines("", Usd::ZERO, dec!(10000), Usd::ZERO, dec!(12000), dec!(12000)).unwrap();
assert_eq!(l.line10, dec!(2000)); assert_eq!(l.line13, Usd::ZERO);
assert_eq!(l.line14, Usd::ZERO);
assert_eq!(l.line15, Usd::ZERO); }
#[test]
fn form_8995_loss_carryforward_lines_are_positive_magnitudes() {
let l =
form_8995_lines("", Usd::ZERO, dec!(10000), dec!(15000), dec!(100000), Usd::ZERO).unwrap();
assert_eq!(l.line6, dec!(10000));
assert_eq!(l.line7, dec!(15000)); assert_eq!(l.line8, Usd::ZERO); assert_eq!(l.line9, Usd::ZERO);
assert_eq!(l.line15, Usd::ZERO); assert_eq!(l.line17, dec!(5000));
for cell in [l.line7, l.line16, l.line17] {
assert!(
cell >= Usd::ZERO,
"parenthesized cells are magnitudes: {cell}"
);
}
}
#[test]
fn form_8995_absent_when_there_is_no_qbi() {
assert!(
form_8995_lines("", Usd::ZERO, Usd::ZERO, Usd::ZERO, dec!(100000), Usd::ZERO).is_none()
);
let l = form_8995_lines("", Usd::ZERO, Usd::ZERO, dec!(5000), dec!(100000), Usd::ZERO).unwrap();
assert_eq!(l.line17, dec!(5000));
}
#[test]
fn form_8995_printed_lines_cross_foot() {
for (reit, cf_in, ti, ncg) in [
(dec!(10000), Usd::ZERO, dec!(100000), dec!(20000)),
(dec!(10000), dec!(15000), dec!(100000), Usd::ZERO),
(dec!(2502.50), Usd::ZERO, dec!(80000.49), dec!(0.50)), (dec!(10000), Usd::ZERO, dec!(12000), dec!(12000)), ] {
let l = form_8995_lines("", Usd::ZERO, reit, cf_in, ti, ncg).unwrap();
assert_eq!(l.line4, l.line2, "L4 = 2 + 3 (3 blank)");
assert_eq!(l.line5, round_dollar(QBI_RATE * l.line4), "L5 = 20% × 4");
assert_eq!(
l.line8,
(l.line6 - l.line7).max(Usd::ZERO),
"L8 = 6 + 7(loss), floored"
);
assert_eq!(l.line9, round_dollar(QBI_RATE * l.line8), "L9 = 20% × 8");
assert_eq!(l.line10, l.line5 + l.line9, "L10 = 5 + 9");
assert_eq!(
l.line13,
(l.line11 - l.line12).max(Usd::ZERO),
"L13 = 11 − 12, floored"
);
assert_eq!(
l.line14,
round_dollar(QBI_RATE * l.line13),
"L14 = 20% × 13"
);
assert_eq!(
l.line15,
l.line10.min(l.line14),
"L15 = smaller of 10 or 14"
);
assert_eq!(
l.line17,
(l.line7 - l.line6).max(Usd::ZERO),
"L17 = 6 + 7, if > 0 then -0-"
);
for cell in [
l.line2, l.line4, l.line5, l.line6, l.line7, l.line8, l.line9, l.line10, 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}"
);
}
}
}
}