use btctax_cli::cli::FormArg;
use btctax_cli::{cmd, CliError, Session, ATTEST_PHRASE};
use btctax_core::event::*;
use btctax_core::identity::*;
use btctax_store::Passphrase;
use rust_decimal_macros::dec;
use std::path::PathBuf;
use time::macros::{date, datetime, offset};
fn pp() -> Passphrase {
Passphrase::new("pw".into())
}
fn wallet() -> WalletId {
WalletId::Exchange {
provider: "cb".into(),
account: "m".into(),
}
}
fn ev(rf: &str, ts: time::OffsetDateTime, p: EventPayload) -> LedgerEvent {
LedgerEvent {
id: EventId::import(Source::Coinbase, SourceRef::new(rf)),
utc_timestamp: ts,
original_tz: offset!(+00:00),
wallet: Some(wallet()),
payload: p,
}
}
fn real_events() -> Vec<LedgerEvent> {
vec![
ev(
"buy-1",
datetime!(2025-01-05 12:00 UTC),
EventPayload::Acquire(Acquire {
sat: 1_000_000,
usd_cost: dec!(200),
fee_usd: dec!(0),
basis_source: BasisSource::ExchangeProvided,
}),
),
ev(
"sell-1",
datetime!(2025-06-15 12:00 UTC),
EventPayload::Dispose(Dispose {
sat: 1_000_000,
usd_proceeds: dec!(500),
fee_usd: dec!(0),
kind: DisposeKind::Sell,
}),
),
]
}
fn pseudo_events() -> Vec<LedgerEvent> {
vec![
ev(
"in-1",
datetime!(2025-03-01 12:00 UTC),
EventPayload::TransferIn(TransferIn {
sat: 1_000_000,
src_addr: None,
txid: None,
}),
),
ev(
"sell-1",
datetime!(2025-06-01 12:00 UTC),
EventPayload::Dispose(Dispose {
sat: 400_000,
usd_proceeds: dec!(500),
fee_usd: dec!(0),
kind: DisposeKind::Sell,
}),
),
]
}
fn make_vault(evs: &[LedgerEvent]) -> (tempfile::TempDir, PathBuf) {
let dir = tempfile::tempdir().unwrap();
let vault = dir.path().join("vault.pgp");
cmd::init::run(&vault, &pp(), &dir.path().join("k.asc")).unwrap();
let mut s = Session::open(&vault, &pp()).unwrap();
btctax_core::persistence::append_import_batch(s.conn(), evs).unwrap();
s.save().unwrap();
(dir, vault)
}
fn contains(hay: &[u8], needle: &[u8]) -> bool {
hay.windows(needle.len()).any(|w| w == needle)
}
#[test]
fn export_irs_pdf_out_collision_names_path() {
let (_dir, vault) = make_vault(&real_events());
let tmp = tempfile::tempdir().unwrap();
let out = tmp.path().join("collide");
std::fs::write(&out, b"i am a file, not a directory").unwrap();
let err = cmd::admin::export_irs_pdf(&vault, &pp(), &out, 2025, &[], None)
.expect_err("an --out that collides with a file must error");
let msg = err.to_string();
assert!(
msg.contains(&out.display().to_string()),
"names the --out path: {msg}"
);
}
#[test]
fn real_ledger_fills_clean_official_pdfs() {
let (_dir, vault) = make_vault(&real_events());
let out = tempfile::tempdir().unwrap();
let report = cmd::admin::export_irs_pdf(&vault, &pp(), out.path(), 2025, &[], None)
.expect("real ledger export must succeed");
assert!(!report.watermarked, "a real ledger fill is NOT watermarked");
let f8949 = std::fs::read(out.path().join("f8949.pdf")).unwrap();
let sd = std::fs::read(out.path().join("schedule_d.pdf")).unwrap();
assert!(f8949.starts_with(b"%PDF") && sd.starts_with(b"%PDF"));
assert!(
!contains(&f8949, b"ESTIMATE, NOT FOR FILING"),
"real fill must NOT carry the DRAFT watermark"
);
use btctax_forms::testonly::*;
let doc = load(&f8949).unwrap();
let idx = index(&collect_fields(&doc).unwrap());
assert_eq!(
checkbox_on(&doc, idx["topmostSubform[0].Page1[0].c1_1[5]"].id).as_deref(),
Some("6"),
"Box I checked for short-term BTC"
);
assert_eq!(
checkbox_on(&doc, idx["topmostSubform[0].Page1[0].c1_1[2]"].id),
None,
"Box C stays off"
);
}
#[test]
fn pseudo_fill_requires_attestation() {
let (_dir, vault) = make_vault(&pseudo_events());
cmd::reconcile::pseudo_set_mode(&vault, &pp(), true).unwrap();
let out = tempfile::tempdir().unwrap();
let err = cmd::admin::export_irs_pdf(&vault, &pp(), out.path(), 2025, &[], None).unwrap_err();
assert!(
matches!(err, CliError::AttestationRequired),
"pseudo-active export without attestation must be refused, got {err:?}"
);
assert!(
!out.path().join("f8949.pdf").exists(),
"a refused export writes no PDF"
);
let err =
cmd::admin::export_irs_pdf(&vault, &pp(), out.path(), 2025, &[], Some("nope")).unwrap_err();
assert!(matches!(err, CliError::AttestationFailed), "got {err:?}");
let report =
cmd::admin::export_irs_pdf(&vault, &pp(), out.path(), 2025, &[], Some(ATTEST_PHRASE))
.unwrap();
assert!(report.watermarked, "a pseudo fill must be watermarked");
let f8949 = std::fs::read(out.path().join("f8949.pdf")).unwrap();
assert!(
contains(&f8949, b"ESTIMATE, NOT FOR FILING"),
"the pseudo fill must carry the DRAFT watermark"
);
}
fn se_plus_disposal_events() -> Vec<LedgerEvent> {
let mut evs = real_events();
evs.push(ev(
"mine-1",
datetime!(2025-08-01 12:00 UTC),
EventPayload::Income(Income {
sat: 200_000_000,
usd_fmv: Some(dec!(100000)),
fmv_status: FmvStatus::ExchangeProvided,
kind: IncomeKind::Mining,
business: true,
}),
));
evs
}
#[test]
fn sp2_packet_writes_schedule_se_and_1040_capgains() {
let (_dir, vault) = make_vault(&se_plus_disposal_events());
cmd::tax::set_profile(
&vault,
&pp(),
2025,
btctax_core::TaxProfile {
filing_status: btctax_core::FilingStatus::Single,
ordinary_taxable_income: dec!(0),
magi_excluding_crypto: dec!(0),
qualified_dividends_and_other_pref_income: dec!(0),
other_net_capital_gain: dec!(0),
capital_loss_carryforward_in: Default::default(),
w2_ss_wages: dec!(0),
w2_medicare_wages: dec!(0),
schedule_c_expenses: dec!(0),
},
false,
)
.unwrap();
let out = tempfile::tempdir().unwrap();
let report = cmd::admin::export_irs_pdf(&vault, &pp(), out.path(), 2025, &[], None).unwrap();
assert!(
report.schedule_se_path.is_some(),
"SE written (business mining ≥ $400)"
);
assert!(
report.form_1040_path.is_some(),
"1040 written (reportable activity)"
);
assert!(report.form_1040_filled_7a, "7a filled (active gain)");
assert!(report.form_8283_path.is_none(), "no donations ⇒ no 8283");
assert!(!report.se_below_floor && report.se_addl_medicare.is_none());
use btctax_forms::testonly::*;
let se = std::fs::read(out.path().join("schedule_se.pdf")).unwrap();
let doc = load(&se).unwrap();
let idx = index(&collect_fields(&doc).unwrap());
assert_eq!(
text_value(&doc, idx["topmostSubform[0].Page1[0].f1_21[0]"].id).as_deref(),
Some("14129.55"),
"SE line 12 = ss + medicare"
);
let f1040 = std::fs::read(out.path().join("form_1040_capgains.pdf")).unwrap();
let doc = load(&f1040).unwrap();
let idx = index(&collect_fields(&doc).unwrap());
assert_eq!(
checkbox_on(&doc, idx["topmostSubform[0].Page1[0].c1_10[0]"].id).as_deref(),
Some("1"),
"Digital-Asset question = YES"
);
assert_eq!(
text_value(&doc, idx["topmostSubform[0].Page1[0].f1_70[0]"].id).as_deref(),
Some("300"),
"1040 line 7a = Schedule D line 16"
);
}
#[test]
fn sp2_forms_filter_selects_subset() {
let (_dir, vault) = make_vault(&real_events());
let out = tempfile::tempdir().unwrap();
let report =
cmd::admin::export_irs_pdf(&vault, &pp(), out.path(), 2025, &[FormArg::F8949], None)
.unwrap();
assert!(report.f8949_path.is_some());
assert!(report.schedule_d_path.is_none(), "Schedule D not selected");
assert!(report.form_1040_path.is_none(), "1040 not selected");
assert!(out.path().join("f8949.pdf").exists());
assert!(!out.path().join("schedule_d.pdf").exists());
}
#[test]
fn unsupported_year_is_refused() {
let (_dir, vault) = make_vault(&real_events());
let out = tempfile::tempdir().unwrap();
let err = cmd::admin::export_irs_pdf(&vault, &pp(), out.path(), 2023, &[], None).unwrap_err();
assert!(
matches!(
err,
CliError::FormFill(btctax_forms::FormsError::UnsupportedYear(2023))
),
"only 2017/2024/2025 are bundled, got {err:?}"
);
assert!(
!out.path().join("basis_methodology.txt").exists(),
"an unsupported year must leave no half-written packet: basis_methodology.txt was written"
);
assert!(
!out.path().join("form_8275.txt").exists(),
"an unsupported year must leave no half-written packet: form_8275.txt was written"
);
}
fn real_events_2024() -> Vec<LedgerEvent> {
vec![
ev(
"buy-1",
datetime!(2024-01-05 12:00 UTC),
EventPayload::Acquire(Acquire {
sat: 1_000_000,
usd_cost: dec!(200),
fee_usd: dec!(0),
basis_source: BasisSource::ExchangeProvided,
}),
),
ev(
"sell-1",
datetime!(2024-06-15 12:00 UTC),
EventPayload::Dispose(Dispose {
sat: 1_000_000,
usd_proceeds: dec!(500),
fee_usd: dec!(0),
kind: DisposeKind::Sell,
}),
),
]
}
#[test]
fn ty2024_real_ledger_fills_box_c_f_and_line7_and_da() {
let (_dir, vault) = make_vault(&real_events_2024());
let out = tempfile::tempdir().unwrap();
let report = cmd::admin::export_irs_pdf(&vault, &pp(), out.path(), 2024, &[], None)
.expect("2024 real-ledger export must succeed");
assert!(!report.watermarked);
use btctax_forms::testonly::*;
let f8949 = std::fs::read(out.path().join("f8949.pdf")).unwrap();
assert!(f8949.starts_with(b"%PDF"));
assert!(!contains(&f8949, b"ESTIMATE, NOT FOR FILING"));
let doc = load(&f8949).unwrap();
assert!(!pdf_has_xfa(&doc).unwrap(), "XFA must be dropped");
let idx = index(&collect_fields(&doc).unwrap());
assert_eq!(
checkbox_on(&doc, idx["topmostSubform[0].Page1[0].c1_1[2]"].id).as_deref(),
Some("3"),
"Box C checked for short-term BTC on the 2024 form"
);
let f1040 = std::fs::read(out.path().join("form_1040_capgains.pdf")).unwrap();
let doc = load(&f1040).unwrap();
let idx = index(&collect_fields(&doc).unwrap());
assert_eq!(
text_value(
&doc,
idx["topmostSubform[0].Page1[0].Line4a-11_ReadOrder[0].f1_52[0]"].id
)
.as_deref(),
Some("300"),
"1040 line 7 = Schedule D line 16"
);
assert_eq!(
checkbox_on(&doc, idx["topmostSubform[0].Page1[0].c1_5[0]"].id).as_deref(),
Some("1"),
"Digital-Asset question = YES (2024 c1_5, adjacency-selected)"
);
}
fn real_events_2017() -> Vec<LedgerEvent> {
vec![
ev(
"buy-1",
datetime!(2017-01-05 12:00 UTC),
EventPayload::Acquire(Acquire {
sat: 1_000_000,
usd_cost: dec!(200),
fee_usd: dec!(0),
basis_source: BasisSource::ExchangeProvided,
}),
),
ev(
"sell-1",
datetime!(2017-06-15 12:00 UTC),
EventPayload::Dispose(Dispose {
sat: 1_000_000,
usd_proceeds: dec!(500),
fee_usd: dec!(0),
kind: DisposeKind::Sell,
}),
),
]
}
#[test]
fn ty2017_real_ledger_fills_box_c_f_and_line13_no_da() {
let (_dir, vault) = make_vault(&real_events_2017());
let out = tempfile::tempdir().unwrap();
let report = cmd::admin::export_irs_pdf(&vault, &pp(), out.path(), 2017, &[], None)
.expect("2017 real-ledger export must succeed");
assert!(!report.watermarked);
use btctax_forms::testonly::*;
let f8949 = std::fs::read(out.path().join("f8949.pdf")).unwrap();
assert!(f8949.starts_with(b"%PDF"));
let doc = load(&f8949).unwrap();
assert!(!pdf_has_xfa(&doc).unwrap(), "XFA must be dropped");
let idx = index(&collect_fields(&doc).unwrap());
assert_eq!(
checkbox_on(&doc, idx["topmostSubform[0].Page1[0].c1_1[2]"].id).as_deref(),
Some("3"),
"Box C checked for short-term BTC on the 2017 form"
);
let f1040 = std::fs::read(out.path().join("form_1040_capgains.pdf")).unwrap();
let doc = load(&f1040).unwrap();
assert!(!pdf_has_xfa(&doc).unwrap());
let idx = index(&collect_fields(&doc).unwrap());
assert_eq!(
text_value(&doc, idx["topmostSubform[0].Page1[0].f1-_51[0]"].id).as_deref(),
Some("300"),
"1040 line 13 dollars = Schedule D line 16"
);
assert_eq!(
text_value(&doc, idx["topmostSubform[0].Page1[0].f1_52[0]"].id).as_deref(),
Some("00"),
"1040 line 13 cents"
);
assert!(report.form_1040_filled_7a, "line 13 filled (active gain)");
}
#[test]
fn export_dispatches_a_full_return_year_to_the_full_packet() {
use btctax_cli::{return_inputs, Session};
use btctax_core::tax::return_inputs::ReturnInputs;
use btctax_core::tax::types::FilingStatus;
let (_dir, vault) = make_vault(&real_events());
let out = tempfile::tempdir().unwrap();
{
let mut s = Session::open(&vault, &pp()).unwrap();
let mut ri = ReturnInputs {
filing_status: FilingStatus::Single,
header: btctax_core::tax::testonly::not_a_dependent(),
..Default::default()
};
ri.header.taxpayer = btctax_core::tax::return_inputs::Person {
first_name: "Pat".into(),
last_name: "Roe".into(),
ssn: "222-33-4444".into(),
..Default::default()
};
btctax_core::tax::testonly::answer_all_live_declarations(&mut ri);
return_inputs::set(s.conn(), 2024, &ri).unwrap();
s.save().unwrap();
}
let rep = cmd::admin::export_irs_pdf(&vault, &pp(), out.path(), 2024, &[], None)
.expect("a full-return year exports the full packet");
assert!(
out.path().join("00_f1040.pdf").exists(),
"the full packet writes sequence-prefixed files"
);
assert!(
out.path().join("manifest.txt").exists(),
"…and the filer's stapling order"
);
assert!(!rep.full_return_paths.is_empty());
assert!(
!out.path().join("form_1040_capgains.pdf").exists(),
"the slice's 1040 must never appear beside the full packet"
);
assert!(rep.form_1040_path.is_none());
}
#[test]
fn export_full_return_out_collision_names_path() {
use btctax_cli::{return_inputs, Session};
use btctax_core::tax::return_inputs::ReturnInputs;
use btctax_core::tax::types::FilingStatus;
let (_dir, vault) = make_vault(&real_events());
{
let mut s = Session::open(&vault, &pp()).unwrap();
let mut ri = ReturnInputs {
filing_status: FilingStatus::Single,
header: btctax_core::tax::testonly::not_a_dependent(),
..Default::default()
};
ri.header.taxpayer = btctax_core::tax::return_inputs::Person {
first_name: "Pat".into(),
last_name: "Roe".into(),
ssn: "222-33-4444".into(),
..Default::default()
};
btctax_core::tax::testonly::answer_all_live_declarations(&mut ri);
return_inputs::set(s.conn(), 2024, &ri).unwrap();
s.save().unwrap();
}
let tmp = tempfile::tempdir().unwrap();
let out = tmp.path().join("collide");
std::fs::write(&out, b"i am a file, not a directory").unwrap();
let err = cmd::admin::export_irs_pdf(&vault, &pp(), &out, 2024, &[], None)
.expect_err("a full-return --out that collides with a file must error");
let msg = err.to_string();
assert!(
msg.contains(&out.display().to_string()),
"names the --out path: {msg}"
);
}
#[test]
fn forms_slice_ignored_on_full_return_year_is_flagged_and_packet_unchanged() {
use btctax_cli::{return_inputs, Session};
use btctax_core::tax::return_inputs::ReturnInputs;
use btctax_core::tax::types::FilingStatus;
let (_dir, vault) = make_vault(&real_events());
{
let mut s = Session::open(&vault, &pp()).unwrap();
let mut ri = ReturnInputs {
filing_status: FilingStatus::Single,
header: btctax_core::tax::testonly::not_a_dependent(),
..Default::default()
};
ri.header.taxpayer = btctax_core::tax::return_inputs::Person {
first_name: "Pat".into(),
last_name: "Roe".into(),
ssn: "222-33-4444".into(),
..Default::default()
};
btctax_core::tax::testonly::answer_all_live_declarations(&mut ri);
return_inputs::set(s.conn(), 2024, &ri).unwrap();
s.save().unwrap();
}
let out = tempfile::tempdir().unwrap();
let rep = cmd::admin::export_irs_pdf(&vault, &pp(), out.path(), 2024, &[FormArg::F8949], None)
.expect("full-return export succeeds");
assert!(
rep.forms_ignored_full_return,
"a --forms slice on a full-return year is flagged as ignored"
);
assert!(
!rep.full_return_paths.is_empty(),
"the full packet still writes despite the ignored slice"
);
let out2 = tempfile::tempdir().unwrap();
let rep2 = cmd::admin::export_irs_pdf(&vault, &pp(), out2.path(), 2024, &[], None)
.expect("full-return export succeeds");
assert!(
!rep2.forms_ignored_full_return,
"no --forms → nothing was ignored"
);
assert_eq!(
rep.full_return_paths.len(),
rep2.full_return_paths.len(),
"the packet path count is unchanged regardless of --forms (the slice is inert)"
);
}
#[test]
fn forms_slice_on_full_return_year_warns_on_stderr_and_packet_is_identical() {
use btctax_cli::{return_inputs, Session};
use btctax_core::tax::return_inputs::ReturnInputs;
use btctax_core::tax::types::FilingStatus;
let (_dir, vault) = make_vault(&real_events());
{
let mut s = Session::open(&vault, &pp()).unwrap();
let mut ri = ReturnInputs {
filing_status: FilingStatus::Single,
header: btctax_core::tax::testonly::not_a_dependent(),
..Default::default()
};
ri.header.taxpayer = btctax_core::tax::return_inputs::Person {
first_name: "Pat".into(),
last_name: "Roe".into(),
ssn: "222-33-4444".into(),
..Default::default()
};
btctax_core::tax::testonly::answer_all_live_declarations(&mut ri);
return_inputs::set(s.conn(), 2024, &ri).unwrap();
s.save().unwrap();
}
let bin = env!("CARGO_BIN_EXE_btctax");
let run = |out: &std::path::Path, with_forms: bool| -> (String, Vec<String>) {
let mut args: Vec<String> = vec![
"--vault".into(),
vault.to_str().unwrap().into(),
"export-irs-pdf".into(),
"--out".into(),
out.to_str().unwrap().into(),
"--tax-year".into(),
"2024".into(),
];
if with_forms {
args.push("--forms".into());
args.push("f8949".into());
}
let o = std::process::Command::new(bin)
.args(&args)
.env("BTCTAX_PASSPHRASE", "pw")
.output()
.expect("btctax runs");
assert!(
o.status.success(),
"export must succeed: {}",
String::from_utf8_lossy(&o.stderr)
);
let stderr = String::from_utf8_lossy(&o.stderr).into_owned();
let mut files: Vec<String> = std::fs::read_dir(out)
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.collect();
files.sort();
(stderr, files)
};
let tmp = tempfile::tempdir().unwrap();
let (stderr_forms, files_forms) = run(&tmp.path().join("with"), true);
let (stderr_plain, files_plain) = run(&tmp.path().join("without"), false);
assert!(
stderr_forms.contains("--forms is ignored on a full-return year"),
"the warning is emitted with --forms:\n{stderr_forms}"
);
assert!(
!stderr_plain.contains("--forms is ignored"),
"no warning without --forms:\n{stderr_plain}"
);
assert_eq!(
files_forms, files_plain,
"the written packet file-set is byte-for-byte identical regardless of --forms"
);
}
#[test]
fn export_without_return_inputs_still_gets_the_crypto_slice() {
let (_dir, vault) = make_vault(&real_events());
let out = tempfile::tempdir().unwrap();
let rep = cmd::admin::export_irs_pdf(&vault, &pp(), out.path(), 2025, &[], None)
.expect("a crypto-only year exports the slice");
assert!(
rep.full_return_paths.is_empty(),
"no full packet on this path"
);
assert!(
!out.path().join("00_f1040.pdf").exists(),
"the full packet's 1040 must never appear on the slice path"
);
assert!(
rep.schedule_d_path.is_some() || rep.f8949_path.is_some(),
"the slice still produces its own forms"
);
}
#[test]
fn a_full_return_without_an_ssn_refuses_and_writes_no_bytes() {
use btctax_cli::{return_inputs, Session};
use btctax_core::tax::return_inputs::ReturnInputs;
use btctax_core::tax::types::FilingStatus;
let (_dir, vault) = make_vault(&real_events());
let out = tempfile::tempdir().unwrap();
{
let mut s = Session::open(&vault, &pp()).unwrap();
return_inputs::set(
s.conn(),
2024,
&btctax_core::tax::testonly::answered(ReturnInputs {
filing_status: FilingStatus::Single,
header: btctax_core::tax::testonly::not_a_dependent(), ..Default::default()
}),
)
.unwrap();
s.save().unwrap();
}
let err = cmd::admin::export_irs_pdf(&vault, &pp(), out.path(), 2024, &[], None)
.expect_err("an unnamed return must not produce a filable packet");
assert!(
format!("{err}").contains("no SSN"),
"the refusal says what is missing: {err}"
);
assert!(
std::fs::read_dir(out.path())
.map(|mut d| d.next().is_none())
.unwrap_or(true),
"a refused export leaves out_dir EMPTY — never a half-written packet"
);
}
#[test]
fn the_two_pipelines_cannot_overwrite_each_others_files() {
use btctax_cli::{return_inputs, Session};
use btctax_core::tax::return_inputs::ReturnInputs;
use btctax_core::tax::types::FilingStatus;
use std::collections::BTreeMap;
let (_dir, vault) = make_vault(&real_events_2024());
let out = tempfile::tempdir().unwrap();
{
let mut s = Session::open(&vault, &pp()).unwrap();
let mut ri = ReturnInputs {
filing_status: FilingStatus::Single,
header: btctax_core::tax::testonly::not_a_dependent(),
..Default::default()
};
ri.header.taxpayer = btctax_core::tax::return_inputs::Person {
first_name: "Pat".into(),
last_name: "Roe".into(),
ssn: "222-33-4444".into(),
..Default::default()
};
btctax_core::tax::testonly::answer_all_live_declarations(&mut ri);
return_inputs::set(s.conn(), 2024, &ri).unwrap();
s.save().unwrap();
}
cmd::admin::export_irs_pdf(&vault, &pp(), out.path(), 2024, &[], None).unwrap();
let snapshot: BTreeMap<String, Vec<u8>> = std::fs::read_dir(out.path())
.unwrap()
.map(|e| {
let e = e.unwrap();
(
e.file_name().to_string_lossy().into_owned(),
std::fs::read(e.path()).unwrap(),
)
})
.collect();
assert!(
snapshot.len() > 1,
"the packet wrote several files: {:?}",
snapshot.keys().collect::<Vec<_>>()
);
assert!(
snapshot.keys().any(|k| k.contains("schedule_d")),
"the packet must contain the colliding forms, or this test proves nothing: {:?}",
snapshot.keys().collect::<Vec<_>>()
);
cmd::admin::export_irs_pdf(&vault, &pp(), out.path(), 2017, &[], None).unwrap();
for (name, bytes) in &snapshot {
let now = std::fs::read(out.path().join(name))
.unwrap_or_else(|_| panic!("the slice DELETED the packet's {name}"));
assert_eq!(
&now, bytes,
"★ the slice OVERWROTE the packet's {name} — a cents form inside a whole-dollar return"
);
}
}
#[test]
fn export_irs_pdf_writes_basis_methodology_when_a_tranche_is_filed() {
let dir = tempfile::tempdir().unwrap();
let vault = dir.path().join("vault.pgp");
cmd::init::run(&vault, &pp(), &dir.path().join("k.asc")).unwrap();
cmd::tranche::declare_tranche(
&vault,
&pp(),
1_000_000,
wallet(),
date!(2025 - 01 - 01),
date!(2025 - 01 - 31),
datetime!(2026-01-01 0:00 UTC),
)
.unwrap();
let sell = vec![ev(
"sell-t",
datetime!(2025-06-15 12:00 UTC),
EventPayload::Dispose(Dispose {
sat: 1_000_000,
usd_proceeds: dec!(500),
fee_usd: dec!(0),
kind: DisposeKind::Sell,
}),
)];
let mut s = Session::open(&vault, &pp()).unwrap();
btctax_core::persistence::append_import_batch(s.conn(), &sell).unwrap();
s.save().unwrap();
drop(s);
let out = tempfile::tempdir().unwrap();
cmd::admin::export_irs_pdf(&vault, &pp(), out.path(), 2025, &[], None).unwrap();
let disclosure = out.path().join("basis_methodology.txt");
assert!(
disclosure.exists(),
"the PDF packet must write the mandatory basis_methodology.txt (I-3 / D-4)"
);
assert!(
std::fs::read_to_string(&disclosure)
.unwrap()
.contains("Basis methodology disclosure"),
"the disclosure content is present"
);
}