use crate::conventions::{round_dollar, Usd};
use crate::tax::other_taxes::{Form8959Lines, Form8960Lines};
use crate::tax::return_1040::{AbsoluteReturn, MEDICAL_FLOOR_RATE};
use crate::tax::types::FilingStatus;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Printed8949Row {
pub description: String,
pub date_acquired: crate::conventions::TaxDate,
pub date_sold: crate::conventions::TaxDate,
pub proceeds_d: Usd,
pub cost_e: Usd,
pub gain_h: Usd,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Printed8949Totals {
pub proceeds_d: Usd,
pub cost_e: Usd,
pub gain_h: Usd,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Printed8949 {
pub short_term: Vec<Printed8949Row>,
pub long_term: Vec<Printed8949Row>,
pub st_totals: Printed8949Totals,
pub lt_totals: Printed8949Totals,
}
pub fn form_8949_printed(rows: &[crate::forms::Form8949Row]) -> Option<Printed8949> {
use crate::forms::Form8949Part;
if rows.is_empty() {
return None;
}
let printed = |r: &crate::forms::Form8949Row| {
let proceeds_d = round_dollar(r.proceeds);
let cost_e = round_dollar(r.cost_basis);
Printed8949Row {
description: r.description.clone(),
date_acquired: r.date_acquired,
date_sold: r.date_sold,
proceeds_d,
cost_e,
gain_h: proceeds_d - cost_e,
}
};
let total = |rs: &[Printed8949Row]| Printed8949Totals {
proceeds_d: rs.iter().map(|r| r.proceeds_d).sum(),
cost_e: rs.iter().map(|r| r.cost_e).sum(),
gain_h: rs.iter().map(|r| r.gain_h).sum(),
};
let short_term: Vec<_> = rows
.iter()
.filter(|r| r.part == Form8949Part::ShortTerm)
.map(printed)
.collect();
let long_term: Vec<_> = rows
.iter()
.filter(|r| r.part == Form8949Part::LongTerm)
.map(printed)
.collect();
Some(Printed8949 {
st_totals: total(&short_term),
lt_totals: total(&long_term),
short_term,
long_term,
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Printed8283Rows(Vec<crate::forms::Form8283Row>);
impl Printed8283Rows {
pub fn rows(&self) -> &[crate::forms::Form8283Row] {
&self.0
}
}
pub fn form_8283_printed(rows: &[crate::forms::Form8283Row]) -> Option<Printed8283Rows> {
if rows.is_empty() {
return None;
}
Some(Printed8283Rows(
rows.iter()
.map(|r| crate::forms::Form8283Row {
cost_basis: round_dollar(r.cost_basis),
fmv: round_dollar(r.fmv),
claimed_deduction: r.claimed_deduction.map(round_dollar),
..r.clone()
})
.collect(),
))
}
pub const FORM_8283_THRESHOLD: Usd = rust_decimal_macros::dec!(500);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScheduleSeLines {
pub line2: Usd,
pub line3: Usd,
pub line4a: Usd,
pub line4c: Usd,
pub line6: Usd,
pub line8a: Usd,
pub line8d: Usd,
pub line9: Usd,
pub line10: Usd,
pub line11: Usd,
pub line12: Usd,
pub line13: Usd,
}
pub fn schedule_se_lines(ar: &AbsoluteReturn, sch_c: &ScheduleCLines) -> Option<ScheduleSeLines> {
let se = ar.se.as_ref()?;
let pi = &ar.printed_inputs;
let line2 = sch_c.line31; let line3 = line2;
let line4a = round_dollar(se.base);
let line4c = line4a;
let line6 = line4c;
let line8a = round_dollar(pi.se_w2_ss_wages);
let line8d = line8a;
let line9 = (round_dollar(pi.ss_wage_base) - line8d).max(Usd::ZERO);
let line10 = round_dollar(se.ss);
let line11 = round_dollar(se.medicare);
let line12 = line10 + line11; let line13 = round_dollar(se.deductible_half);
Some(ScheduleSeLines {
line2,
line3,
line4a,
line4c,
line6,
line8a,
line8d,
line9,
line10,
line11,
line12,
line13,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Schedule2Lines {
pub line4: Usd,
pub line11: Usd,
pub line12: Usd,
pub line21: Usd,
}
pub fn schedule_2_lines(
sch_se: Option<&ScheduleSeLines>,
f8959: &Form8959Lines,
f8960: Option<&Form8960Lines>,
) -> Option<Schedule2Lines> {
let line4 = sch_se.map_or(Usd::ZERO, |s| s.line12);
let line11 = f8959.line18; let line12 = f8960.map_or(Usd::ZERO, |f| f.line17); let line21 = line4 + line11 + line12;
if line21 <= Usd::ZERO {
return None;
}
Some(Schedule2Lines {
line4,
line11,
line12,
line21,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Schedule1Lines {
pub line1: Usd,
pub line3: Usd,
pub line7: Usd,
pub line8v: Usd,
pub line9: Usd,
pub line10: Usd,
pub line15: Usd,
pub line18: Usd,
pub line21: Usd,
pub line26: Usd,
}
pub fn schedule_1_lines(ar: &AbsoluteReturn) -> Option<Schedule1Lines> {
let p = &ar.schedule_1;
let line1 = round_dollar(p.state_refund_1);
let line3 = round_dollar(p.schedule_c_net_3);
let line7 = round_dollar(p.unemployment_7);
let line8v = round_dollar(p.crypto_ordinary_8v);
let line9 = line8v; let line10 = line1 + line3 + line7 + line9;
let line15 = round_dollar(p.half_se_15);
let line18 = round_dollar(p.early_withdrawal_18);
let line21 = round_dollar(p.student_loan_21);
let line26 = line15 + line18 + line21;
if line10 <= Usd::ZERO && line26 <= Usd::ZERO {
return None;
}
Some(Schedule1Lines {
line1,
line3,
line7,
line8v,
line9,
line10,
line15,
line18,
line21,
line26,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Form1040Lines {
pub line1z: Usd,
pub line1a: Usd,
pub line2a: Usd,
pub line2b: Usd,
pub line3a: Usd,
pub line3b: 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 line18: Usd,
pub line19: Usd,
pub line20: Usd,
pub line21: Usd,
pub line22: Usd,
pub line23: Usd,
pub line24: Usd,
pub line25a: Usd,
pub line25b: Usd,
pub line25c: Usd,
pub line25d: Usd,
pub line26: Usd,
pub line31: Usd,
pub line32: Usd,
pub line33: Usd,
pub line34: Usd,
pub line37: Usd,
pub digital_asset_yes: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Form1040Income {
pub qdcgt_net_capital_gain: Usd,
pub line1a: Usd,
pub line1z: Usd,
pub line2a: Usd,
pub line2b: Usd,
pub line3a: Usd,
pub line3b: Usd,
pub line7: Usd,
pub line8: Usd,
pub line9: Usd,
pub line10: Usd,
pub line11: Usd,
}
pub fn form_1040_income_lines(
ar: &AbsoluteReturn,
sch_b: Option<&ScheduleBLines>,
sch_1: Option<&Schedule1Lines>,
sch_d: &ScheduleDLines,
) -> Form1040Income {
let line1a = round_dollar(ar.wages);
let line1z = line1a;
let line2a = round_dollar(ar.printed_inputs.tax_exempt_interest);
let line2b = sch_b.map_or_else(|| round_dollar(ar.taxable_interest), |b| b.line4);
let line3a = round_dollar(ar.qualified_dividends);
let line3b = sch_b.map_or_else(|| round_dollar(ar.ordinary_dividends), |b| b.line6);
let line7 = match sch_d.routing {
ScheduleDRouting::NetLoss { line21, .. } => -line21,
_ => sch_d.line16,
};
let line8 = sch_1.map_or(Usd::ZERO, |s| s.line10);
let line9 = line1z + line2b + line3b + line7 + line8; let line10 = sch_1.map_or(Usd::ZERO, |s| s.line26);
let line11 = line9 - line10;
Form1040Income {
qdcgt_net_capital_gain: sch_d.line15.min(sch_d.line16).max(Usd::ZERO),
line1a,
line1z,
line2a,
line2b,
line3a,
line3b,
line7,
line8,
line9,
line10,
line11,
}
}
#[allow(clippy::too_many_arguments)]
pub fn form_1040_lines(
ar: &AbsoluteReturn,
income: &Form1040Income,
sch_a: Option<&ScheduleALines>,
sch_2: Option<&Schedule2Lines>,
sch_3: Option<&Schedule3Lines>,
f8959: &Form8959Lines,
f8995: Option<&crate::tax::qbi::Form8995Lines>,
table: &crate::tax::tables::TaxTable,
status: FilingStatus,
other_withholding: Usd,
estimated_payments: Usd,
digital_asset_yes: bool,
) -> Form1040Lines {
let Form1040Income {
qdcgt_net_capital_gain,
line1a,
line1z,
line2a,
line2b,
line3a,
line3b,
line7,
line8,
line9,
line10,
line11,
} = *income;
let line12 = match sch_a {
Some(a) => a.line17, None => round_dollar(ar.deduction),
};
let line13 = f8995.map_or(Usd::ZERO, |q| q.line15);
let line14 = line12 + line13;
let line15 = (line11 - line14).max(Usd::ZERO);
let line16 = crate::tax::method::qdcgt_line16(
table.ordinary_for(status),
table.ltcg_for(status),
line15,
line3a,
qdcgt_net_capital_gain, );
let line17 = Usd::ZERO; let line18 = line16 + line17;
let line19 = Usd::ZERO; let line20 = sch_3.map_or(Usd::ZERO, |s| s.line8);
let line21 = line19 + line20;
let line22 = (line18 - line21).max(Usd::ZERO);
let line23 = sch_2.map_or(Usd::ZERO, |s| s.line21);
let line24 = line22 + line23;
let line25a = round_dollar(ar.withholding_25a);
let line25b = round_dollar(ar.withholding_25b);
let line25c = f8959.line24 + round_dollar(other_withholding);
let line25d = line25a + line25b + line25c;
let line26 = round_dollar(estimated_payments);
let line31 = sch_3.map_or(Usd::ZERO, |s| s.line15);
let line32 = line31; let line33 = line25d + line26 + line32;
let line34 = (line33 - line24).max(Usd::ZERO);
let line37 = (line24 - line33).max(Usd::ZERO);
Form1040Lines {
line1a,
line1z,
line2a,
line2b,
line3a,
line3b,
line7,
line8,
line9,
line10,
line11,
line12,
line13,
line14,
line15,
line16,
line17,
line18,
line19,
line20,
line21,
line22,
line23,
line24,
line25a,
line25b,
line25c,
line25d,
line26,
line31,
line32,
line33,
line34,
line37,
digital_asset_yes,
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ScheduleDRouting {
BothGains,
ShortGainLongLoss { line22_yes: bool },
NetLoss {
line21: Usd,
line22_yes: bool,
},
Zero { line22_yes: bool },
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScheduleDLines {
pub line3_d: Usd,
pub line3_e: Usd,
pub line3_h: Usd,
pub line6: Usd,
pub line7: Usd,
pub line10_d: Usd,
pub line10_e: Usd,
pub line10_h: Usd,
pub line13: Usd,
pub line14: Usd,
pub line15: Usd,
pub line16: Usd,
pub routing: ScheduleDRouting,
}
impl ScheduleDLines {
pub fn must_file(&self) -> bool {
[
self.line3_d,
self.line3_e,
self.line3_h,
self.line6,
self.line10_d,
self.line10_e,
self.line10_h,
self.line13,
self.line14,
self.line16,
]
.iter()
.any(|v| *v != Usd::ZERO)
}
}
pub fn schedule_d_lines(ar: &AbsoluteReturn, f8949: Option<&Printed8949>) -> ScheduleDLines {
let p = &ar.schedule_d;
let st = f8949.map(|f| f.st_totals).unwrap_or_default();
let lt = f8949.map(|f| f.lt_totals).unwrap_or_default();
let line3_d = st.proceeds_d;
let line3_e = st.cost_e;
let line3_h = st.gain_h;
let line6 = round_dollar(p.st_carryover_6); let line7 = line3_h - line6;
let line10_d = lt.proceeds_d;
let line10_e = lt.cost_e;
let line10_h = lt.gain_h;
let line13 = round_dollar(p.cap_gain_distr_13);
let line14 = round_dollar(p.lt_carryover_14); let line15 = line10_h + line13 - line14;
let line16 = line7 + line15; let has_qd = round_dollar(p.qualified_dividends) > Usd::ZERO;
let routing = if line16 > Usd::ZERO && line15 > Usd::ZERO {
ScheduleDRouting::BothGains
} else if line16 > Usd::ZERO {
ScheduleDRouting::ShortGainLongLoss { line22_yes: has_qd }
} else if line16 < Usd::ZERO {
ScheduleDRouting::NetLoss {
line21: line16.abs().min(ar.printed_inputs.capital_loss_limit),
line22_yes: has_qd,
}
} else {
ScheduleDRouting::Zero { line22_yes: has_qd }
};
ScheduleDLines {
line3_d,
line3_e,
line3_h,
line6,
line7,
line10_d,
line10_e,
line10_h,
line13,
line14,
line15,
line16,
routing,
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScheduleBRow {
pub payer: String,
pub amount: Usd,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScheduleBLines {
pub part1_rows: Vec<ScheduleBRow>,
pub line2: Usd,
pub line4: Usd,
pub part2_rows: Vec<ScheduleBRow>,
pub line6: Usd,
pub foreign_accounts_7a: bool,
pub foreign_trust_8: bool,
pub line7b_countries: String,
}
pub fn schedule_b_lines(ri: &crate::tax::return_inputs::ReturnInputs) -> Option<ScheduleBLines> {
if !crate::tax::return_1040::schedule_b_files(ri) {
return None;
}
let part1_rows: Vec<ScheduleBRow> = ri
.int_1099
.iter()
.map(|i| ScheduleBRow {
payer: i.payer.clone(),
amount: round_dollar(i.box1_interest + i.box3_treasury_interest),
})
.filter(|r| r.amount > Usd::ZERO)
.collect();
let line2 = part1_rows.iter().map(|r| r.amount).sum(); let line4 = line2;
let part2_rows: Vec<ScheduleBRow> = ri
.div_1099
.iter()
.map(|d| ScheduleBRow {
payer: d.payer.clone(),
amount: round_dollar(d.box1a_ordinary),
})
.filter(|r| r.amount > Usd::ZERO)
.collect();
let line6 = part2_rows.iter().map(|r| r.amount).sum();
Some(ScheduleBLines {
part1_rows,
line2,
line4,
part2_rows,
line6,
foreign_accounts_7a: ri.foreign_accounts.unwrap_or(false),
line7b_countries: if ri.foreign_accounts == Some(true) {
ri.foreign_country_names.clone()
} else {
String::new()
},
foreign_trust_8: ri.foreign_trust.unwrap_or(false),
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ScheduleCLines {
pub line_a_business: String,
pub line_b_naics: String,
pub line_f_accrual: bool,
pub line1: Usd,
pub line3: Usd,
pub line5: Usd,
pub line7: Usd,
pub line28: Usd,
pub line29: Usd,
pub line31: Usd,
}
pub fn schedule_c_lines(ar: &AbsoluteReturn) -> Option<ScheduleCLines> {
let p = ar.schedule_c.as_ref()?;
let h = &ar.printed_inputs.schedule_c_header;
let line1 = round_dollar(p.gross_receipts_1);
let line3 = line1; let line5 = line3; let line7 = line5; let line28 = round_dollar(p.total_expenses_28);
let line29 = (line7 - line28).max(Usd::ZERO); let line31 = line29;
Some(ScheduleCLines {
line_a_business: h.business_description.clone(),
line_b_naics: h.naics_code.clone(),
line_f_accrual: h.accrual,
line1,
line3,
line5,
line7,
line28,
line29,
line31,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ScheduleALines {
pub line5a_is_sales_tax: bool,
pub line18_elects_smaller: bool,
pub line8_mixed_use_box: bool,
pub line1: Usd,
pub line2: Usd,
pub line3: Usd,
pub line4: Usd,
pub line5a: Usd,
pub line5b: Usd,
pub line5c: Usd,
pub line5d: Usd,
pub line5e: Usd,
pub line7: Usd,
pub line8a: Usd,
pub line8e: Usd,
pub line10: Usd,
pub line11: Usd,
pub line12: Usd,
pub line13: Usd,
pub line14: Usd,
pub line17: Usd,
}
pub fn schedule_a_lines(ar: &AbsoluteReturn, line11_1040: Usd) -> Option<ScheduleALines> {
if !ar.deduction_is_itemized {
return None;
}
let p = ar.schedule_a.as_ref()?;
let line1 = round_dollar(p.medical_expenses);
let line2 = line11_1040;
let line3 = round_dollar(MEDICAL_FLOOR_RATE * line2).max(Usd::ZERO);
let line4 = (line1 - line3).max(Usd::ZERO);
let line5a = round_dollar(p.salt_5a);
let line5b = round_dollar(p.salt_5b);
let line5c = round_dollar(p.salt_5c);
let line5d = line5a + line5b + line5c;
let line5e = line5d.min(p.salt_cap);
let line7 = line5e;
let line8a = round_dollar(p.mortgage_8a);
let line8e = line8a; let line10 = line8e;
let line11 = round_dollar(p.charitable_cash_11);
let line12 = round_dollar(p.charitable_noncash_12);
let line13 = round_dollar(p.charitable_carryover_13);
let line14 = line11 + line12 + line13;
let line17 = line4 + line7 + line10 + line14;
Some(ScheduleALines {
line5a_is_sales_tax: p.salt_is_sales_tax,
line18_elects_smaller: ar
.itemized_deduction
.is_some_and(|it| it < ar.standard_deduction),
line8_mixed_use_box: p.mortgage_mixed_use_box,
line1,
line2,
line3,
line4,
line5a,
line5b,
line5c,
line5d,
line5e,
line7,
line8a,
line8e,
line10,
line11,
line12,
line13,
line14,
line17,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Schedule3Lines {
pub line1: Usd,
pub line8: Usd,
pub line10: Usd,
pub line11: Usd,
pub line15: Usd,
}
pub fn schedule_3_lines(ar: &AbsoluteReturn) -> Option<Schedule3Lines> {
let line1 = round_dollar(ar.foreign_tax_credit);
let line8 = line1; let line10 = round_dollar(ar.printed_inputs.extension_payment);
let line11 = round_dollar(ar.excess_social_security);
let line15 = line10 + line11;
if line8 <= Usd::ZERO && line15 <= Usd::ZERO {
return None;
}
Some(Schedule3Lines {
line1,
line8,
line10,
line11,
line15,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::forms::{Form8949Box, Form8949Part, Form8949Row};
use crate::tax::other_taxes::form_8959_lines;
use crate::tax::se::SeTaxResult;
use crate::tax::types::FilingStatus;
use rust_decimal_macros::dec;
use time::macros::date;
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),
}
}
fn ar_with(se: Option<SeTaxResult>, ftc: Usd, excess_ss: Usd) -> AbsoluteReturn {
use crate::tax::other_taxes::{Form8959, Form8960};
let z = Usd::ZERO;
AbsoluteReturn {
wages: z,
taxable_interest: z,
ordinary_dividends: z,
qualified_dividends: z,
capital_gain: z,
schedule_1_income: z,
total_income: z,
adjustments: z,
half_se_deduction: z,
agi: z,
se,
schedule_1: crate::tax::return_1040::Schedule1Parts {
state_refund_1: z,
schedule_c_net_3: z,
unemployment_7: z,
crypto_ordinary_8v: z,
half_se_15: z,
early_withdrawal_18: z,
student_loan_21: z,
},
schedule_c: None,
schedule_d: crate::tax::return_1040::ScheduleDParts {
st_proceeds_3d: z,
st_cost_3e: z,
st_gain_3h: z,
st_carryover_6: z,
st_net_7: z,
lt_proceeds_10d: z,
lt_cost_10e: z,
lt_gain_10h: z,
cap_gain_distr_13: z,
lt_carryover_14: z,
lt_net_15: z,
total_16: z,
loss_deduction_21: z,
qualified_dividends: z,
},
standard_deduction: z,
itemized_deduction: None,
schedule_a: None,
deduction: z,
deduction_is_itemized: false,
qbi_deduction: z,
total_deductions: z,
taxable_income: z,
net_ltcg: z,
charitable_carryover_out: Vec::new(),
qbi_reit_ptp_carryforward_out: z,
regular_tax: z,
se_tax_sch2_l4: z,
schedule_2_other_taxes: z,
additional_medicare: Form8959 {
part1_wages: z,
part2_se: z,
additional_medicare_tax: z,
part5_withholding: z,
},
niit: Form8960 {
nii: z,
magi: z,
tax: z,
},
foreign_tax_credit: ftc,
ctc_odc_credit: z,
tax_after_credits: z,
total_tax: z,
excess_social_security: excess_ss,
withholding_25a: z,
withholding_25b: z,
total_withholding: z,
total_payments: z,
overpayment_refund: z,
amount_owed: z,
printed_inputs: crate::tax::return_1040::PrintedInputs {
medicare_wages: z,
medicare_withheld: z,
schedule_c_header: crate::tax::return_1040::ScheduleCHeader::default(),
business_qbi: z,
tax_exempt_interest: z,
crypto_lending_interest: z,
reit_dividends: z,
reit_ptp_carryforward_in: z,
ti_before_qbi: z,
qbi_net_capital_gain: z,
se_w2_ss_wages: z,
ss_wage_base: dec!(168600), capital_loss_limit: dec!(3000), extension_payment: z,
digital_asset_activity: false,
},
}
}
fn tt() -> crate::tax::tables::TaxTable {
crate::tax::testonly::ty2024_table()
}
fn sched_a_parts_sales_tax() -> crate::tax::return_1040::ScheduleAParts {
crate::tax::return_1040::ScheduleAParts {
salt_is_sales_tax: true,
medical_expenses: Usd::ZERO,
agi: dec!(50000),
medical_floor: dec!(3750),
medical_allowed: Usd::ZERO,
salt_5a: dec!(4000),
salt_5b: Usd::ZERO,
salt_5c: Usd::ZERO,
salt_5d: dec!(4000),
salt_cap: dec!(10000),
salt_5e: dec!(4000),
mortgage_8a: dec!(5000),
mortgage_mixed_use_box: false,
charitable_cash_11: Usd::ZERO,
charitable_noncash_12: Usd::ZERO,
charitable_carryover_13: Usd::ZERO,
charitable_14: Usd::ZERO,
total_17: dec!(9000),
}
}
#[test]
fn form_1040_line2a_carries_tax_exempt_interest() {
let mut ar = ar_with(None, Usd::ZERO, Usd::ZERO);
ar.printed_inputs.tax_exempt_interest = dec!(1234.49);
let sd = schedule_d_lines(&ar, None);
let f8959 = form_8959_lines(FilingStatus::Single, Usd::ZERO, Usd::ZERO, None);
let income = form_1040_income_lines(&ar, None, None, &sd);
let l = form_1040_lines(
&ar,
&income,
None,
None,
None,
&f8959,
None,
&tt(),
FilingStatus::Single,
Usd::ZERO,
Usd::ZERO,
false,
);
assert_eq!(
l.line2a,
dec!(1234),
"rounded at the line, like every printed cell"
);
}
#[test]
fn schedule_a_prints_the_sales_tax_and_force_itemize_elections_it_already_honours() {
let mut ar = ar_with(None, Usd::ZERO, Usd::ZERO);
ar.deduction_is_itemized = true;
ar.standard_deduction = dec!(14600);
ar.itemized_deduction = Some(dec!(9000)); ar.schedule_a = Some(sched_a_parts_sales_tax());
let a = schedule_a_lines(&ar, round_dollar(ar.agi)).expect("the return itemizes");
assert!(
a.line5a_is_sales_tax,
"the §164(b)(5) election must PRINT, not just compute"
);
assert!(
a.line18_elects_smaller,
"§63(e): itemizing BELOW the standard deduction must be declared on the form"
);
}
#[test]
fn schedule_a_leaves_both_election_boxes_unchecked_when_no_election_was_made() {
let mut ar = ar_with(None, Usd::ZERO, Usd::ZERO);
ar.deduction_is_itemized = true;
ar.standard_deduction = dec!(14600);
ar.itemized_deduction = Some(dec!(20000)); let mut parts = sched_a_parts_sales_tax();
parts.salt_is_sales_tax = false;
ar.schedule_a = Some(parts);
let a = schedule_a_lines(&ar, round_dollar(ar.agi)).unwrap();
assert!(!a.line5a_is_sales_tax);
assert!(!a.line18_elects_smaller);
}
#[test]
fn schedule_a_line8_mixed_use_box_prints_with_zeroed_8a() {
let mut p = sched_a_parts_sales_tax();
p.salt_is_sales_tax = false;
p.mortgage_8a = Usd::ZERO;
p.mortgage_mixed_use_box = true;
let mut ar = ar_with(None, Usd::ZERO, Usd::ZERO);
ar.deduction_is_itemized = true;
ar.itemized_deduction = Some(dec!(20000));
ar.schedule_a = Some(p);
let a = schedule_a_lines(&ar, round_dollar(ar.agi)).unwrap();
assert!(a.line8_mixed_use_box, "the line-8 mixed-use box must print");
assert_eq!(a.line8a, Usd::ZERO, "8a printed as $0");
assert_eq!(a.line8e, Usd::ZERO, "8e = 8a");
}
#[test]
fn schedule_a_line8_box_off_for_acquisition_only_mortgage() {
let mut ar = ar_with(None, Usd::ZERO, Usd::ZERO);
ar.deduction_is_itemized = true;
ar.itemized_deduction = Some(dec!(20000));
ar.schedule_a = Some(sched_a_parts_sales_tax()); let a = schedule_a_lines(&ar, round_dollar(ar.agi)).unwrap();
assert!(!a.line8_mixed_use_box);
assert_eq!(a.line8a, dec!(5000));
}
fn se_result(ss: Usd, medicare: Usd, base: Usd, half: Usd) -> SeTaxResult {
SeTaxResult {
net_se: base,
base,
ss,
medicare,
addl: Usd::ZERO,
total: ss + medicare,
deductible_half: half,
}
}
#[test]
fn schedule_2_line4_takes_the_printed_se_line_12_not_the_rounded_total() {
let se = se_result(dec!(10.50), dec!(11.50), dec!(1000), dec!(11.00));
let mut ar = ar_with(Some(se), Usd::ZERO, Usd::ZERO);
ar.schedule_c = Some(crate::tax::return_1040::ScheduleCParts {
gross_receipts_1: dec!(1000),
total_expenses_28: Usd::ZERO,
net_profit_31: dec!(1000),
});
let sch_c = schedule_c_lines(&ar).expect("there is a business");
let sse = schedule_se_lines(&ar, &sch_c).expect("there is SE tax");
assert_eq!(sse.line10, dec!(11), "ss rounds at the line");
assert_eq!(sse.line11, dec!(12), "medicare rounds at the line");
assert_eq!(sse.line12, dec!(23), "L12 = add the PRINTED 10 and 11");
assert_ne!(
sse.line12,
round_dollar(dec!(22.00)),
"…and is deliberately NOT a re-rounding of the exact SE tax"
);
let f8959 = form_8959_lines(FilingStatus::Single, Usd::ZERO, Usd::ZERO, Some(&se));
let s2 = schedule_2_lines(Some(&sse), &f8959, None).expect("SE tax ⇒ Schedule 2 files");
assert_eq!(
s2.line4, sse.line12,
"★ Schedule 2 L4 IS Schedule SE's printed L12"
);
}
#[test]
fn schedule_se_composes_with_schedule_c_schedule_1_and_form_8959() {
let se = se_result(dec!(1240), dec!(290), dec!(9235), dec!(765));
let mut ar = ar_with(Some(se), Usd::ZERO, Usd::ZERO);
ar.schedule_c = Some(crate::tax::return_1040::ScheduleCParts {
gross_receipts_1: dec!(11000),
total_expenses_28: dec!(1000),
net_profit_31: dec!(10000),
});
ar.half_se_deduction = dec!(765);
ar.schedule_1.half_se_15 = dec!(765);
let sch_c = schedule_c_lines(&ar).unwrap();
let sse = schedule_se_lines(&ar, &sch_c).unwrap();
assert_eq!(sse.line2, sch_c.line31, "SE L2 ← Schedule C's PRINTED L31");
assert_eq!(sse.line3, sse.line2);
assert_eq!(
sse.line6,
dec!(9235),
"L6 = the §1402(a) base, rounded at the line"
);
let f8959 = form_8959_lines(FilingStatus::Single, Usd::ZERO, Usd::ZERO, Some(&se));
assert_eq!(
f8959.line8, sse.line6,
"Form 8959 L8 cites Schedule SE Part I line 6"
);
let sch_1 = schedule_1_lines(&ar).unwrap();
assert_eq!(
sch_1.line15, sse.line13,
"Sch 1 L15 ← Schedule SE's printed L13"
);
}
#[test]
fn no_se_tax_files_no_schedule_se() {
let ar = ar_with(None, Usd::ZERO, Usd::ZERO);
let sch_c = ScheduleCLines {
line_a_business: String::new(),
line_b_naics: String::new(),
line_f_accrual: false,
line1: Usd::ZERO,
line3: Usd::ZERO,
line5: Usd::ZERO,
line7: Usd::ZERO,
line28: Usd::ZERO,
line29: Usd::ZERO,
line31: Usd::ZERO,
};
assert!(schedule_se_lines(&ar, &sch_c).is_none());
}
#[test]
fn form_1040_line16_is_the_tax_on_the_printed_line15_not_on_the_exact_taxable_income() {
let mut ar = ar_with(None, Usd::ZERO, Usd::ZERO);
ar.wages = dec!(61749.80);
ar.agi = dec!(61749.80);
ar.deduction = dec!(14600);
ar.total_deductions = dec!(14600);
ar.taxable_income = dec!(47149.80); ar.regular_tax = crate::tax::method::qdcgt_line16(
tt().ordinary_for(FilingStatus::Single),
tt().ltcg_for(FilingStatus::Single),
dec!(47149.80),
Usd::ZERO,
Usd::ZERO,
);
let sd = schedule_d_lines(&ar, None);
let f8959 = form_8959_lines(FilingStatus::Single, Usd::ZERO, Usd::ZERO, None);
let income = form_1040_income_lines(&ar, None, None, &sd);
let l = form_1040_lines(
&ar,
&income,
None,
None,
None,
&f8959,
None,
&tt(),
FilingStatus::Single,
Usd::ZERO,
Usd::ZERO,
false,
);
assert_eq!(l.line15, dec!(47150), "the PRINTED L15 (61,750 − 14,600)");
let table_on_printed = crate::tax::method::qdcgt_line16(
tt().ordinary_for(FilingStatus::Single),
tt().ltcg_for(FilingStatus::Single),
dec!(47150),
Usd::ZERO,
Usd::ZERO,
);
assert_eq!(
l.line16, table_on_printed,
"★ the filed L16 must be what a human gets by looking up the FILED L15"
);
assert_ne!(
l.line16,
round_dollar(ar.regular_tax),
"…and it is NOT the tax on the exact-cents TI — the two bins differ"
);
}
#[test]
fn schedule_a_medical_floor_never_goes_negative_on_a_negative_agi() {
let mut ar = ar_with(None, Usd::ZERO, Usd::ZERO);
ar.deduction_is_itemized = true;
ar.itemized_deduction = Some(dec!(20000));
ar.standard_deduction = dec!(14600);
let mut parts = sched_a_parts_sales_tax();
parts.medical_expenses = dec!(10000);
ar.schedule_a = Some(parts);
let a = schedule_a_lines(&ar, dec!(-50000)).unwrap();
assert_eq!(
a.line2,
dec!(-50000),
"L2 still carries the true printed L11"
);
assert_eq!(
a.line3,
Usd::ZERO,
"★ the 7.5% floor is CLAMPED — never negative"
);
assert_eq!(
a.line4,
dec!(10000),
"…so the allowed medical is the expense PAID, never more"
);
assert!(
a.line4 <= a.line1,
"§213(a): the deduction can never exceed the expense"
);
}
#[test]
fn form_1040_line16_takes_the_qdcgt_operand_off_the_printed_schedule_d() {
let mut ar = ar_with(None, Usd::ZERO, Usd::ZERO);
ar.wages = dec!(60000);
ar.deduction = dec!(5003);
ar.net_ltcg = dec!(5000);
let mut sd = schedule_d_lines(&ar, None);
sd.line15 = dec!(5003);
sd.line16 = dec!(5003);
sd.routing = ScheduleDRouting::BothGains;
let income = form_1040_income_lines(&ar, None, None, &sd);
assert_eq!(
income.qdcgt_net_capital_gain,
dec!(5003),
"★ the operand is min(printed Sch D L15, L16) — what the filer copies off the filed form"
);
assert_ne!(
income.qdcgt_net_capital_gain,
round_dollar(ar.net_ltcg),
"…and NOT a re-rounding of the exact preferential gain"
);
let f8959 = form_8959_lines(FilingStatus::Single, Usd::ZERO, Usd::ZERO, None);
let l = form_1040_lines(
&ar,
&income,
None,
None,
None,
&f8959,
None,
&tt(),
FilingStatus::Single,
Usd::ZERO,
Usd::ZERO,
false,
);
assert_eq!(
l.line15,
dec!(60000),
"the fixture's printed taxable income"
);
let worksheet = |net_cap_gain: Usd| {
crate::tax::method::qdcgt_line16(
tt().ordinary_for(FilingStatus::Single),
tt().ltcg_for(FilingStatus::Single),
l.line15,
l.line3a,
net_cap_gain,
)
};
assert_eq!(
l.line16,
worksheet(income.qdcgt_net_capital_gain),
"★ line 16 IS the worksheet on the PRINTED operands"
);
assert_ne!(
l.line16,
worksheet(round_dollar(ar.net_ltcg)),
"a $1 drift in the operand crosses a Tax-Table bin edge and moves the filed L16"
);
}
#[test]
fn form_1040_line1a_carries_the_w2_wages_that_line1z_adds_up() {
let mut ar = ar_with(None, Usd::ZERO, Usd::ZERO);
ar.wages = dec!(82000.49);
let sd = schedule_d_lines(&ar, None);
let income = form_1040_income_lines(&ar, None, None, &sd);
assert_eq!(
income.line1a,
dec!(82000),
"Σ W-2 box 1, rounded at the line"
);
assert_eq!(
income.line1z, income.line1a,
"L1z = 'Add lines 1a through 1h' — with no 1b–1h, it IS 1a, and both cells print"
);
}
#[test]
fn schedule_a_line2_is_the_printed_1040_line11() {
let mut ar = ar_with(None, Usd::ZERO, Usd::ZERO);
ar.deduction_is_itemized = true;
ar.itemized_deduction = Some(dec!(20000));
ar.standard_deduction = dec!(14600);
ar.schedule_a = Some(sched_a_parts_sales_tax());
let printed_l11 = dec!(50123);
let a = schedule_a_lines(&ar, printed_l11).unwrap();
assert_eq!(a.line2, printed_l11, "★ L2 IS the 1040's printed line 11");
assert_eq!(
a.line3,
round_dollar(MEDICAL_FLOOR_RATE * printed_l11),
"…and the 7.5% floor is taken on THAT figure, as the form says"
);
}
fn row8949(part: Form8949Part, proceeds: Usd, basis: Usd) -> Form8949Row {
Form8949Row {
part,
box_: Form8949Box::C,
box_needs_review: false,
description: "1.00000000 BTC".into(),
date_acquired: date!(2020 - 01 - 01),
date_sold: date!(2024 - 05 - 01),
proceeds,
cost_basis: basis,
adjustment_code: String::new(),
adjustment_amount: Usd::ZERO,
gain: proceeds - basis,
wallet: crate::identity::WalletId::SelfCustody { label: "w".into() },
disposition_kind: crate::event::DisposeKind::Sell,
}
}
#[test]
fn form_8949_column_h_is_derived_from_the_printed_d_and_e_not_rounded_independently() {
let rows = vec![row8949(Form8949Part::ShortTerm, dec!(100.49), dec!(0.50))];
let p = form_8949_printed(&rows).expect("there are rows");
let r = &p.short_term[0];
assert_eq!(r.proceeds_d, dec!(100), "d rounds at the cell");
assert_eq!(r.cost_e, dec!(1), "e rounds at the cell");
assert_eq!(
r.gain_h,
dec!(99),
"h = d − e on the PRINTED cells; an independently-rounded h would print 100 and the row \
would contradict its own subtraction"
);
assert_eq!(r.gain_h, r.proceeds_d - r.cost_e, "the row self-checks");
assert_ne!(
r.gain_h,
round_dollar(dec!(99.99)),
"…and it is NOT round(exact gain)"
);
}
#[test]
fn form_8949_part_totals_sum_the_printed_rows_and_cross_foot() {
let rows = vec![
row8949(Form8949Part::ShortTerm, dec!(100.50), Usd::ZERO),
row8949(Form8949Part::ShortTerm, dec!(200.50), Usd::ZERO),
row8949(Form8949Part::LongTerm, dec!(50.49), dec!(10.50)),
];
let p = form_8949_printed(&rows).unwrap();
assert_eq!(
p.st_totals.proceeds_d,
dec!(302),
"the total sums the PRINTED rows"
);
assert_ne!(
p.st_totals.proceeds_d,
round_dollar(dec!(301.00)),
"…and is deliberately NOT a re-rounding of the exact aggregate"
);
assert_eq!(
p.st_totals.gain_h,
p.st_totals.proceeds_d - p.st_totals.cost_e,
"Σh ≡ Σd − Σe"
);
assert_eq!(
p.lt_totals.gain_h,
p.lt_totals.proceeds_d - p.lt_totals.cost_e
);
}
#[test]
fn schedule_d_line3_takes_the_printed_8949_totals_not_a_re_rounded_aggregate() {
let rows = vec![
row8949(Form8949Part::ShortTerm, dec!(100.50), Usd::ZERO),
row8949(Form8949Part::ShortTerm, dec!(200.50), Usd::ZERO),
];
let f8949 = form_8949_printed(&rows).unwrap();
let mut ar = ar_with(None, Usd::ZERO, Usd::ZERO);
ar.schedule_d.st_proceeds_3d = dec!(301.00); ar.schedule_d.st_cost_3e = Usd::ZERO;
ar.schedule_d.st_gain_3h = dec!(301.00);
let sd = schedule_d_lines(&ar, Some(&f8949));
assert_eq!(
sd.line3_d,
dec!(302),
"= the 8949's printed column-(d) total"
);
assert_ne!(
sd.line3_d,
round_dollar(dec!(301.00)),
"≠ round(exact aggregate)"
);
assert_eq!(
sd.line3_h,
sd.line3_d - sd.line3_e,
"Schedule D Part I cross-foots too"
);
}
#[test]
fn schedule_3_line10_carries_the_extension_payment_and_line15_adds_it() {
let mut ar = ar_with(None, Usd::ZERO, Usd::ZERO);
ar.printed_inputs.extension_payment = dec!(4000);
let s3 = schedule_3_lines(&ar).expect("an extension payment alone makes Schedule 3 file");
assert_eq!(
s3.line10,
dec!(4000),
"L10 — amount paid with the extension request"
);
assert_eq!(
s3.line15,
dec!(4000),
"L15 = add 9 through 12 and 14 ⇒ includes L10"
);
}
#[test]
fn schedule_3_line15_adds_the_extension_payment_to_the_excess_ss_credit() {
let mut ar = ar_with(None, Usd::ZERO, dec!(1200));
ar.printed_inputs.extension_payment = dec!(4000);
let s3 = schedule_3_lines(&ar).unwrap();
assert_eq!(s3.line10, dec!(4000));
assert_eq!(s3.line11, dec!(1200));
assert_eq!(s3.line15, dec!(5200), "L15 sums the PRINTED lines above it");
}
#[test]
fn schedule_2_line4_excludes_the_addl_medicare_which_lands_on_line_11() {
let se = se_300k_single();
let mut ar = ar_with(Some(se), Usd::ZERO, Usd::ZERO);
ar.schedule_c = Some(crate::tax::return_1040::ScheduleCParts {
gross_receipts_1: dec!(300000),
total_expenses_28: Usd::ZERO,
net_profit_31: dec!(300000),
});
let sch_c = schedule_c_lines(&ar).unwrap();
let sse = schedule_se_lines(&ar, &sch_c).unwrap();
let f8959 = form_8959_lines(FilingStatus::Single, Usd::ZERO, Usd::ZERO, Some(&se));
let s2 = schedule_2_lines(Some(&sse), &f8959, None).unwrap();
assert_eq!(s2.line4, dec!(29870));
assert_eq!(
s2.line4, sse.line12,
"★ Sch 2 L4 IS Schedule SE's printed line 12"
);
assert_ne!(
s2.line4,
round_dollar(dec!(29870.85)),
"NOT a re-rounding of the exact ss + medicare — that would disagree with Schedule SE"
);
assert_ne!(
s2.line4,
round_dollar(se.total),
"NOT the §1401 total (that folds in the 0.9%)"
);
assert_eq!(s2.line11, dec!(693)); assert_eq!(s2.line12, Usd::ZERO); assert_eq!(s2.line21, dec!(30563)); }
#[test]
fn schedule_2_line11_takes_the_printed_8959_line_18_not_the_rounded_total() {
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 f8959 = form_8959_lines(FilingStatus::Mfj, dec!(280500), Usd::ZERO, Some(&se));
assert_eq!(f8959.line18, dec!(775)); let exact_total = dec!(274.50) + dec!(499.50); assert_eq!(round_dollar(exact_total), dec!(774));
let s2 = schedule_2_lines(None, &f8959, None).unwrap();
assert_eq!(
s2.line11,
dec!(775),
"Schedule 2 must carry the 8959's PRINTED line 18"
);
assert_ne!(s2.line11, round_dollar(exact_total));
}
#[test]
fn schedule_2_absent_when_no_other_taxes() {
let f8959 = form_8959_lines(FilingStatus::Single, dec!(50000), Usd::ZERO, None);
assert_eq!(f8959.line18, Usd::ZERO);
assert!(schedule_2_lines(None, &f8959, None).is_none());
}
#[test]
fn schedule_3_carries_ftc_and_excess_social_security() {
let ar = ar_with(None, dec!(287.40), dec!(1234.56));
let s3 = schedule_3_lines(&ar).unwrap();
assert_eq!(s3.line1, dec!(287)); assert_eq!(s3.line8, dec!(287)); assert_eq!(s3.line11, dec!(1235)); assert_eq!(s3.line15, dec!(1235));
assert!(schedule_3_lines(&ar_with(None, dec!(100), Usd::ZERO)).is_some());
assert!(schedule_3_lines(&ar_with(None, Usd::ZERO, dec!(100))).is_some());
assert!(schedule_3_lines(&ar_with(None, Usd::ZERO, Usd::ZERO)).is_none());
}
#[allow(clippy::too_many_arguments)]
fn parts(
medical: Usd,
agi: Usd,
salt_5a: Usd,
salt_5b: Usd,
salt_5c: Usd,
salt_cap: Usd,
mortgage: Usd,
cash: Usd,
noncash: Usd,
carryover: Usd,
) -> crate::tax::return_1040::ScheduleAParts {
use crate::tax::return_1040::{ScheduleAParts, MEDICAL_FLOOR_RATE};
let agi = agi.max(Usd::ZERO);
let floor = MEDICAL_FLOOR_RATE * agi;
let salt_5d = salt_5a + salt_5b + salt_5c;
let salt_5e = salt_5d.min(salt_cap);
let medical_allowed = (medical - floor).max(Usd::ZERO);
ScheduleAParts {
salt_is_sales_tax: false,
medical_expenses: medical,
agi,
medical_floor: floor,
medical_allowed,
salt_5a,
salt_5b,
salt_5c,
salt_5d,
salt_5e,
salt_cap,
mortgage_8a: mortgage,
mortgage_mixed_use_box: false,
charitable_cash_11: cash,
charitable_noncash_12: noncash,
charitable_carryover_13: carryover,
charitable_14: cash + noncash + carryover,
total_17: medical_allowed + salt_5e + mortgage + cash + noncash + carryover,
}
}
fn ar_itemizing(p: crate::tax::return_1040::ScheduleAParts) -> AbsoluteReturn {
let mut ar = ar_with(None, Usd::ZERO, Usd::ZERO);
ar.schedule_a = Some(p);
ar.deduction_is_itemized = true;
ar.itemized_deduction = Some(p.total_17);
ar
}
#[test]
fn schedule_a_printed_chain_medical_floor_and_salt_cap() {
let ar = ar_itemizing(parts(
dec!(10000),
dec!(100000),
dec!(8000),
dec!(4000),
dec!(500),
dec!(10000),
dec!(12000),
dec!(1000),
dec!(2000),
dec!(500),
));
let l = schedule_a_lines(&ar, round_dollar(ar.schedule_a.as_ref().unwrap().agi)).unwrap();
assert_eq!(l.line1, dec!(10000));
assert_eq!(l.line2, dec!(100000));
assert_eq!(l.line3, dec!(7500)); assert_eq!(l.line4, dec!(2500));
assert_eq!(l.line5d, dec!(12500));
assert_eq!(l.line5e, dec!(10000)); assert_eq!(l.line7, dec!(10000));
assert_eq!(l.line8a, dec!(12000));
assert_eq!(l.line10, dec!(12000));
assert_eq!(l.line11, dec!(1000));
assert_eq!(l.line12, dec!(2000));
assert_eq!(l.line13, dec!(500));
assert_eq!(l.line14, dec!(3500));
assert_eq!(l.line17, dec!(28000)); }
#[test]
fn schedule_a_not_filed_when_the_standard_deduction_wins() {
let p = parts(
Usd::ZERO,
dec!(100000),
dec!(1000),
Usd::ZERO,
Usd::ZERO,
dec!(10000),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
);
let mut ar = ar_itemizing(p);
ar.deduction_is_itemized = false; assert!(
schedule_a_lines(&ar, round_dollar(ar.agi)).is_none(),
"a Schedule A the filer did not use must not be filed"
);
}
#[test]
fn schedule_a_printed_lines_cross_foot() {
for p in [
parts(
dec!(10000.49),
dec!(100000.51),
dec!(8000.50),
dec!(4000),
dec!(500),
dec!(10000),
dec!(12000),
dec!(1000),
dec!(2000),
dec!(500),
),
parts(
dec!(10000),
dec!(-50000),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
dec!(10000),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
),
parts(
Usd::ZERO,
dec!(80000),
dec!(9000),
dec!(1000),
Usd::ZERO,
dec!(5000),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
),
] {
let l = {
let a = ar_itemizing(p);
schedule_a_lines(&a, round_dollar(a.schedule_a.as_ref().unwrap().agi))
}
.unwrap();
assert_eq!(
l.line3,
round_dollar(MEDICAL_FLOOR_RATE * l.line2),
"L3 = 7.5% × printed L2"
);
assert_eq!(
l.line4,
(l.line1 - l.line3).max(Usd::ZERO),
"L4 = 1 − 3, floored"
);
assert_eq!(
l.line5d,
l.line5a + l.line5b + l.line5c,
"L5d = 5a + 5b + 5c (printed)"
);
assert!(l.line5e <= l.line5d, "L5e never exceeds L5d");
assert_eq!(l.line7, l.line5e, "L7 = 5e + 6 (6 blank)");
assert_eq!(l.line10, l.line8a, "L10 = 8e + 9, 8e = 8a (rest blank)");
assert_eq!(
l.line14,
l.line11 + l.line12 + l.line13,
"L14 = 11 + 12 + 13 (printed)"
);
assert_eq!(
l.line17,
l.line4 + l.line7 + l.line10 + l.line14,
"L17 sums the PRINTED subtotals"
);
for cell in [
l.line1, l.line2, l.line3, l.line4, l.line5a, l.line5b, l.line5c, l.line5d,
l.line5e, l.line7, l.line8a, l.line8e, l.line10, l.line11, l.line12, l.line13,
l.line14, l.line17,
] {
assert_eq!(
cell.fract(),
Usd::ZERO,
"printed cells are whole dollars: {cell}"
);
}
}
let neg = {
let a = ar_itemizing(parts(
dec!(10000),
dec!(-50000),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
dec!(10000),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
));
schedule_a_lines(&a, round_dollar(a.schedule_a.as_ref().unwrap().agi))
}
.unwrap();
assert_eq!(
neg.line2,
Usd::ZERO,
"a negative AGI is clamped — the floor must never HELP"
);
assert_eq!(neg.line3, Usd::ZERO);
assert_eq!(neg.line4, dec!(10000));
}
#[allow(clippy::too_many_arguments)]
fn f8949_for(ar: &AbsoluteReturn) -> Printed8949 {
let p = &ar.schedule_d;
Printed8949 {
short_term: Vec::new(),
long_term: Vec::new(),
st_totals: Printed8949Totals {
proceeds_d: round_dollar(p.st_proceeds_3d),
cost_e: round_dollar(p.st_cost_3e),
gain_h: round_dollar(p.st_gain_3h),
},
lt_totals: Printed8949Totals {
proceeds_d: round_dollar(p.lt_proceeds_10d),
cost_e: round_dollar(p.lt_cost_10e),
gain_h: round_dollar(p.lt_gain_10h),
},
}
}
fn sd_lines(ar: &AbsoluteReturn) -> ScheduleDLines {
let f = f8949_for(ar);
schedule_d_lines(ar, Some(&f))
}
fn ar_sched_d(
st_net: Usd,
lt_net: Usd,
st_cf: Usd,
lt_cf: Usd,
distr: Usd,
loss_ded: Usd,
qd: Usd,
) -> AbsoluteReturn {
use crate::tax::return_1040::ScheduleDParts;
let mut ar = ar_with(None, Usd::ZERO, Usd::ZERO);
ar.schedule_d = ScheduleDParts {
st_proceeds_3d: Usd::ZERO,
st_cost_3e: Usd::ZERO,
st_gain_3h: st_net + st_cf, st_carryover_6: st_cf,
st_net_7: st_net,
lt_proceeds_10d: Usd::ZERO,
lt_cost_10e: Usd::ZERO,
lt_gain_10h: lt_net + lt_cf - distr,
cap_gain_distr_13: distr,
lt_carryover_14: lt_cf,
lt_net_15: lt_net,
total_16: st_net + lt_net,
loss_deduction_21: loss_ded,
qualified_dividends: qd,
};
ar
}
#[test]
fn schedule_d_routing_both_gains() {
let l = sd_lines(&ar_sched_d(
dec!(5000),
dec!(20000),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
dec!(1200),
));
assert_eq!(l.line7, dec!(5000));
assert_eq!(l.line15, dec!(20000));
assert_eq!(l.line16, dec!(25000));
assert_eq!(l.routing, ScheduleDRouting::BothGains);
}
#[test]
fn schedule_d_routing_short_gain_long_loss() {
let l = sd_lines(&ar_sched_d(
dec!(30000),
dec!(-4000),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
dec!(1200),
));
assert_eq!(l.line16, dec!(26000));
assert_eq!(
l.routing,
ScheduleDRouting::ShortGainLongLoss { line22_yes: true }
);
let l = sd_lines(&ar_sched_d(
dec!(30000),
dec!(-4000),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
));
assert_eq!(
l.routing,
ScheduleDRouting::ShortGainLongLoss { line22_yes: false }
);
}
#[test]
fn schedule_d_routing_net_loss_line21_is_a_positive_magnitude() {
let l = sd_lines(&ar_sched_d(
dec!(-10000),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
dec!(3000),
dec!(800),
));
assert_eq!(l.line16, dec!(-10000));
match l.routing {
ScheduleDRouting::NetLoss { line21, line22_yes } => {
assert_eq!(line21, dec!(3000), "the §1211(b) cap");
assert!(
line21 > Usd::ZERO,
"★ line 21 is a MAGNITUDE — the form prints the ( )"
);
assert!(line22_yes);
}
other => panic!("expected NetLoss, got {other:?}"),
}
}
#[test]
fn schedule_d_routing_zero() {
let l = sd_lines(&ar_sched_d(
dec!(4000),
dec!(-4000),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
));
assert_eq!(l.line16, Usd::ZERO);
assert_eq!(l.routing, ScheduleDRouting::Zero { line22_yes: false });
}
#[test]
fn schedule_d_prints_the_lines_the_crypto_slice_omits() {
let l = sd_lines(&ar_sched_d(
dec!(1000), dec!(15000), dec!(2000), dec!(500), dec!(3000), Usd::ZERO,
Usd::ZERO,
));
assert_eq!(
l.line6,
dec!(2000),
"line 6 is filled — the crypto slice has no line 6"
);
assert_eq!(
l.line13,
dec!(3000),
"line 13 is filled — the crypto slice has no line 13"
);
assert_eq!(
l.line14,
dec!(500),
"line 14 is filled — the crypto slice has no line 14"
);
for paren in [l.line6, l.line14] {
assert!(paren >= Usd::ZERO, "paren cells are magnitudes: {paren}");
}
assert_eq!(l.line16, dec!(16000)); }
#[test]
fn form_1040_takes_the_printed_figures_from_its_schedules() {
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 mut ar = ar_with(Some(se), Usd::ZERO, Usd::ZERO);
ar.wages = dec!(280500);
ar.regular_tax = dec!(50000);
ar.deduction = dec!(29200);
let f8959 = form_8959_lines(FilingStatus::Mfj, dec!(280500), Usd::ZERO, Some(&se));
assert_eq!(f8959.line18, dec!(775), "the printed 8959 total");
assert_eq!(
round_dollar(dec!(274.50) + dec!(499.50)),
dec!(774),
"…the exact one differs"
);
let s2 = schedule_2_lines(None, &f8959, None).unwrap();
assert_eq!(
s2.line11,
dec!(775),
"Schedule 2 L11 = the 8959's PRINTED L18"
);
let sd = schedule_d_lines(&ar, None);
let l = {
let income = form_1040_income_lines(&ar, None, None, &sd);
form_1040_lines(
&ar,
&income,
None,
Some(&s2),
None,
&f8959,
None,
&tt(),
FilingStatus::Single,
Usd::ZERO,
Usd::ZERO,
false,
)
};
assert_eq!(l.line23, s2.line21, "1040 L23 = Schedule 2's PRINTED L21");
assert_eq!(
l.line24,
l.line22 + l.line23,
"TOTAL TAX sums the PRINTED lines"
);
assert_eq!(l.line25c, f8959.line24);
}
#[test]
fn form_1040_printed_lines_cross_foot() {
let mut ar = ar_with(None, dec!(287.40), dec!(1234.56));
ar.wages = dec!(120000.49);
ar.taxable_interest = dec!(2000.50);
ar.ordinary_dividends = dec!(4000.50);
ar.qualified_dividends = dec!(3000);
ar.regular_tax = dec!(18000.50);
ar.deduction = dec!(14600);
ar.withholding_25a = dec!(15000.49);
ar.withholding_25b = dec!(300.50);
let f8959 = form_8959_lines(FilingStatus::Single, dec!(120000), dec!(1800), None);
let s3 = schedule_3_lines(&ar).unwrap();
let sd = schedule_d_lines(&ar, None);
let l = {
let income = form_1040_income_lines(&ar, None, None, &sd);
form_1040_lines(
&ar,
&income,
None,
None,
Some(&s3),
&f8959,
None,
&tt(),
FilingStatus::Single,
Usd::ZERO,
dec!(500),
true,
)
};
assert_eq!(
l.line9,
l.line1z + l.line2b + l.line3b + l.line7 + l.line8,
"L9 = 1z+2b+3b+7+8"
);
assert_eq!(l.line11, l.line9 - l.line10, "AGI = 9 − 10");
assert_eq!(l.line14, l.line12 + l.line13, "L14 = 12 + 13");
assert_eq!(
l.line15,
(l.line11 - l.line14).max(Usd::ZERO),
"TI = 11 − 14, floored"
);
assert_eq!(l.line18, l.line16 + l.line17, "L18 = 16 + 17");
assert_eq!(l.line21, l.line19 + l.line20, "L21 = 19 + 20");
assert_eq!(
l.line22,
(l.line18 - l.line21).max(Usd::ZERO),
"L22 = 18 − 21, floored"
);
assert_eq!(l.line24, l.line22 + l.line23, "TOTAL TAX = 22 + 23");
assert_eq!(
l.line25d,
l.line25a + l.line25b + l.line25c,
"L25d = 25a+25b+25c"
);
assert_eq!(
l.line33,
l.line25d + l.line26 + l.line32,
"TOTAL PAYMENTS = 25d + 26 + 32"
);
assert_eq!(l.line34, (l.line33 - l.line24).max(Usd::ZERO));
assert_eq!(l.line37, (l.line24 - l.line33).max(Usd::ZERO));
assert!(
l.line34.is_zero() || l.line37.is_zero(),
"cannot both owe and be refunded"
);
assert_eq!(l.line20, s3.line8);
assert_eq!(l.line31, s3.line15);
assert_eq!(l.line19, Usd::ZERO);
for cell in [
l.line1z, l.line2b, l.line3a, l.line3b, l.line7, l.line8, l.line9, l.line10, l.line11,
l.line12, l.line13, l.line14, l.line15, l.line16, l.line18, l.line21, l.line22,
l.line24, l.line25a, l.line25b, l.line25c, l.line25d, l.line26, l.line33,
] {
assert_eq!(
cell.fract(),
Usd::ZERO,
"printed cells are whole dollars: {cell}"
);
}
}
#[test]
fn form_1040_line7_on_a_loss_year_is_the_limited_amount_with_a_leading_minus() {
let mut ar = ar_sched_d(
dec!(-10000),
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
Usd::ZERO,
dec!(3000), Usd::ZERO,
);
ar.wages = dec!(80000);
ar.deduction = dec!(14600);
let sd = sd_lines(&ar);
assert_eq!(
sd.line16,
dec!(-10000),
"Schedule D's own line 16 is the FULL loss"
);
assert!(
matches!(sd.routing, ScheduleDRouting::NetLoss { line21, .. } if line21 == dec!(3000))
);
let f8959 = form_8959_lines(FilingStatus::Single, dec!(80000), Usd::ZERO, None);
let l = {
let income = form_1040_income_lines(&ar, None, None, &sd);
form_1040_lines(
&ar,
&income,
None,
None,
None,
&f8959,
None,
&tt(),
FilingStatus::Single,
Usd::ZERO,
Usd::ZERO,
true,
)
};
assert_eq!(
l.line7,
dec!(-3000),
"★ the §1211-LIMITED loss, not the full −10,000"
);
assert!(
l.line7 < Usd::ZERO,
"a leading minus — 1040 L7 is not a paren box"
);
}
}