use crate::donation::DonationDetails;
use crate::event::LedgerEvent;
use crate::identity::EventId;
use crate::state::LedgerState;
use crate::tax::other_taxes::{form_8959_lines, form_8960_lines, Form8959Lines, Form8960Lines};
use crate::tax::printed::{
form_1040_income_lines, form_1040_lines, form_8949_printed, schedule_1_lines, schedule_2_lines,
schedule_3_lines, schedule_a_lines, schedule_b_lines, schedule_c_lines, schedule_d_lines,
schedule_se_lines, Form1040Lines, Printed8949, Schedule1Lines, Schedule2Lines, Schedule3Lines,
ScheduleALines, ScheduleBLines, ScheduleCLines, ScheduleDLines, ScheduleSeLines,
};
use crate::tax::printed::{form_8283_printed, Printed8283Rows, FORM_8283_THRESHOLD};
use crate::tax::printed::{printed_8275, Printed8275};
use crate::tax::qbi::{form_8995_lines, Form8995Lines};
use crate::tax::questions::{QuestionId, FORM_QUESTIONS};
use crate::tax::return_1040::{is_aged, AbsoluteReturn};
use crate::tax::return_inputs::{Owner, Person, ReturnInputs};
use crate::tax::tables::TaxTable;
use crate::tax::types::FilingStatus;
use std::collections::BTreeMap;
use std::fmt;
#[derive(Clone, PartialEq, Eq)]
pub struct Ssn(String);
impl Ssn {
pub fn canonical(raw: &str) -> Result<Self, SsnError> {
let digits: String = raw
.chars()
.filter(|c| !c.is_whitespace() && *c != '-')
.collect();
if digits.is_empty() {
return Err(SsnError::Missing);
}
if let Some(c) = digits.chars().find(|c| !c.is_ascii_digit()) {
return Err(SsnError::NotDigits(c));
}
if digits.len() != 9 {
return Err(SsnError::WrongLength(digits.len()));
}
Ok(Self(digits))
}
pub fn digits(&self) -> &str {
&self.0
}
pub fn hyphenated(&self) -> String {
format!("{}-{}-{}", &self.0[0..3], &self.0[3..5], &self.0[5..9])
}
}
impl fmt::Debug for Ssn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Ssn(***-**-{})", &self.0[5..9])
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SsnError {
Missing,
NotDigits(char),
WrongLength(usize),
}
impl fmt::Display for SsnError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Missing => write!(f, "no SSN was entered"),
Self::NotDigits(c) => write!(f, "contains {c:?}, which is not a digit"),
Self::WrongLength(n) => write!(f, "has {n} digits — an SSN has exactly 9"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum HeaderError {
Ssn(SsnError),
Unanswered(QuestionId),
MfjWithoutSpouse,
}
impl From<SsnError> for HeaderError {
fn from(e: SsnError) -> Self {
HeaderError::Ssn(e)
}
}
impl fmt::Display for HeaderError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Ssn(e) => write!(f, "an SSN {e} — fix the identity and re-run"),
Self::Unanswered(id) => write!(
f,
"the {id:?} question is unanswered, and an unanswered box must not reach a filed form — \
run `btctax income answer`"
),
Self::MfjWithoutSpouse => write!(
f,
"a married-filing-jointly return has no spouse on file — the joint name and SSN cannot be \
printed; add the spouse's identity (`btctax set-pii`) or change the filing status"
),
}
}
}
#[derive(Clone, PartialEq, Eq)]
pub struct IpPin(String);
impl IpPin {
pub fn canonical(raw: &str) -> Result<Self, SsnError> {
let digits: String = raw.chars().filter(|c| !c.is_whitespace()).collect();
if digits.is_empty() {
return Err(SsnError::Missing);
}
if let Some(c) = digits.chars().find(|c| !c.is_ascii_digit()) {
return Err(SsnError::NotDigits(c));
}
if digits.len() != 6 {
return Err(SsnError::WrongLength(digits.len()));
}
Ok(Self(digits))
}
pub fn digits(&self) -> &str {
&self.0
}
}
impl fmt::Debug for IpPin {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "IpPin(******)")
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FiledPerson {
pub first_name: String,
pub last_name: String,
pub ssn: Ssn,
pub occupation: String,
}
impl FiledPerson {
fn build(p: &Person) -> Result<Self, SsnError> {
Ok(Self {
first_name: p.first_name.clone(),
last_name: p.last_name.clone(),
ssn: Ssn::canonical(&p.ssn)?,
occupation: p.occupation.clone(),
})
}
pub fn full_name(&self) -> String {
format!("{} {}", self.first_name, self.last_name)
.trim()
.to_string()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DependentRow {
pub name: String,
pub ssn: Ssn,
pub relationship: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct AgedBlindBoxes {
pub taxpayer_aged: bool,
pub taxpayer_blind: bool,
pub spouse_aged: bool,
pub spouse_blind: bool,
}
impl AgedBlindBoxes {
pub fn for_return(ri: &ReturnInputs, year: i32) -> Self {
let t = &ri.header.taxpayer;
let joint_spouse = ri
.header
.spouse
.as_ref()
.filter(|_| ri.filing_status == FilingStatus::Mfj);
Self {
taxpayer_aged: is_aged(t.date_of_birth, year),
taxpayer_blind: t.blind == Some(true),
spouse_aged: joint_spouse.is_some_and(|s| is_aged(s.date_of_birth, year)),
spouse_blind: joint_spouse.is_some_and(|s| s.blind == Some(true)),
}
}
pub fn count(&self) -> u32 {
u32::from(self.taxpayer_aged)
+ u32::from(self.taxpayer_blind)
+ u32::from(self.spouse_aged)
+ u32::from(self.spouse_blind)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReturnHeader {
pub name_line: String,
pub taxpayer: FiledPerson,
pub spouse: Option<FiledPerson>,
pub address_street: String,
pub address_city: String,
pub address_state: String,
pub address_zip: String,
pub aged_blind: AgedBlindBoxes,
pub claimed_as_dependent_taxpayer: bool,
pub claimed_as_dependent_spouse: bool,
pub mfs_spouse_itemizes: bool,
pub presidential_fund_taxpayer: bool,
pub presidential_fund_spouse: bool,
pub ip_pin: Option<IpPin>,
pub dependents: Vec<DependentRow>,
pub proprietor: Option<FiledPerson>,
}
impl ReturnHeader {
pub fn build(ri: &ReturnInputs, year: i32) -> Result<Self, HeaderError> {
for q in FORM_QUESTIONS {
if (q.live)(ri) && (q.get)(ri).is_none() {
return Err(HeaderError::Unanswered(q.id));
}
}
if ri.filing_status == FilingStatus::Mfj && ri.header.spouse.is_none() {
return Err(HeaderError::MfjWithoutSpouse);
}
let taxpayer = FiledPerson::build(&ri.header.taxpayer)?;
let spouse = ri
.header
.spouse
.as_ref()
.map(FiledPerson::build)
.transpose()?;
let name_line = match (ri.filing_status, &spouse) {
(FilingStatus::Mfj, Some(s)) => format!("{} & {}", taxpayer.full_name(), s.full_name()),
_ => taxpayer.full_name(),
};
let proprietor = match ri.schedule_c.as_ref().map(|c| c.owner) {
None => None,
Some(Owner::Taxpayer) => Some(taxpayer.clone()),
Some(Owner::Spouse) => Some(spouse.clone().unwrap_or_else(|| taxpayer.clone())),
};
let dependents = ri
.header
.dependents
.iter()
.map(|d| {
Ok(DependentRow {
name: d.name.clone(),
ssn: Ssn::canonical(&d.ssn)?,
relationship: d.relationship.clone(),
})
})
.collect::<Result<Vec<_>, SsnError>>()?;
Ok(Self {
name_line,
taxpayer,
spouse,
address_street: ri.header.address_street.clone(),
address_city: ri.header.address_city.clone(),
address_state: ri.header.address_state.clone(),
address_zip: ri.header.address_zip.clone(),
aged_blind: AgedBlindBoxes::for_return(ri, year),
claimed_as_dependent_taxpayer: ri.header.can_be_claimed_as_dependent_taxpayer
== Some(true),
claimed_as_dependent_spouse: ri.header.can_be_claimed_as_dependent_spouse == Some(true),
mfs_spouse_itemizes: ri.filing_status == FilingStatus::Mfs
&& ri.mfs_spouse_itemizes == Some(true),
presidential_fund_taxpayer: ri.header.presidential_fund_taxpayer,
presidential_fund_spouse: ri.header.presidential_fund_spouse,
ip_pin: ri
.header
.ip_pin
.as_deref()
.map(IpPin::canonical)
.transpose()?,
dependents,
proprietor,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PrintedReturn {
pub header: ReturnHeader,
pub filing_status: FilingStatus,
pub forms: PrintedForms,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PrintedForms {
pub f1040: Form1040Lines,
pub sch_1: Option<Schedule1Lines>,
pub sch_2: Option<Schedule2Lines>,
pub sch_3: Option<Schedule3Lines>,
pub sch_a: Option<ScheduleALines>,
pub sch_b: Option<ScheduleBLines>,
pub sch_c: Option<ScheduleCLines>,
pub sch_d: ScheduleDLines,
pub f8949: Option<Printed8949>,
pub sch_se: Option<ScheduleSeLines>,
pub f8959: Form8959Lines,
pub f8960: Option<Form8960Lines>,
pub f8995: Option<Form8995Lines>,
pub f8283: Option<Printed8283Rows>,
pub f8275: Option<Printed8275>,
}
pub fn assemble_printed_return(
ri: &ReturnInputs,
state: &LedgerState,
donation_details: &BTreeMap<EventId, DonationDetails>,
ar: &AbsoluteReturn,
table: &TaxTable,
year: i32,
events: &[LedgerEvent],
) -> Result<PrintedReturn, HeaderError> {
Ok(PrintedReturn {
header: ReturnHeader::build(ri, year)?,
filing_status: ri.filing_status,
forms: assemble_printed_forms(ri, state, donation_details, ar, table, year, events),
})
}
pub fn assemble_printed_forms(
ri: &ReturnInputs,
state: &LedgerState,
donation_details: &BTreeMap<EventId, DonationDetails>,
ar: &AbsoluteReturn,
table: &TaxTable,
year: i32,
events: &[LedgerEvent],
) -> PrintedForms {
let status = ri.filing_status;
let pi = &ar.printed_inputs;
let f8949 = form_8949_printed(&crate::forms::form_8949(state, year));
let f8959 = form_8959_lines(
status,
pi.medicare_wages,
pi.medicare_withheld,
ar.se.as_ref(),
);
let f8960 = form_8960_lines(
status,
ar.taxable_interest,
ar.ordinary_dividends,
ar.capital_gain,
pi.crypto_lending_interest,
ar.agi,
);
let f8995 = form_8995_lines(
&pi.schedule_c_header.business_description,
pi.business_qbi,
pi.reit_dividends,
pi.reit_ptp_carryforward_in,
pi.ti_before_qbi,
pi.qbi_net_capital_gain,
);
let sch_b = schedule_b_lines(ri);
let sch_c = schedule_c_lines(ar);
let sch_d = schedule_d_lines(ar, f8949.as_ref());
let sch_1 = schedule_1_lines(ar);
let income = form_1040_income_lines(ar, sch_b.as_ref(), sch_1.as_ref(), &sch_d);
let sch_a = schedule_a_lines(ar, income.line11);
let sch_se = sch_c.as_ref().and_then(|c| schedule_se_lines(ar, c));
let sch_2 = schedule_2_lines(sch_se.as_ref(), &f8959, f8960.as_ref());
let sch_3 = schedule_3_lines(ar);
let f8283 = sch_a
.as_ref()
.filter(|a| a.line12 > FORM_8283_THRESHOLD)
.and_then(|_| form_8283_printed(&crate::forms::form_8283(state, year, donation_details)));
let f8275 =
crate::tax::form8275::disclosure_8275(events, state, year).map(|d| printed_8275(&d));
let f1040 = form_1040_lines(
ar,
&income,
sch_a.as_ref(),
sch_2.as_ref(),
sch_3.as_ref(),
&f8959,
f8995.as_ref(),
table,
status,
ri.payments.other_withholding,
ri.payments.estimated_tax_payments,
pi.digital_asset_activity,
);
PrintedForms {
f1040,
sch_1,
sch_2,
sch_3,
sch_a,
sch_b,
sch_c,
sch_d,
f8949,
sch_se,
f8959,
f8960,
f8995,
f8283,
f8275,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tax::return_1040::assemble_absolute;
use crate::tax::return_inputs::{Dependent, HouseholdHeader, ScheduleCInputs, W2};
use crate::tax::testonly::{
kitchen_sink_household, ty2024_params, ty2024_table, w2_only_household,
};
use rust_decimal_macros::dec;
use time::macros::date;
fn person(first: &str, last: &str, ssn: &str) -> Person {
Person {
first_name: first.into(),
last_name: last.into(),
ssn: ssn.into(),
..Default::default()
}
}
#[test]
fn ssn_canonical_accepts_hyphenated_bare_and_spaced() {
for raw in ["123-45-6789", "123456789", " 123 45 6789 "] {
let ssn = Ssn::canonical(raw).expect("a nine-digit SSN is canonicalizable");
assert_eq!(ssn.digits(), "123456789", "raw = {raw:?}");
assert_eq!(ssn.hyphenated(), "123-45-6789", "raw = {raw:?}");
}
}
#[test]
fn ssn_canonical_rejects_anything_that_is_not_nine_digits() {
for raw in ["", "12345678", "1234567890", "123-45-678X", "not an ssn"] {
assert!(
Ssn::canonical(raw).is_err(),
"{raw:?} must not canonicalize into an SSN"
);
}
}
#[test]
fn header_name_line_is_joint_on_a_joint_return() {
let mut ri = ReturnInputs {
filing_status: FilingStatus::Mfj,
..Default::default()
};
ri.header.taxpayer = person("John", "Doe", "123-45-6789");
ri.header.spouse = Some(person("Jane", "Doe", "987-65-4321"));
let h = ReturnHeader::build(&crate::tax::testonly::answered(ri.clone()), 2024).unwrap();
assert_eq!(h.name_line, "John Doe & Jane Doe");
assert_eq!(h.taxpayer.ssn.hyphenated(), "123-45-6789");
assert_eq!(
h.spouse.as_ref().map(|s| s.ssn.hyphenated()),
Some("987-65-4321".to_string())
);
}
#[test]
fn header_name_line_is_the_taxpayer_alone_when_not_joint() {
for status in [FilingStatus::Single, FilingStatus::Mfs, FilingStatus::HoH] {
let mut ri = ReturnInputs {
filing_status: status,
..Default::default()
};
ri.header.taxpayer = person("John", "Doe", "123456789");
ri.header.spouse = Some(person("Jane", "Doe", "987654321"));
let h = ReturnHeader::build(&crate::tax::testonly::answered(ri.clone()), 2024).unwrap();
assert_eq!(h.name_line, "John Doe", "status = {status:?}");
}
}
#[test]
fn schedule_c_proprietor_is_the_business_owner_not_the_joint_name_line() {
let mut ri = ReturnInputs {
filing_status: FilingStatus::Mfj,
schedule_c: Some(ScheduleCInputs {
owner: Owner::Spouse,
..Default::default()
}),
..Default::default()
};
ri.header.taxpayer = person("John", "Doe", "123456789");
ri.header.spouse = Some(person("Jane", "Roe", "987654321"));
let h = ReturnHeader::build(&crate::tax::testonly::answered(ri.clone()), 2024).unwrap();
let p = h
.proprietor
.as_ref()
.expect("a Schedule C has a proprietor");
assert_eq!(p.full_name(), "Jane Roe");
assert_eq!(p.ssn.hyphenated(), "987-65-4321");
assert_eq!(h.name_line, "John Doe & Jane Roe");
}
#[test]
fn aged_blind_box_count_matches_the_standard_deduction_core_actually_computed() {
let p = ty2024_params();
let mut ri = ReturnInputs {
filing_status: FilingStatus::Mfj,
w2s: vec![W2 {
box1_wages: dec!(90000),
..Default::default()
}],
..Default::default()
};
ri.header.taxpayer = Person {
date_of_birth: Some(date!(1955 - 03 - 02)),
blind: Some(true),
..person("John", "Doe", "123456789")
};
ri.header.spouse = Some(Person {
blind: Some(true),
..person("Jane", "Doe", "987654321")
});
let h = ReturnHeader::build(&crate::tax::testonly::answered(ri.clone()), 2024).unwrap();
assert!(h.aged_blind.taxpayer_aged);
assert!(h.aged_blind.taxpayer_blind);
assert!(!h.aged_blind.spouse_aged);
assert!(h.aged_blind.spouse_blind);
assert_eq!(h.aged_blind.count(), 3);
let ar = assemble_absolute(&ri, &Default::default(), &p, &ty2024_table(), 2024);
let expected = p.std_deduction_for(FilingStatus::Mfj)
+ p.std_aged_blind_married * rust_decimal::Decimal::from(h.aged_blind.count());
assert_eq!(ar.standard_deduction, expected);
}
#[test]
fn aged_blind_ignores_the_spouse_unless_the_return_is_joint() {
let mut ri = ReturnInputs {
filing_status: FilingStatus::Mfs,
..Default::default()
};
ri.header.taxpayer = person("John", "Doe", "123456789");
ri.header.spouse = Some(Person {
blind: Some(true),
date_of_birth: Some(date!(1950 - 01 - 01)),
..person("Jane", "Doe", "987654321")
});
let h = ReturnHeader::build(&crate::tax::testonly::answered(ri.clone()), 2024).unwrap();
assert!(!h.aged_blind.spouse_aged);
assert!(!h.aged_blind.spouse_blind);
assert_eq!(h.aged_blind.count(), 0);
}
#[test]
fn dependents_carry_through_with_canonical_ssns() {
let ri = ReturnInputs {
filing_status: FilingStatus::Single,
header: HouseholdHeader {
taxpayer: person("John", "Doe", "123456789"),
dependents: vec![Dependent {
name: "Sam Doe".into(),
ssn: "111223333".into(),
relationship: "Son".into(),
..Default::default()
}],
..Default::default()
},
..Default::default()
};
let h = ReturnHeader::build(&crate::tax::testonly::answered(ri.clone()), 2024).unwrap();
assert_eq!(h.dependents.len(), 1);
assert_eq!(h.dependents[0].name, "Sam Doe");
assert_eq!(h.dependents[0].ssn.hyphenated(), "111-22-3333");
assert_eq!(h.dependents[0].relationship, "Son");
}
#[test]
fn a_bad_ssn_anywhere_in_the_household_fails_the_header() {
let base = |ssn_t: &str, ssn_s: &str, ssn_d: &str| ReturnInputs {
filing_status: FilingStatus::Mfj,
header: HouseholdHeader {
taxpayer: person("John", "Doe", ssn_t),
spouse: Some(person("Jane", "Doe", ssn_s)),
dependents: vec![Dependent {
name: "Sam Doe".into(),
ssn: ssn_d.into(),
relationship: "Son".into(),
..Default::default()
}],
..Default::default()
},
..Default::default()
};
let good = "123456789";
let a = crate::tax::testonly::answered;
assert!(ReturnHeader::build(&a(base(good, good, good)), 2024).is_ok());
assert!(ReturnHeader::build(&a(base("12345", good, good)), 2024).is_err());
assert!(ReturnHeader::build(&a(base(good, "nope", good)), 2024).is_err());
assert!(ReturnHeader::build(&a(base(good, good, "1234567890")), 2024).is_err());
}
#[test]
fn build_refuses_an_unanswered_live_declaration() {
let mut ri = ReturnInputs {
filing_status: FilingStatus::Single,
..Default::default()
};
ri.header.taxpayer = person("John", "Doe", "123456789"); crate::tax::testonly::answer_all_live_declarations(&mut ri);
assert!(
ReturnHeader::build(&ri, 2024).is_ok(),
"fully answered ⇒ builds"
);
ri.sch1.hsa_activity = None;
assert_eq!(
ReturnHeader::build(&ri, 2024),
Err(HeaderError::Unanswered(QuestionId::HsaActivity)),
"an unanswered live declaration must not reach a printed form (P8a I3)"
);
}
#[test]
fn build_refuses_mfj_with_no_spouse_identity() {
let mut ri = ReturnInputs {
filing_status: FilingStatus::Mfj,
..Default::default()
};
ri.header.taxpayer = person("John", "Doe", "123456789");
ri.header.spouse = None; crate::tax::testonly::answer_all_live_declarations(&mut ri); assert_eq!(
ReturnHeader::build(&ri, 2024),
Err(HeaderError::MfjWithoutSpouse),
"MFJ with no spouse Person cannot print the joint header (r3 M-6)"
);
}
#[test]
fn the_header_carries_the_dependent_claim_and_mfs_itemize_flags_that_l12_depends_on() {
let mut ri = ReturnInputs {
filing_status: FilingStatus::Single,
..Default::default()
};
ri.header.taxpayer = person("John", "Doe", "123456789");
ri.header.can_be_claimed_as_dependent_taxpayer = Some(true);
let h = ReturnHeader::build(&crate::tax::testonly::answered(ri.clone()), 2024).unwrap();
assert!(
h.claimed_as_dependent_taxpayer,
"the §63(c)(5) floor is claimed ⇒ the box must print"
);
assert!(!h.mfs_spouse_itemizes);
let mut ri = ReturnInputs {
filing_status: FilingStatus::Mfs,
mfs_spouse_itemizes: Some(true),
..Default::default()
};
ri.header.taxpayer = person("John", "Doe", "123456789");
let h = ReturnHeader::build(&crate::tax::testonly::answered(ri.clone()), 2024).unwrap();
assert!(
h.mfs_spouse_itemizes,
"§63(c)(6) coupling is in force ⇒ the box must print"
);
}
#[test]
fn the_assembled_packet_ties_the_1040_to_its_attachments() {
let (ri, state) = kitchen_sink_household();
let ar = assemble_absolute(&ri, &state, &ty2024_params(), &ty2024_table(), 2024);
let pr = assemble_printed_return(
&ri,
&state,
&BTreeMap::new(),
&ar,
&ty2024_table(),
2024,
&[],
)
.unwrap();
let sch_1 = pr
.forms
.sch_1
.expect("the kitchen sink has Schedule 1 income");
let sch_2 = pr.forms.sch_2.expect("…and SE tax ⇒ Schedule 2");
let sch_a = pr.forms.sch_a.expect("…and itemized deductions");
let sch_b = pr
.forms
.sch_b
.expect("…and > $1,500 of interest ⇒ Schedule B");
let sch_c = pr.forms.sch_c.expect("…and business mining ⇒ Schedule C");
let f8995 = pr.forms.f8995.expect("…and REIT dividends ⇒ Form 8995");
assert_eq!(
pr.forms.f1040.line2b, sch_b.line4,
"1040 2b ← Schedule B line 4"
);
assert_eq!(
pr.forms.f1040.line3b, sch_b.line6,
"1040 3b ← Schedule B line 6"
);
assert_eq!(
pr.forms.f1040.line8, sch_1.line10,
"1040 8 ← Schedule 1 line 10"
);
assert_eq!(
pr.forms.f1040.line10, sch_1.line26,
"1040 10 ← Schedule 1 line 26"
);
assert_eq!(
pr.forms.f1040.line12, sch_a.line17,
"1040 12 ← Schedule A line 17"
);
assert_eq!(
pr.forms.f1040.line13, f8995.line15,
"1040 13 ← Form 8995 line 15"
);
assert_eq!(
pr.forms.f1040.line23, sch_2.line21,
"1040 23 ← Schedule 2 line 21"
);
assert_eq!(
sch_2.line11, pr.forms.f8959.line18,
"Sch 2 11 ← Form 8959 line 18"
);
let sch_se = pr.forms.sch_se.expect("…and business mining ⇒ Schedule SE");
let f8949 = pr
.forms
.f8949
.as_ref()
.expect("…and a disposal ⇒ Form 8949");
assert_eq!(
sch_2.line4, sch_se.line12,
"Sch 2 L4 ← Schedule SE's printed L12"
);
assert_eq!(
sch_1.line15, sch_se.line13,
"Sch 1 L15 ← Schedule SE's printed L13"
);
assert_eq!(
pr.forms.f8959.line8, sch_se.line6,
"8959 L8 ← Schedule SE Part I L6"
);
assert_eq!(
sch_se.line2, sch_c.line31,
"SE L2 ← Schedule C's printed L31"
);
assert_eq!(
pr.forms.sch_d.line10_d, f8949.lt_totals.proceeds_d,
"Sch D L10(d) ← the 8949's printed long-term column total"
);
assert_eq!(
pr.forms.sch_d.line10_h,
pr.forms.sch_d.line10_d - pr.forms.sch_d.line10_e,
"…and Schedule D Part II cross-foots on its own printed cells"
);
let sch_3 = pr
.forms
.sch_3
.expect("…and an extension payment + FTC ⇒ Schedule 3");
assert_eq!(
pr.forms.f1040.line31, sch_3.line15,
"1040 31 ← Schedule 3 line 15"
);
assert_eq!(
sch_3.line10,
dec!(500),
"the kitchen sink paid $500 with its extension"
);
assert!(
sch_3.line15 >= sch_3.line10,
"L15 = 'add 9 through 12 and 14' — it can never DROP the extension payment"
);
}
#[test]
fn the_packet_omits_every_form_that_is_not_required() {
let (ri, state) = w2_only_household();
let ar = assemble_absolute(&ri, &state, &ty2024_params(), &ty2024_table(), 2024);
let pr = assemble_printed_return(
&ri,
&state,
&BTreeMap::new(),
&ar,
&ty2024_table(),
2024,
&[],
)
.unwrap();
assert!(
pr.forms.sch_1.is_none(),
"no additional income or adjustments"
);
assert!(
pr.forms.sch_2.is_none(),
"no SE / Additional Medicare / NIIT"
);
assert!(pr.forms.sch_3.is_none(), "no credits");
assert!(pr.forms.sch_a.is_none(), "standard deduction");
assert!(
pr.forms.sch_b.is_none(),
"interest under the $1,500 threshold"
);
assert!(pr.forms.sch_c.is_none(), "no business");
assert!(pr.forms.f8960.is_none(), "no NIIT");
assert!(pr.forms.f8995.is_none(), "no QBI");
assert!(
!pr.forms.f8959.must_file(),
"no Additional Medicare Tax, none withheld"
);
}
#[test]
fn the_printed_8959_reads_the_same_box5_sum_the_computed_8959_used() {
let (ri, state) = kitchen_sink_household();
let ar = assemble_absolute(&ri, &state, &ty2024_params(), &ty2024_table(), 2024);
let pr = assemble_printed_return(
&ri,
&state,
&BTreeMap::new(),
&ar,
&ty2024_table(),
2024,
&[],
)
.unwrap();
let box5_sum: crate::conventions::Usd = ri.w2s.iter().map(|w| w.box5_medicare_wages).sum();
assert_eq!(ar.printed_inputs.medicare_wages, box5_sum);
assert_eq!(
pr.forms.f8959.line1,
crate::conventions::round_dollar(box5_sum)
);
}
}