use crate::{return_inputs, tax_profile, CliError};
use btctax_core::state::LedgerState;
use btctax_core::tax::derive_tax_profile;
use btctax_core::tax::return_1040::screen_compute_dependent;
use btctax_core::tax::return_inputs::ReturnInputs;
use btctax_core::tax::return_refuse::{screen_inputs, Refusal};
use btctax_core::tax::tables::FullReturnParams;
use btctax_core::{Carryforward, FilingStatus, TaxProfile, TaxTable, Usd};
use rusqlite::Connection;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Provenance {
ReturnInputs,
StoredProfile,
PseudoPlaceholder,
Missing,
}
#[derive(Debug, Clone)]
pub struct Resolved {
pub profile: Option<TaxProfile>,
pub provenance: Provenance,
pub refusal: Option<Refusal>,
}
impl Resolved {
pub fn is_return_inputs_uncomputable(&self) -> bool {
self.provenance == Provenance::ReturnInputs && self.profile.is_none()
}
}
pub fn placeholder_tax_profile() -> TaxProfile {
TaxProfile {
filing_status: FilingStatus::Single,
ordinary_taxable_income: Usd::ZERO,
magi_excluding_crypto: Usd::ZERO,
qualified_dividends_and_other_pref_income: Usd::ZERO,
other_net_capital_gain: Usd::ZERO,
capital_loss_carryforward_in: Carryforward::default(),
w2_ss_wages: Usd::ZERO,
w2_medicare_wages: Usd::ZERO,
schedule_c_expenses: Usd::ZERO,
}
}
fn resolve_core(
conn: &Connection,
year: i32,
pseudo_reconcile: bool,
full_return: Option<&FullReturnParams>,
tax_table: Option<&TaxTable>,
) -> Result<(Resolved, Option<ReturnInputs>), CliError> {
if let Some(ri) = return_inputs::get(conn, year)? {
let (Some(params), Some(table)) = (full_return, tax_table) else {
let r = Resolved {
profile: None,
provenance: Provenance::ReturnInputs,
refusal: None,
};
return Ok((r, Some(ri)));
};
if let Some(refusal) = screen_inputs(&ri, table, params) {
let r = Resolved {
profile: None,
provenance: Provenance::ReturnInputs,
refusal: Some(refusal),
};
return Ok((r, Some(ri)));
}
let r = Resolved {
profile: Some(derive_tax_profile(&ri, params, year)),
provenance: Provenance::ReturnInputs,
refusal: None,
};
return Ok((r, Some(ri)));
}
if let Some(p) = tax_profile::get(conn, year)? {
let r = Resolved {
profile: Some(p),
provenance: Provenance::StoredProfile,
refusal: None,
};
return Ok((r, None));
}
if pseudo_reconcile {
let r = Resolved {
profile: Some(placeholder_tax_profile()),
provenance: Provenance::PseudoPlaceholder,
refusal: None,
};
return Ok((r, None));
}
let r = Resolved {
profile: None,
provenance: Provenance::Missing,
refusal: None,
};
Ok((r, None))
}
pub fn resolve_profile(
conn: &Connection,
year: i32,
pseudo_reconcile: bool,
full_return: Option<&FullReturnParams>,
tax_table: Option<&TaxTable>,
) -> Result<Resolved, CliError> {
Ok(resolve_core(conn, year, pseudo_reconcile, full_return, tax_table)?.0)
}
pub enum ProfileOutcome {
Ready {
profile: Option<TaxProfile>,
provenance: Provenance,
},
Uncomputable { detail: String },
}
pub fn resolve_and_screen(
conn: &Connection,
state: &LedgerState,
year: i32,
pseudo_reconcile: bool,
full_return: Option<&FullReturnParams>,
tax_table: Option<&TaxTable>,
) -> Result<ProfileOutcome, CliError> {
let (resolved, ri) = resolve_core(conn, year, pseudo_reconcile, full_return, tax_table)?;
if resolved.is_return_inputs_uncomputable() {
return Ok(ProfileOutcome::Uncomputable {
detail: uncomputable_detail(year, resolved.refusal.as_ref()),
});
}
if resolved.provenance == Provenance::ReturnInputs {
let (Some(ri), Some(params)) = (ri.as_ref(), full_return) else {
return Ok(ProfileOutcome::Uncomputable {
detail: uncomputable_detail(year, None),
});
};
if let Some(refusal) = screen_compute_dependent(ri, state, year, params) {
return Ok(ProfileOutcome::Uncomputable {
detail: uncomputable_detail(year, Some(&refusal)),
});
}
}
Ok(ProfileOutcome::Ready {
profile: resolved.profile,
provenance: resolved.provenance,
})
}
fn uncomputable_detail(year: i32, refusal: Option<&Refusal>) -> String {
match refusal {
Some(r) => format!(
"tax year {year} cannot be computed from its full-return inputs: {}; run \
`income clear --year {year}` to remove them and use a raw `tax-profile`",
r.detail
),
None => format!(
"tax year {year} has full-return inputs, but full-return computation is not supported for \
{year} in this version (v1 supports TY2024); run `income clear --year {year}` to remove \
them and use a raw `tax-profile`"
),
}
}
#[cfg(test)]
mod tests {
use super::*;
use btctax_adapters::{BundledFullReturnTables, BundledTaxTables};
use btctax_core::tax::return_inputs::{ReturnInputs, W2};
use btctax_core::tax::tables::FullReturnTables;
use btctax_core::TaxTables;
use rust_decimal_macros::dec;
fn mem() -> Connection {
let c = Connection::open_in_memory().unwrap();
tax_profile::init_table(&c).unwrap();
return_inputs::init_table(&c).unwrap();
c
}
fn prof() -> TaxProfile {
let mut p = placeholder_tax_profile();
p.filing_status = FilingStatus::Mfj;
p.ordinary_taxable_income = dec!(120000);
p
}
fn ty2024() -> (BundledFullReturnTables, BundledTaxTables) {
(BundledFullReturnTables::load(), BundledTaxTables::load())
}
fn resolve(
c: &Connection,
year: i32,
pseudo: bool,
fr: &BundledFullReturnTables,
tt: &BundledTaxTables,
) -> Resolved {
resolve_profile(
c,
year,
pseudo,
fr.full_return_for(year),
tt.table_for(year),
)
.unwrap()
}
#[test]
fn missing_when_nothing_stored_and_mode_off() {
let c = mem();
let (fr, tt) = ty2024();
let r = resolve(&c, 2024, false, &fr, &tt);
assert_eq!(r.provenance, Provenance::Missing);
assert!(r.profile.is_none());
}
#[test]
fn pseudo_placeholder_when_mode_on_and_nothing_stored() {
let c = mem();
let (fr, tt) = ty2024();
let r = resolve(&c, 2024, true, &fr, &tt);
assert_eq!(r.provenance, Provenance::PseudoPlaceholder);
assert_eq!(r.profile.unwrap(), placeholder_tax_profile());
}
#[test]
fn stored_profile_beats_pseudo() {
let c = mem();
let (fr, tt) = ty2024();
tax_profile::set(&c, 2024, &prof()).unwrap();
let r = resolve(&c, 2024, true, &fr, &tt); assert_eq!(r.provenance, Provenance::StoredProfile);
assert_eq!(r.profile.unwrap(), prof());
}
#[test]
fn return_inputs_beats_stored_profile_and_derives_a_profile() {
let c = mem();
let (fr, tt) = ty2024();
tax_profile::set(&c, 2024, &prof()).unwrap();
let mut ri = ReturnInputs {
filing_status: FilingStatus::Single,
header: btctax_core::tax::testonly::not_a_dependent(),
w2s: vec![W2 {
box1_wages: dec!(100000),
box5_medicare_wages: dec!(100000),
..Default::default()
}],
..Default::default()
};
btctax_core::tax::testonly::answer_all_live_declarations(&mut ri); return_inputs::set(&c, 2024, &ri).unwrap();
let r = resolve(&c, 2024, true, &fr, &tt);
assert_eq!(r.provenance, Provenance::ReturnInputs); assert!(!r.is_return_inputs_uncomputable());
let p = r.profile.unwrap();
assert_eq!(p.filing_status, FilingStatus::Single);
assert_eq!(p.magi_excluding_crypto, dec!(100000));
assert_ne!(p, prof());
}
#[test]
fn resolve_and_screen_gives_return_inputs_precedence_over_stored() {
let c = mem();
let (fr, tt) = ty2024();
tax_profile::set(&c, 2024, &prof()).unwrap(); let mut ri = ReturnInputs {
filing_status: FilingStatus::Single,
header: btctax_core::tax::testonly::not_a_dependent(),
w2s: vec![W2 {
box1_wages: dec!(100000),
box5_medicare_wages: dec!(100000),
..Default::default()
}],
..Default::default()
};
btctax_core::tax::testonly::answer_all_live_declarations(&mut ri); return_inputs::set(&c, 2024, &ri).unwrap();
let state = btctax_core::LedgerState::default();
match resolve_and_screen(
&c,
&state,
2024,
true,
fr.full_return_for(2024),
tt.table_for(2024),
)
.unwrap()
{
ProfileOutcome::Ready {
profile,
provenance,
} => {
assert_eq!(provenance, Provenance::ReturnInputs); let p = profile.unwrap();
assert_eq!(p.filing_status, FilingStatus::Single); assert_eq!(p.magi_excluding_crypto, dec!(100000));
assert_ne!(p, prof());
}
ProfileOutcome::Uncomputable { detail } => panic!("expected Ready, got: {detail}"),
}
}
#[test]
fn return_inputs_refused_by_guard_is_uncomputable_with_reason() {
let c = mem();
let (fr, tt) = ty2024();
let mut ri = ReturnInputs {
filing_status: FilingStatus::Single,
header: btctax_core::tax::testonly::not_a_dependent(),
..Default::default()
};
btctax_core::tax::testonly::answer_all_live_declarations(&mut ri);
ri.sch1.hsa_activity = Some(true);
return_inputs::set(&c, 2024, &ri).unwrap();
let r = resolve(&c, 2024, false, &fr, &tt);
assert_eq!(r.provenance, Provenance::ReturnInputs);
assert!(r.is_return_inputs_uncomputable());
assert!(r.profile.is_none());
assert!(r.refusal.is_some()); }
#[test]
fn return_inputs_for_unsupported_year_is_uncomputable_without_refusal() {
let c = mem();
let (fr, tt) = ty2024();
return_inputs::set(&c, 2025, &ReturnInputs::default()).unwrap();
let r = resolve(&c, 2025, true, &fr, &tt);
assert_eq!(r.provenance, Provenance::ReturnInputs);
assert!(r.is_return_inputs_uncomputable());
assert!(r.refusal.is_none()); }
}