use crate::cli::FormArg;
use crate::config::{set_fee_treatment, set_pre2025_method as config_set_pre2025_method};
use crate::render::write_csv_exports;
use crate::{require_attestation, CliConfig, CliError, Session};
use btctax_adapters::BundledTaxTables;
use btctax_core::{
compute_se_tax, se_net_income, FeeTreatment, LotMethod, ScheduleDPart, Severity, TaxTables, Usd,
};
use btctax_forms::Form1040Inputs;
use btctax_store::{fsperms, Passphrase};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct ExportReport {
pub path: PathBuf,
pub unresolved_hard: usize,
}
pub fn show_config(vault_path: &Path, pp: &Passphrase) -> Result<CliConfig, CliError> {
Session::open(vault_path, pp)?.config()
}
pub fn set_config(
vault_path: &Path,
pp: &Passphrase,
fee_treatment: Option<FeeTreatment>,
) -> Result<CliConfig, CliError> {
let mut session = Session::open(vault_path, pp)?;
if let Some(t) = fee_treatment {
set_fee_treatment(session.conn(), t)?;
session.save()?;
}
session.config()
}
pub fn set_pre2025_method(
vault_path: &Path,
pp: &Passphrase,
m: LotMethod,
attested: bool,
) -> Result<CliConfig, CliError> {
let mut session = Session::open(vault_path, pp)?;
config_set_pre2025_method(session.conn(), m, attested)?;
session.save()?;
session.config()
}
pub fn export_snapshot(
vault_path: &Path,
pp: &Passphrase,
out_dir: &Path,
tax_year: Option<i32>,
attest: Option<&str>,
) -> Result<ExportReport, CliError> {
let session = Session::open(vault_path, pp)?;
let (state, _cfg) = session.project()?;
if state.pseudo_active() {
require_attestation(attest)?;
}
let sqlite = session.vault().export_snapshot(out_dir)?; let se_result = match tax_year {
Some(y) => {
let tables = BundledTaxTables::load();
session.tax_profile(y)?.and_then(|p| {
tables.table_for(y).and_then(|t| {
compute_se_tax(
&state,
y,
p.filing_status,
t,
p.w2_ss_wages,
p.w2_medicare_wages,
p.schedule_c_expenses,
)
})
})
}
None => None,
};
let donation_details = session.donation_details()?;
write_csv_exports(
out_dir,
&state,
tax_year,
se_result.as_ref(),
&donation_details,
)?;
let unresolved_hard = state
.blockers
.iter()
.filter(|b| b.kind.severity() == Severity::Hard)
.count();
Ok(ExportReport {
path: sqlite,
unresolved_hard,
})
}
pub fn export_pseudo_active(vault_path: &Path, pp: &Passphrase) -> Result<bool, CliError> {
let (state, _cfg) = Session::open(vault_path, pp)?.project()?;
Ok(state.pseudo_active())
}
#[derive(Debug, Clone)]
pub struct IrsPdfReport {
pub f8949_path: Option<PathBuf>,
pub schedule_d_path: Option<PathBuf>,
pub tax_year: i32,
pub unresolved_hard: usize,
pub broker_reported_rows: usize,
pub watermarked: bool,
pub schedule_se_path: Option<PathBuf>,
pub se_below_floor: bool,
pub se_addl_medicare: Option<Usd>,
pub se_income_without_profile: bool,
pub form_8283_path: Option<PathBuf>,
pub form_8283_needs_review: bool,
pub form_8283_section_b: Option<bool>,
pub form_1040_path: Option<PathBuf>,
pub form_1040_filled_7a: bool,
pub form_1040_loss: bool,
}
fn wants(selected: &[FormArg], f: FormArg) -> bool {
selected.is_empty() || selected.contains(&f)
}
fn sd_part_active(p: &ScheduleDPart) -> bool {
!p.proceeds.is_zero() || !p.cost_basis.is_zero() || !p.gain.is_zero()
}
pub fn export_irs_pdf(
vault_path: &Path,
pp: &Passphrase,
out_dir: &Path,
tax_year: i32,
forms: &[FormArg],
attest: Option<&str>,
) -> Result<IrsPdfReport, CliError> {
let session = Session::open(vault_path, pp)?;
let (state, _cfg) = session.project()?;
let watermarked = state.pseudo_active();
if watermarked {
require_attestation(attest)?;
}
let rows = btctax_core::form_8949(&state, tax_year);
let totals = btctax_core::schedule_d(&state, tax_year);
let stamp = |bytes: Vec<u8>| -> Result<Vec<u8>, CliError> {
Ok(if watermarked {
btctax_forms::stamp_draft_watermark(&bytes)?
} else {
bytes
})
};
fsperms::mkdir_owner_only(out_dir)?;
let f8949_path = if wants(forms, FormArg::F8949) {
let bytes = stamp(btctax_forms::fill_form_8949(&rows, tax_year)?)?;
let path = out_dir.join("f8949.pdf");
write_bytes_owner_only(&path, &bytes)?;
Some(path)
} else {
None
};
let schedule_d_path = if wants(forms, FormArg::ScheduleD) {
let bytes = stamp(btctax_forms::fill_schedule_d(&totals, tax_year)?)?;
let path = out_dir.join("schedule_d.pdf");
write_bytes_owner_only(&path, &bytes)?;
Some(path)
} else {
None
};
let se_computed = {
let tables = BundledTaxTables::load();
session.tax_profile(tax_year)?.and_then(|p| {
tables.table_for(tax_year).and_then(|t| {
compute_se_tax(
&state,
tax_year,
p.filing_status,
t,
p.w2_ss_wages,
p.w2_medicare_wages,
p.schedule_c_expenses,
)
.map(|se| (se, p.w2_ss_wages, t.ss_wage_base))
})
})
};
let se_income_without_profile =
se_computed.is_none() && !se_net_income(&state, tax_year).is_zero();
let mut schedule_se_path = None;
let mut se_below_floor = false;
let mut se_addl_medicare = None;
if wants(forms, FormArg::ScheduleSe) {
if let Some((se, w2_ss_wages, ss_wage_base)) = se_computed {
if !se.addl.is_zero() {
se_addl_medicare = Some(se.addl);
}
match btctax_forms::fill_schedule_se(&se, w2_ss_wages, ss_wage_base, tax_year)? {
Some(bytes) => {
let bytes = stamp(bytes)?;
let path = out_dir.join("schedule_se.pdf");
write_bytes_owner_only(&path, &bytes)?;
schedule_se_path = Some(path);
}
None => se_below_floor = true, }
}
}
let mut form_8283_path = None;
let mut form_8283_needs_review = false;
let mut form_8283_section_b = None;
if wants(forms, FormArg::Form8283) {
let details = session.donation_details()?;
let rows_8283 = btctax_core::form_8283(&state, tax_year, &details);
if let Some(bytes) = btctax_forms::fill_form_8283(&rows_8283, tax_year)? {
form_8283_needs_review = rows_8283.iter().any(|r| r.needs_review);
form_8283_section_b = rows_8283
.iter()
.find_map(|r| r.section)
.map(|s| s == btctax_core::Form8283Section::B);
let bytes = stamp(bytes)?;
let path = out_dir.join("form_8283.pdf");
write_bytes_owner_only(&path, &bytes)?;
form_8283_path = Some(path);
}
}
let mut form_1040_path = None;
let mut form_1040_filled_7a = false;
let mut form_1040_loss = false;
if wants(forms, FormArg::Form1040) {
let da_yes = !rows.is_empty()
|| state
.income_recognized
.iter()
.any(|i| i.recognized_at.year() == tax_year)
|| state
.removals
.iter()
.any(|r| r.removed_at.year() == tax_year);
let inputs = Form1040Inputs {
da_yes,
schedule_d_active: sd_part_active(&totals.st) || sd_part_active(&totals.lt),
schedule_d_line16: totals.st.gain + totals.lt.gain,
};
if let Some(fill) = btctax_forms::fill_form_1040_capgains(&inputs, tax_year)? {
form_1040_filled_7a = fill.filled_7a;
form_1040_loss = fill.loss;
let bytes = stamp(fill.pdf)?;
let path = out_dir.join("form_1040_capgains.pdf");
write_bytes_owner_only(&path, &bytes)?;
form_1040_path = Some(path);
}
}
let unresolved_hard = state
.blockers
.iter()
.filter(|b| b.kind.severity() == Severity::Hard)
.count();
Ok(IrsPdfReport {
f8949_path,
schedule_d_path,
tax_year,
unresolved_hard,
broker_reported_rows: btctax_forms::rows_possibly_broker_reported(&rows),
watermarked,
schedule_se_path,
se_below_floor,
se_addl_medicare,
se_income_without_profile,
form_8283_path,
form_8283_needs_review,
form_8283_section_b,
form_1040_path,
form_1040_filled_7a,
form_1040_loss,
})
}
fn write_bytes_owner_only(path: &Path, bytes: &[u8]) -> Result<(), CliError> {
use std::io::Write;
let mut f = fsperms::open_owner_only(path)?;
f.write_all(bytes)?;
f.flush()?;
Ok(())
}
pub fn backup_key(vault_path: &Path, pp: &Passphrase, out_path: &Path) -> Result<(), CliError> {
Session::open(vault_path, pp)?
.vault()
.backup_key(out_path)?;
Ok(())
}