use crate::conventions::Usd;
use crate::event::{BasisSource, DisposeKind, IncomeKind};
use crate::identity::{EventId, LotId, WalletId};
use crate::state::{Disposal, DisposalLeg, IncomeRecord, LedgerState, Term};
use crate::tax::questions::{QuestionId, FORM_QUESTIONS};
use crate::tax::return_inputs::{
CharitableClass, CharitableGift, Dependent, Form1099Div, Form1099G, Form1099Int,
HouseholdHeader, Owner, Payments, Person, ReturnInputs, ScheduleAInputs, ScheduleCInputs, W2,
};
use crate::tax::tables::{
AmtParams, FullReturnParams, LtcgBreakpoints, OrdinaryBracket, OrdinarySchedule, TaxTable,
};
use crate::tax::types::FilingStatus;
use rust_decimal_macros::dec;
pub fn answer_all_live_declarations(ri: &mut ReturnInputs) {
for q in FORM_QUESTIONS {
if (q.live)(ri) && (q.get)(ri).is_none() {
let ans = matches!(q.id, QuestionId::MortgageAllUsedToBuyBuildImprove);
(q.set)(ri, ans);
}
}
}
pub fn answered(mut ri: ReturnInputs) -> ReturnInputs {
answer_all_live_declarations(&mut ri);
ri
}
use serde::Deserialize;
use std::collections::BTreeMap;
use time::macros::date;
pub fn ty2024_params() -> FullReturnParams {
let mut std_deduction = BTreeMap::new();
std_deduction.insert(FilingStatus::Single, dec!(14600));
std_deduction.insert(FilingStatus::Mfj, dec!(29200));
std_deduction.insert(FilingStatus::Mfs, dec!(14600));
std_deduction.insert(FilingStatus::HoH, dec!(21900));
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: 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),
},
}
}
pub fn ty2024_table() -> TaxTable {
let mut ordinary = BTreeMap::new();
ordinary.insert(
FilingStatus::Single,
OrdinarySchedule {
brackets: vec![
bracket(dec!(0), dec!(0.10)),
bracket(dec!(11600), dec!(0.12)),
bracket(dec!(47150), dec!(0.22)),
bracket(dec!(100525), dec!(0.24)),
bracket(dec!(191950), dec!(0.32)),
bracket(dec!(243725), dec!(0.35)),
bracket(dec!(609350), dec!(0.37)),
],
},
);
ordinary.insert(
FilingStatus::Mfj,
OrdinarySchedule {
brackets: vec![
bracket(dec!(0), dec!(0.10)),
bracket(dec!(23200), dec!(0.12)),
bracket(dec!(94300), dec!(0.22)),
bracket(dec!(201050), dec!(0.24)),
bracket(dec!(383900), dec!(0.32)),
bracket(dec!(487450), dec!(0.35)),
bracket(dec!(731200), dec!(0.37)),
],
},
);
let mut ltcg = BTreeMap::new();
ltcg.insert(
FilingStatus::Single,
LtcgBreakpoints {
max_zero: dec!(47025),
max_fifteen: dec!(518900),
},
);
ltcg.insert(
FilingStatus::Mfj,
LtcgBreakpoints {
max_zero: dec!(94050),
max_fifteen: dec!(583750),
},
);
TaxTable {
year: 2024,
source: "TEST-TY2024",
ordinary,
ltcg,
gift_annual_exclusion: dec!(18000),
ss_wage_base: dec!(168600),
gift_lifetime_exclusion: dec!(13_610_000),
}
}
fn bracket(lower: Usd, rate: Usd) -> OrdinaryBracket {
OrdinaryBracket { lower, rate }
}
fn person(first: &str, last: &str, ssn: &str, occupation: &str) -> Person {
Person {
first_name: first.into(),
last_name: last.into(),
ssn: ssn.into(),
occupation: occupation.into(),
..Default::default()
}
}
pub fn kitchen_sink_household() -> (ReturnInputs, LedgerState) {
let mut ri = ReturnInputs {
filing_status: FilingStatus::Mfj,
header: HouseholdHeader {
taxpayer: person("John", "Doe", "123-45-6789", "Engineer"),
spouse: Some(person("Jane", "Doe", "987-65-4321", "Architect")),
address_street: "100 Main St".into(),
address_city: "Springfield".into(),
address_state: "IL".into(),
address_zip: "62704".into(),
dependents: vec![Dependent {
name: "Sam Doe".into(),
ssn: "111-22-3333".into(),
relationship: "Son".into(),
date_of_birth: Some(date!(2012 - 04 - 15)),
}],
..Default::default()
},
w2s: vec![
W2 {
owner: Owner::Taxpayer,
employer: "ACME".into(),
box1_wages: dec!(200000),
box2_fed_withheld: dec!(40000),
box3_ss_wages: dec!(168600),
box4_ss_withheld: dec!(10453.20),
box5_medicare_wages: dec!(200000),
box6_medicare_withheld: dec!(2900),
box17_state_tax_withheld: dec!(9000),
..Default::default()
},
W2 {
owner: Owner::Spouse,
employer: "GLOBEX".into(),
box1_wages: dec!(90000),
box2_fed_withheld: dec!(15000),
box3_ss_wages: dec!(90000),
box4_ss_withheld: dec!(5580),
box5_medicare_wages: dec!(90000),
box6_medicare_withheld: dec!(1305),
box17_state_tax_withheld: dec!(4000),
..Default::default()
},
],
int_1099: vec![Form1099Int {
payer: "First Bank".into(),
box1_interest: dec!(2000),
box4_fed_withheld: dec!(100),
box6_foreign_tax: dec!(100),
..Default::default()
}],
div_1099: vec![Form1099Div {
payer: "Broker LLC".into(),
box1a_ordinary: dec!(3000),
box1b_qualified: dec!(1000),
box2a_capgain_distr: dec!(500),
box5_section_199a: dec!(1200),
box7_foreign_tax: dec!(50),
..Default::default()
}],
g_1099: vec![Form1099G {
payer: "State of IL".into(),
box1_unemployment: dec!(1000),
..Default::default()
}],
schedule_c: Some(ScheduleCInputs {
owner: Owner::Taxpayer,
business_description: "Bitcoin mining".into(),
naics_code: "518210".into(),
expenses: dec!(1000),
..Default::default()
}),
schedule_a: Some(ScheduleAInputs {
medical: dec!(2000),
salt_state_estimated_payments: dec!(12000),
salt_real_estate: dec!(6000),
mortgage_interest_1098: dec!(22000),
charitable: vec![CharitableGift {
class: CharitableClass::Cash60,
amount: dec!(5000),
}],
..Default::default()
}),
payments: Payments {
estimated_tax_payments: dec!(1000),
extension_payment: dec!(500), ..Default::default()
},
foreign_accounts: Some(false), foreign_trust: Some(false),
..Default::default()
};
let state = LedgerState {
income_recognized: vec![IncomeRecord {
event: EventId::decision(1),
recognized_at: date!(2024 - 06 - 01),
sat: 100_000_000,
usd_fmv: dec!(20000),
kind: IncomeKind::Mining,
business: true,
pseudo: false,
}],
disposals: vec![Disposal {
event: EventId::decision(2),
kind: DisposeKind::Sell,
disposed_at: date!(2024 - 05 - 01),
legs: vec![DisposalLeg {
lot_id: LotId {
origin_event_id: EventId::decision(3),
split_sequence: 0,
},
sat: 100_000_000,
proceeds: dec!(30000),
basis: dec!(10000),
gain: dec!(20000),
term: Term::LongTerm,
basis_source: BasisSource::ExchangeProvided,
gift_zone: None,
acquired_at: date!(2020 - 01 - 01),
wallet: WalletId::SelfCustody {
label: "cold".into(),
},
pseudo: false,
}],
fee_mini_disposition: false,
}],
..Default::default()
};
answer_all_live_declarations(&mut ri);
(ri, state)
}
pub fn kitchen_sink_header() -> crate::tax::packet::ReturnHeader {
let (ri, _) = kitchen_sink_household();
crate::tax::packet::ReturnHeader::build(&ri, 2024).expect("the fixture's SSNs are canonical")
}
pub fn w2_only_household() -> (ReturnInputs, LedgerState) {
let mut ri = ReturnInputs {
filing_status: FilingStatus::Single,
header: HouseholdHeader {
taxpayer: person("Pat", "Roe", "222-33-4444", "Teacher"),
address_street: "9 Elm St".into(),
address_city: "Springfield".into(),
address_state: "IL".into(),
address_zip: "62704".into(),
..Default::default()
},
w2s: vec![W2 {
owner: Owner::Taxpayer,
employer: "SCHOOL DISTRICT".into(),
box1_wages: dec!(60000),
box2_fed_withheld: dec!(6000),
box3_ss_wages: dec!(60000),
box4_ss_withheld: dec!(3720),
box5_medicare_wages: dec!(60000),
box6_medicare_withheld: dec!(870), ..Default::default()
}],
..Default::default()
};
answer_all_live_declarations(&mut ri);
(ri, LedgerState::default())
}
pub const GOLDEN_RETURNS_JSON: &str = include_str!("../../tests/goldens/full_return_goldens.json");
pub fn golden_households() -> Vec<GoldenHousehold> {
let g: Goldens = serde_json::from_str(GOLDEN_RETURNS_JSON).expect("the golden file parses");
g.households
}
#[derive(Debug, Deserialize)]
pub struct Goldens {
pub households: Vec<GoldenHousehold>,
}
#[derive(Debug, Deserialize)]
pub struct GoldenHousehold {
pub name: String,
pub why: String,
pub inputs: GoldenInputs,
pub expected_ots: ExpectedOts,
pub expected_taxcalc: ExpectedTaxcalc,
}
#[derive(Debug, Deserialize)]
pub struct ExpectedOts {
pub adjusted_gross_income: f64,
pub taxable_income: f64,
pub qbi_deduction: f64,
pub income_tax_before_credits: f64,
pub se_tax: f64,
pub niit: f64,
pub additional_medicare_tax: f64,
pub total_tax: f64,
#[serde(default)]
pub deduction_taken: Option<f64>, #[serde(default)]
pub salt_capped: Option<f64>, #[serde(default)]
pub sch_d_to_l7: Option<f64>, #[serde(default)]
pub qbi_cap_l12: Option<f64>, #[serde(default)]
pub qual_div_l3a: Option<f64>, #[serde(default)]
pub net_ltcg_qd_exclusive: Option<f64>, #[serde(default)]
pub se_l10_oasdi: Option<f64>, #[serde(default)]
pub se_l11_medicare: Option<f64>, #[serde(default)]
pub f8959_l7: Option<f64>, #[serde(default)]
pub f8959_l13: Option<f64>, }
#[derive(Debug, Deserialize)]
pub struct ExpectedTaxcalc {
pub adjusted_gross_income: f64,
pub taxable_income: f64,
pub qbi_deduction: f64,
pub income_tax_before_credits: f64,
pub se_tax: f64,
pub niit: f64,
pub additional_medicare_tax: f64,
#[serde(default)]
pub deduction_taken: Option<f64>, #[serde(default)]
pub salt_capped: Option<f64>, #[serde(default)]
pub sch_d_to_l7: Option<f64>, #[serde(default)]
pub qbi_cap_l12: Option<f64>, #[serde(default)]
pub qual_div_l3a: Option<f64>, #[serde(default)]
pub net_ltcg_qd_exclusive: Option<f64>, #[serde(default)]
pub total_tax: Option<f64>, }
#[derive(Debug, Deserialize)]
pub struct GoldenInputs {
pub filing_status: String,
#[serde(default)]
pub w2_income: f64,
#[serde(default)]
pub taxable_interest: f64,
#[serde(default)]
pub qualified_dividends: f64,
#[serde(default)]
pub ordinary_dividends: f64,
#[serde(default)]
pub short_term_capital_gains: f64,
#[serde(default)]
pub long_term_capital_gains: f64,
#[serde(default)]
pub self_employment_income: f64,
#[serde(default)]
pub itemized_deductions: f64,
#[serde(default)]
pub state_income_tax: f64,
#[serde(default)]
pub real_estate_tax: f64,
#[serde(default)]
pub mortgage_interest: f64,
}
pub fn golden_usd(v: f64) -> Usd {
Usd::try_from(v).expect("the oracle emits finite figures")
}
pub fn not_a_dependent() -> HouseholdHeader {
HouseholdHeader {
can_be_claimed_as_dependent_taxpayer: Some(false),
..Default::default()
}
}
pub fn build_golden_household(h: &GoldenHousehold) -> (ReturnInputs, LedgerState) {
build_golden_return(&h.inputs)
}
pub fn build_golden_return(i: &GoldenInputs) -> (ReturnInputs, LedgerState) {
let status = match i.filing_status.as_str() {
"Single" => FilingStatus::Single,
"Married/Joint" => FilingStatus::Mfj,
other => panic!("unmapped filing status {other:?}"),
};
let mut ri = ReturnInputs {
filing_status: status,
..Default::default()
};
ri.header.taxpayer = crate::tax::return_inputs::Person {
first_name: "Golden".into(),
last_name: "Household".into(),
ssn: "123456789".into(),
..Default::default()
};
ri.header.can_be_claimed_as_dependent_taxpayer = Some(false);
if status == FilingStatus::Mfj {
ri.header.spouse = Some(crate::tax::return_inputs::Person {
first_name: "Golden".into(),
last_name: "Spouse".into(),
ssn: "987654321".into(),
..Default::default()
});
ri.header.can_be_claimed_as_dependent_spouse = Some(false);
}
if i.w2_income > 0.0 {
let w = golden_usd(i.w2_income);
ri.w2s.push(W2 {
owner: Owner::Taxpayer,
employer: "ORACLE CO".into(),
box1_wages: w,
box3_ss_wages: w, box5_medicare_wages: w, ..Default::default()
});
}
if i.taxable_interest > 0.0 {
ri.int_1099.push(Form1099Int {
payer: "ORACLE BANK".into(),
box1_interest: golden_usd(i.taxable_interest),
..Default::default()
});
}
if i.ordinary_dividends > 0.0 || i.qualified_dividends > 0.0 {
ri.div_1099.push(Form1099Div {
payer: "ORACLE BROKER".into(),
box1a_ordinary: golden_usd(i.ordinary_dividends), box1b_qualified: golden_usd(i.qualified_dividends),
..Default::default()
});
}
if i.itemized_deductions > 0.0
|| i.state_income_tax > 0.0
|| i.real_estate_tax > 0.0
|| i.mortgage_interest > 0.0
{
ri.schedule_a = Some(ScheduleAInputs {
salt_state_estimated_payments: golden_usd(i.state_income_tax),
salt_real_estate: golden_usd(i.real_estate_tax),
mortgage_interest_1098: golden_usd(i.itemized_deductions + i.mortgage_interest),
..Default::default()
});
}
if i.self_employment_income > 0.0 {
ri.schedule_c = Some(ScheduleCInputs {
owner: Owner::Taxpayer,
business_description: "Bitcoin mining".into(),
..Default::default()
});
}
ri.foreign_accounts = Some(false);
ri.foreign_trust = Some(false);
answer_all_live_declarations(&mut ri);
let mut state = LedgerState::default();
let mut leg = |gain: f64, term: Term, ev: u64| {
let (proceeds, basis) = if gain >= 0.0 {
(golden_usd(gain), Usd::ZERO)
} else {
(Usd::ZERO, golden_usd(-gain))
};
state.disposals.push(Disposal {
event: EventId::decision(ev),
kind: DisposeKind::Sell,
disposed_at: date!(2024 - 05 - 01),
legs: vec![DisposalLeg {
lot_id: LotId {
origin_event_id: EventId::decision(ev + 100),
split_sequence: 0,
},
sat: 100_000_000,
proceeds,
basis,
gain: proceeds - basis,
term,
basis_source: BasisSource::ExchangeProvided,
gift_zone: None,
acquired_at: if term == Term::LongTerm {
date!(2020 - 01 - 01)
} else {
date!(2024 - 01 - 02)
},
wallet: WalletId::SelfCustody {
label: "cold".into(),
},
pseudo: false,
}],
fee_mini_disposition: false,
});
};
if i.short_term_capital_gains != 0.0 {
leg(i.short_term_capital_gains, Term::ShortTerm, 1);
}
if i.long_term_capital_gains != 0.0 {
leg(i.long_term_capital_gains, Term::LongTerm, 2);
}
if i.self_employment_income > 0.0 {
state.income_recognized.push(IncomeRecord {
event: EventId::decision(3),
recognized_at: date!(2024 - 06 - 01),
sat: 100_000_000,
usd_fmv: golden_usd(i.self_employment_income),
kind: IncomeKind::Mining,
business: true, pseudo: false,
});
}
(ri, state)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn goldens_carry_the_deeper_and_provenance_leaves() {
let hs = golden_households(); let floor = &hs[0]; assert!(floor.expected_ots.net_ltcg_qd_exclusive.is_some());
assert!(floor.expected_ots.qual_div_l3a.is_some());
assert!(floor.expected_ots.deduction_taken.is_some());
assert!(floor.expected_taxcalc.total_tax.is_some());
assert!(floor.expected_taxcalc.net_ltcg_qd_exclusive.is_some());
let se = hs
.iter()
.find(|h| h.name == "single_crypto_business_se")
.expect("the SE anchor is in the matrix");
assert!(se.expected_ots.qbi_cap_l12.is_some());
assert!(se.expected_ots.se_l10_oasdi.is_some());
assert!(se.expected_ots.se_l11_medicare.is_some());
let minimal: ExpectedOts = serde_json::from_str(
r#"{"adjusted_gross_income":0.0,"taxable_income":0.0,"qbi_deduction":0.0,
"income_tax_before_credits":0.0,"se_tax":0.0,"niit":0.0,
"additional_medicare_tax":0.0,"total_tax":0.0}"#,
)
.expect("a record without the optional deeper leaves still parses");
assert!(minimal.net_ltcg_qd_exclusive.is_none());
assert!(minimal.qbi_cap_l12.is_none());
assert!(minimal.se_l10_oasdi.is_none());
}
}