use btctax_cli::cli::FormArg;
use btctax_cli::cmd::promote::ProvenanceKind;
use btctax_cli::eventref::parse_event_id;
use btctax_cli::{cmd, return_inputs, CliError, Session, PROMOTE_ACK_PHRASE};
use btctax_core::conservative::{flagged_years, Coverage};
use btctax_core::event::{
Acknowledgment, Acquire, BasisSource, ConsentTerm, DeclareTranche, Dispose, DisposeKind,
EventPayload, FloorMethod, OutflowClass, PromoteTranche, TransferOut,
};
use btctax_core::identity::{EventId, Source, SourceRef, WalletId};
use btctax_core::persistence::{append_decision, append_import_batch, load_all};
use btctax_core::LedgerEvent;
use btctax_store::Passphrase;
use rust_decimal_macros::dec;
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use time::macros::{date, datetime};
use time::UtcOffset;
fn pp() -> Passphrase {
Passphrase::new("pw".into())
}
fn now() -> time::OffsetDateTime {
datetime!(2026 - 01 - 01 0:00 UTC)
}
fn wallet() -> WalletId {
WalletId::Exchange {
provider: "coinbase".into(),
account: "main".into(),
}
}
fn imp(rf: &str, ts: time::OffsetDateTime, payload: EventPayload) -> LedgerEvent {
LedgerEvent {
id: EventId::import(Source::Coinbase, SourceRef::new(rf)),
utc_timestamp: ts,
original_tz: UtcOffset::UTC,
wallet: Some(wallet()),
payload,
}
}
fn build_promoted_vault(dir: &Path) -> (PathBuf, EventId) {
let vault = dir.join("vault.pgp");
let mut s = Session::create(&vault, &pp()).unwrap();
let buy = imp(
"BUY",
datetime!(2017-01-01 00:00 UTC),
EventPayload::Acquire(Acquire {
sat: 60_000_000,
usd_cost: dec!(3_000),
fee_usd: dec!(0),
basis_source: BasisSource::ExchangeProvided,
}),
);
let sell = imp(
"SELL",
datetime!(2018-09-01 00:00 UTC),
EventPayload::Dispose(Dispose {
sat: 40_000_000,
usd_proceeds: dec!(20_000),
fee_usd: dec!(0),
kind: DisposeKind::Sell,
}),
);
append_import_batch(s.conn(), &[buy, sell]).unwrap();
let tranche_id = append_decision(
s.conn(),
EventPayload::DeclareTranche(DeclareTranche {
sat: 40_000_000,
wallet: wallet(),
window_start: time::macros::date!(2018 - 01 - 01),
window_end: time::macros::date!(2018 - 03 - 31),
}),
now(),
UtcOffset::UTC,
None,
)
.unwrap();
let promote_id = append_decision(
s.conn(),
EventPayload::PromoteTranche(PromoteTranche {
target: tranche_id,
method: FloorMethod::WindowLowClose,
filed_basis: dec!(12_000),
coverage: Coverage::Full,
provenance_attested: true,
acknowledgment: Acknowledgment {
phrase: "I understand and accept the risk".into(),
shown_terms: vec![],
provenance_text: "acquired by purchase within the declared window".into(),
provenance_version: "v1".into(),
},
part_ii_narrative: "cash P2P purchase, no records; window bounded on-chain".into(),
}),
now(),
UtcOffset::UTC,
None,
)
.unwrap();
s.save().unwrap();
(vault, promote_id)
}
fn run_void(vault: &Path, target: &str) -> (i32, String, String) {
let bin = env!("CARGO_BIN_EXE_btctax");
let out = std::process::Command::new(bin)
.arg("--vault")
.arg(vault.to_str().unwrap())
.arg("reconcile")
.arg("void")
.arg(target)
.env("BTCTAX_PASSPHRASE", "pw")
.output()
.expect("btctax binary must execute");
(
out.status.code().expect("exits normally"),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
fn decision_count(vault: &Path) -> usize {
let s = Session::open(vault, &pp()).unwrap();
load_all(s.conn())
.unwrap()
.iter()
.filter(|e| matches!(e.id, EventId::Decision { .. }))
.count()
}
#[test]
fn voiding_a_promoted_tranche_prints_the_void_direction_advisory() {
let dir = tempfile::tempdir().unwrap();
let (vault, promote_id) = build_promoted_vault(dir.path());
let before = decision_count(&vault);
let (code, stdout, stderr) = run_void(&vault, &promote_id.canonical());
assert_eq!(code, 0, "the void must succeed; stderr: {stderr}");
assert!(
stdout.contains("2018"),
"the advisory names the affected filed year 2018: {stdout}"
);
assert!(
stdout.contains("1040-X"),
"the advisory names the Form 1040-X implication: {stdout}"
);
assert!(
stdout.to_lowercase().contains("additional tax"),
"voiding a promote over a filed floor-year is amend-to-PAY (additional tax): {stdout}"
);
assert_eq!(
decision_count(&vault),
before + 1,
"the void decision is still recorded after the warning"
);
}
#[test]
fn voiding_a_promoted_declare_tranche_is_refused_and_prints_no_amend_advisory() {
let dir = tempfile::tempdir().unwrap();
let (vault, _promote_id) = build_promoted_vault(dir.path());
let tranche_id = {
let s = Session::open(&vault, &pp()).unwrap();
let id = load_all(s.conn())
.unwrap()
.into_iter()
.find(|e| matches!(e.payload, EventPayload::DeclareTranche(_)))
.map(|e| e.id)
.expect("the promoted DeclareTranche is present");
id
};
let before = decision_count(&vault);
let (code, stdout, stderr) = run_void(&vault, &tranche_id.canonical());
assert_ne!(
code, 0,
"voiding a promote-held DeclareTranche must be refused; stdout={stdout} stderr={stderr}"
);
assert!(
stderr.contains("cannot record this decision")
&& stderr.contains("held in force by a live PromoteTranche"),
"the refusal names the inert-void reason: {stderr}"
);
assert!(
!stdout.to_lowercase().contains("additional tax") && !stdout.contains("1040-X"),
"an inert (refused) void must print NO amend-to-PAY promote-void advisory: {stdout}"
);
assert_eq!(
decision_count(&vault),
before,
"a refused void appends no decision"
);
}
#[test]
fn verify_surfaces_the_promote_drift_advisory_for_a_drifted_promote() {
let dir = tempfile::tempdir().unwrap();
let (vault, _promote_id) = build_promoted_vault(dir.path());
let report = cmd::inspect::verify(&vault, &pp()).unwrap();
assert!(
!report.drift.is_empty(),
"verify's VerifyReport.drift must be non-empty for a drifted promote (wired into build_verify): \
{:?}",
report.drift
);
assert!(
report
.drift
.iter()
.any(|l| l.contains("void") && l.contains("re-promote") && l.contains("not yet filed")),
"the stored floor is OVERSTATED (recomputes lower) → the conditional void+re-promote copy: {:?}",
report.drift
);
}
fn tranche_wallet() -> WalletId {
WalletId::SelfCustody {
label: "promote-t10".into(),
}
}
fn count<P: Fn(&EventPayload) -> bool>(vault: &Path, pred: P) -> usize {
let s = Session::open(vault, &pp()).unwrap();
load_all(s.conn())
.unwrap()
.iter()
.filter(|e| pred(&e.payload))
.count()
}
fn only_promote(vault: &Path) -> PromoteTranche {
let s = Session::open(vault, &pp()).unwrap();
load_all(s.conn())
.unwrap()
.into_iter()
.find_map(|e| match e.payload {
EventPayload::PromoteTranche(p) => Some(p),
_ => None,
})
.expect("exactly one PromoteTranche recorded")
}
fn vault_with_tranche(dir: &Path) -> (PathBuf, String) {
let vault = dir.join("vault.pgp");
cmd::init::run(&vault, &pp(), &dir.join("k.asc")).unwrap();
let id = cmd::tranche::declare_tranche(
&vault,
&pp(),
40_000_000,
tranche_wallet(),
date!(2020 - 01 - 01),
date!(2020 - 01 - 10),
now(),
)
.unwrap();
(vault, id.canonical())
}
fn vault_with_promoted_tranche(dir: &Path) -> (PathBuf, String) {
let (vault, target_ref) = vault_with_tranche(dir);
let target = parse_event_id(&target_ref).unwrap();
let mut s = Session::open(&vault, &pp()).unwrap();
append_decision(
s.conn(),
EventPayload::PromoteTranche(PromoteTranche {
target,
method: FloorMethod::WindowLowClose,
filed_basis: dec!(1_000),
coverage: Coverage::Full,
provenance_attested: true,
acknowledgment: Acknowledgment {
phrase: PROMOTE_ACK_PHRASE.into(),
shown_terms: vec![],
provenance_text: "acquired by purchase within the declared window".into(),
provenance_version: "v1".into(),
},
part_ii_narrative: "cash P2P purchase, no records; window bounded on-chain".into(),
}),
now(),
UtcOffset::UTC,
None,
)
.unwrap();
s.save().unwrap();
(vault, target_ref)
}
fn consent_terms_fixture() -> Vec<ConsentTerm> {
vec![ConsentTerm::ComputedTax {
year: 2020,
delta_usd: dec!(500),
deduction_delta_usd: None,
}]
}
fn consent_terms_with_deduction_and_unrealized() -> Vec<ConsentTerm> {
vec![
ConsentTerm::ComputedTax {
year: 2020,
delta_usd: dec!(0),
deduction_delta_usd: Some(dec!(300)),
},
ConsentTerm::Unrealized {
sat: 10_000_000,
hypothetical_reduction: Some(dec!(1_000)),
as_of: Some(date!(2020 - 06 - 01)),
},
]
}
#[test]
fn every_non_purchase_provenance_is_refused_fail_closed() {
let dir = tempfile::tempdir().unwrap();
let (vault, target) = vault_with_tranche(dir.path());
for pk in [
ProvenanceKind::Gift,
ProvenanceKind::Inheritance,
ProvenanceKind::Mining,
ProvenanceKind::Earned,
ProvenanceKind::Airdrop,
ProvenanceKind::Fork,
] {
let err =
cmd::promote::promote_tranche(&vault, &pp(), &target, pk, "facts".into(), None, now())
.unwrap_err();
assert!(
matches!(err, CliError::Usage(ref m) if m.contains("purchase") && m.contains("real acquisition")),
"{pk:?} must be refused naming 'purchase' + 'real acquisition': {err}"
);
}
assert_eq!(
count(&vault, |p| matches!(p, EventPayload::PromoteTranche(_))),
0,
"fail-closed: nothing recorded across the whole non-purchase sweep (BG-D5)"
);
}
#[test]
fn empty_part_ii_narrative_is_refused_at_record_time() {
let dir = tempfile::tempdir().unwrap();
let (vault, target) = vault_with_tranche(dir.path());
let err = cmd::promote::promote_tranche(
&vault,
&pp(),
&target,
ProvenanceKind::Purchase,
" ".into(),
Some(PROMOTE_ACK_PHRASE),
now(),
)
.unwrap_err();
assert!(
matches!(err, CliError::Usage(ref m) if m.contains("Part II")),
"an empty narrative must be refused naming 'Part II': {err}"
);
assert_eq!(
count(&vault, |p| matches!(p, EventPayload::PromoteTranche(_))),
0
);
}
#[test]
fn a_recorded_promote_carries_the_acknowledgment_and_stored_floor() {
let dir = tempfile::tempdir().unwrap();
let (vault, target) = vault_with_tranche(dir.path());
cmd::promote::promote_tranche(
&vault,
&pp(),
&target,
ProvenanceKind::Purchase,
"cash P2P, no records".into(),
Some(PROMOTE_ACK_PHRASE),
now(),
)
.unwrap();
let p = only_promote(&vault);
assert!(
p.filed_basis > btctax_core::Usd::ZERO,
"filed_basis must be > $0: {p:?}"
);
assert!(!p.acknowledgment.phrase.is_empty());
assert_eq!(p.acknowledgment.phrase, PROMOTE_ACK_PHRASE);
assert!(p.provenance_attested);
assert_eq!(p.part_ii_narrative, "cash P2P, no records");
}
#[test]
fn a_second_promote_is_refused_by_would_conflict() {
let dir = tempfile::tempdir().unwrap();
let (vault, target) = vault_with_promoted_tranche(dir.path());
let err = cmd::promote::promote_tranche(
&vault,
&pp(),
&target,
ProvenanceKind::Purchase,
"x".into(),
Some(PROMOTE_ACK_PHRASE),
now(),
)
.unwrap_err();
assert!(
matches!(err, CliError::Usage(ref m) if m.contains("conflict")),
"a second promote must be refused naming 'conflict': {err}"
);
assert_eq!(
count(&vault, |p| matches!(p, EventPayload::PromoteTranche(_))),
1,
"still exactly the ORIGINAL promote — the second attempt appended nothing"
);
}
#[test]
fn the_consent_copy_names_the_underpayment_base_and_never_says_safe_harbor() {
let screen = cmd::promote::render_consent(&consent_terms_fixture(), &BTreeSet::new());
assert!(!screen.to_lowercase().contains("safe harbor"));
assert!(screen.contains("of the resulting additional tax") && screen.contains("plus interest"));
}
#[test]
fn consent_copy_pins_the_deduction_exclusion_and_unrealized_labels() {
let screen = cmd::promote::render_consent(
&consent_terms_with_deduction_and_unrealized(),
&BTreeSet::new(),
);
assert!(
screen.contains("does NOT capture this charitable-deduction change"),
"tax-Δ-excludes-deduction sentence: {screen}"
);
assert!(
screen.contains("hypothetical, not a filed figure"),
"unrealized label rendered: {screen}"
);
}
#[test]
fn consent_copy_labels_a_gift_only_year_as_donee_basis_not_schedule_a() {
let terms = vec![ConsentTerm::ComputedTax {
year: 2020,
delta_usd: dec!(0),
deduction_delta_usd: Some(dec!(400)),
}];
let mut gift_only_years = BTreeSet::new();
gift_only_years.insert(2020);
let screen = cmd::promote::render_consent(&terms, &gift_only_years);
assert!(
screen.contains("donee-basis (§1015)") && screen.contains("donor's 1040 is unaffected"),
"a gift-only year must be labeled donee-basis, not Schedule-A: {screen}"
);
assert!(
screen.contains("NOT a Schedule-A deduction"),
"the gift-only year's Δ must be explicitly denied as a Schedule-A deduction: {screen}"
);
}
#[test]
fn consent_copy_labels_a_gift_only_uncomputable_year_as_donee_basis_not_schedule_a() {
let terms = vec![ConsentTerm::Uncomputable {
year: 2019,
gain_delta_usd: dec!(0),
deduction_delta_usd: dec!(250),
}];
let mut gift_only_years = BTreeSet::new();
gift_only_years.insert(2019);
let screen = cmd::promote::render_consent(&terms, &gift_only_years);
assert!(
screen.contains("donee-basis (§1015)") && screen.contains("NOT a Schedule-A deduction"),
"a gift-only UNCOMPUTABLE year must also be labeled donee-basis, not Schedule-A: {screen}"
);
}
fn run_promote(
vault: &Path,
target: &str,
part_ii_path: &Path,
extra: &[&str],
) -> (i32, String, String) {
let bin = env!("CARGO_BIN_EXE_btctax");
let mut c = std::process::Command::new(bin);
c.arg("--vault")
.arg(vault.to_str().unwrap())
.arg("reconcile")
.arg("promote-tranche")
.arg(target)
.arg("--provenance")
.arg("purchase")
.arg("--part-ii-file")
.arg(part_ii_path.to_str().unwrap())
.env("BTCTAX_PASSPHRASE", "pw");
for a in extra {
c.arg(a);
}
let out = c.output().expect("btctax binary must execute");
(
out.status.code().expect("exits normally"),
String::from_utf8_lossy(&out.stdout).into_owned(),
String::from_utf8_lossy(&out.stderr).into_owned(),
)
}
#[test]
fn non_tty_missing_acknowledge_still_prints_the_consent_screen_and_refuses() {
let dir = tempfile::tempdir().unwrap();
let (vault, target) = vault_with_tranche(dir.path());
let part_ii = dir.path().join("part_ii.txt");
std::fs::write(
&part_ii,
"cash P2P purchase, no records; window bounded on-chain",
)
.unwrap();
let (code, stdout, stderr) = run_promote(&vault, &target, &part_ii, &[]);
assert_ne!(
code, 0,
"missing --i-acknowledge must refuse; stderr: {stderr}"
);
assert!(
stdout.contains("of the resulting additional tax"),
"the consent screen prints even on refusal (N-2): {stdout}"
);
assert_eq!(
count(&vault, |p| matches!(p, EventPayload::PromoteTranche(_))),
0,
"the refused promote must NOT be appended (fail-closed)"
);
let (code2, stdout2, stderr2) = run_promote(
&vault,
&target,
&part_ii,
&["--i-acknowledge", PROMOTE_ACK_PHRASE],
);
assert_eq!(
code2, 0,
"a correct --i-acknowledge must succeed; stderr: {stderr2}"
);
assert!(stdout2.contains("of the resulting additional tax"));
assert_eq!(
count(&vault, |p| matches!(p, EventPayload::PromoteTranche(_))),
1
);
}
fn vault_with_wide_window_tranche(dir: &Path) -> (PathBuf, String) {
let vault = dir.join("vault.pgp");
cmd::init::run(&vault, &pp(), &dir.join("k.asc")).unwrap();
let id = cmd::tranche::declare_tranche(
&vault,
&pp(),
40_000_000,
tranche_wallet(),
date!(2015 - 01 - 01),
date!(2018 - 01 - 01),
now(),
)
.unwrap();
(vault, id.canonical())
}
#[test]
fn a_wide_window_promote_prints_the_trivial_floor_caution() {
let dir = tempfile::tempdir().unwrap();
let (vault, target) = vault_with_wide_window_tranche(dir.path());
let part_ii = dir.path().join("part_ii.txt");
std::fs::write(
&part_ii,
"cash P2P purchase, no records; wide multi-year window",
)
.unwrap();
let (code, stdout, stderr) = run_promote(
&vault,
&target,
&part_ii,
&["--i-acknowledge", PROMOTE_ACK_PHRASE],
);
assert_eq!(code, 0, "stderr: {stderr}");
let lower = stdout.to_lowercase();
assert!(
lower.contains("trivial") && lower.contains("wide"),
"a wide window must print the trivial-floor caution: {stdout}"
);
}
const T14_YEAR: i32 = 2024;
fn t14_sell() -> LedgerEvent {
imp(
"T14-SELL",
datetime!(2024 - 09 - 01 0:00 UTC),
EventPayload::Dispose(Dispose {
sat: 40_000_000,
usd_proceeds: dec!(20_000),
fee_usd: dec!(0),
kind: DisposeKind::Sell,
}),
)
}
fn raw_vault_promote_with_empty_part_ii(dir: &Path) -> PathBuf {
let vault = dir.join("vault.pgp");
cmd::init::run(&vault, &pp(), &dir.join("k.asc")).unwrap();
let tranche_id = cmd::tranche::declare_tranche(
&vault,
&pp(),
40_000_000,
wallet(),
date!(2024 - 01 - 01),
date!(2024 - 03 - 31),
now(),
)
.unwrap();
let mut s = Session::open(&vault, &pp()).unwrap();
append_decision(
s.conn(),
EventPayload::PromoteTranche(PromoteTranche {
target: tranche_id,
method: FloorMethod::WindowLowClose,
filed_basis: dec!(12_000),
coverage: Coverage::Full,
provenance_attested: true,
acknowledgment: Acknowledgment {
phrase: PROMOTE_ACK_PHRASE.into(),
shown_terms: vec![],
provenance_text: "acquired by purchase within the declared window".into(),
provenance_version: "v1".into(),
},
part_ii_narrative: String::new(), }),
now(),
UtcOffset::UTC,
None,
)
.unwrap();
append_import_batch(s.conn(), &[t14_sell()]).unwrap();
s.save().unwrap();
vault
}
fn vault_with_promoted_disposal_via_cli(dir: &Path) -> PathBuf {
let vault = dir.join("vault.pgp");
cmd::init::run(&vault, &pp(), &dir.join("k.asc")).unwrap();
let tranche_id = cmd::tranche::declare_tranche(
&vault,
&pp(),
40_000_000,
wallet(),
date!(2024 - 01 - 01),
date!(2024 - 03 - 31),
now(),
)
.unwrap();
cmd::promote::promote_tranche(
&vault,
&pp(),
&tranche_id.canonical(),
ProvenanceKind::Purchase,
"cash P2P purchase, no records; window bounded on-chain".into(),
Some(PROMOTE_ACK_PHRASE),
now(),
)
.unwrap();
let mut s = Session::open(&vault, &pp()).unwrap();
append_import_batch(s.conn(), &[t14_sell()]).unwrap();
s.save().unwrap();
vault
}
#[test]
fn export_with_a_promoted_leg_but_incomplete_8275_refuses_before_bytes() {
let dir = tempfile::tempdir().unwrap();
let vault = raw_vault_promote_with_empty_part_ii(dir.path());
let out = dir.path().join("export_out");
let err = cmd::admin::export_irs_pdf(&vault, &pp(), &out, T14_YEAR, &[], None).unwrap_err();
assert!(
matches!(err, CliError::Usage(ref m) if m.contains("Form 8275")),
"a promoted leg without a complete Form 8275 must be REFUSED naming 'Form 8275': {err}"
);
assert!(
std::fs::read_dir(&out)
.map(|mut d| d.next().is_none())
.unwrap_or(true),
"a refused export leaves out_dir untouched (zero bytes written)"
);
}
#[test]
fn a_clean_promoted_export_writes_the_8275_by_name_no_watermark() {
let dir = tempfile::tempdir().unwrap();
let vault = vault_with_promoted_disposal_via_cli(dir.path());
let out = dir.path().join("export_out");
let report = cmd::admin::export_irs_pdf(&vault, &pp(), &out, T14_YEAR, &[], None).unwrap();
assert!(
out.join("form_8275.txt").exists(),
"a clean promoted export emits the 8275 content by its OWN name (form_8275.txt)"
);
assert!(
!report.watermarked,
"a real promoted ledger exports CLEAN (no DRAFT watermark)"
);
}
fn vault_with_promoted_disposal_and_overflowing_part_ii(dir: &Path) -> PathBuf {
let vault = dir.join("vault.pgp");
cmd::init::run(&vault, &pp(), &dir.join("k.asc")).unwrap();
let tranche_id = cmd::tranche::declare_tranche(
&vault,
&pp(),
40_000_000,
wallet(),
date!(2024 - 01 - 01),
date!(2024 - 03 - 31),
now(),
)
.unwrap();
cmd::promote::promote_tranche(
&vault,
&pp(),
&tranche_id.canonical(),
ProvenanceKind::Purchase,
"word ".repeat(900),
Some(PROMOTE_ACK_PHRASE),
now(),
)
.unwrap();
let mut s = Session::open(&vault, &pp()).unwrap();
append_import_batch(s.conn(), &[t14_sell()]).unwrap();
s.save().unwrap();
vault
}
#[test]
fn export_irs_pdf_with_an_overflowing_part_ii_narrative_refuses_before_bytes() {
let dir = tempfile::tempdir().unwrap();
let vault = vault_with_promoted_disposal_and_overflowing_part_ii(dir.path());
let out = dir.path().join("export_out");
let err = cmd::admin::export_irs_pdf(&vault, &pp(), &out, T14_YEAR, &[], None).unwrap_err();
assert!(
matches!(err, CliError::Usage(ref m)
if m.contains(&T14_YEAR.to_string())
&& m.contains("Part II")
&& m.contains("--part-ii-file")),
"must name the year, 'Part II', and the --part-ii-file remedy: {err}"
);
assert!(
std::fs::read_dir(&out)
.map(|mut d| d.next().is_none())
.unwrap_or(true),
"a refused export leaves out_dir untouched (zero bytes written) — NOT a half-populated packet"
);
}
#[test]
fn export_full_return_with_an_overflowing_part_ii_narrative_refuses_with_a_named_remedy() {
let dir = tempfile::tempdir().unwrap();
let vault = vault_with_promoted_disposal_and_overflowing_part_ii(dir.path());
plant_full_return_ri(&vault, T14_YEAR);
let out = dir.path().join("export_out");
let err = cmd::admin::export_irs_pdf(&vault, &pp(), &out, T14_YEAR, &[], None).unwrap_err();
assert!(
matches!(err, CliError::Usage(ref m)
if m.contains(&T14_YEAR.to_string())
&& m.contains("Part II")
&& m.contains("--part-ii-file")),
"the full-return path must give the SAME named-remedy refusal: {err}"
);
assert!(
std::fs::read_dir(&out)
.map(|mut d| d.next().is_none())
.unwrap_or(true),
"a refused full-return export leaves out_dir untouched"
);
}
#[test]
fn a_narrowed_forms_f8949_slice_still_emits_the_mandatory_8275_pdf_on_a_promoted_year() {
let dir = tempfile::tempdir().unwrap();
let vault = vault_with_promoted_disposal_via_cli(dir.path());
let out = dir.path().join("export_out");
let report =
cmd::admin::export_irs_pdf(&vault, &pp(), &out, T14_YEAR, &[FormArg::F8949], None).unwrap();
assert!(
out.join("f8949.pdf").exists(),
"the requested 8949 slice is written"
);
assert!(
out.join("form_8275.pdf").exists(),
"the MANDATORY 8275 disclosure PDF rides even though --forms excluded it (BG-D8)"
);
assert!(
report.form_8275_path.is_some(),
"the report records the mandatory 8275 PDF path"
);
}
fn plant_full_return_ri(vault: &Path, year: i32) {
use btctax_core::tax::return_inputs::{Person, ReturnInputs};
use btctax_core::tax::types::FilingStatus;
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 = 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(), year, &ri).unwrap();
s.save().unwrap();
}
#[test]
fn export_full_return_refuses_before_bytes_on_incomplete_8275() {
let dir = tempfile::tempdir().unwrap();
let vault = raw_vault_promote_with_empty_part_ii(dir.path());
plant_full_return_ri(&vault, T14_YEAR);
let out = dir.path().join("export_out");
let err = cmd::admin::export_irs_pdf(&vault, &pp(), &out, T14_YEAR, &[], None).unwrap_err();
assert!(
matches!(err, CliError::Usage(ref m) if m.contains("Form 8275")),
"the FULL-RETURN placement must refuse a promoted leg without a complete Form 8275: {err}"
);
assert!(
std::fs::read_dir(&out)
.map(|mut d| d.next().is_none())
.unwrap_or(true),
"a refused full-return export leaves out_dir untouched (zero bytes written)"
);
}
#[test]
fn export_snapshot_refuses_before_bytes_on_incomplete_8275() {
let dir = tempfile::tempdir().unwrap();
let vault = raw_vault_promote_with_empty_part_ii(dir.path());
let out = dir.path().join("export_out");
let err = cmd::admin::export_snapshot(&vault, &pp(), &out, Some(T14_YEAR), None).unwrap_err();
assert!(
matches!(err, CliError::Usage(ref m) if m.contains("Form 8275")),
"export_snapshot must refuse a promoted leg without a complete Form 8275: {err}"
);
assert!(
std::fs::read_dir(&out)
.map(|mut d| d.next().is_none())
.unwrap_or(true),
"a refused export_snapshot leaves out_dir untouched (zero bytes written)"
);
}
#[test]
fn export_full_return_writes_form_8275_txt_by_name() {
let dir = tempfile::tempdir().unwrap();
let vault = vault_with_promoted_disposal_via_cli(dir.path());
plant_full_return_ri(&vault, T14_YEAR);
let out = dir.path().join("export_out");
cmd::admin::export_irs_pdf(&vault, &pp(), &out, T14_YEAR, &[], None)
.expect("a complete promoted disclosure exports via the dispatched full-return path");
assert!(
out.join("form_8275.txt").exists(),
"the full-return packet emits the 8275 content by its OWN name (form_8275.txt)"
);
}
#[test]
fn export_snapshot_writes_form_8275_txt_by_name() {
let dir = tempfile::tempdir().unwrap();
let vault = vault_with_promoted_disposal_via_cli(dir.path());
let out = dir.path().join("export_out");
cmd::admin::export_snapshot(&vault, &pp(), &out, Some(T14_YEAR), None)
.expect("a complete promoted disclosure exports via export_snapshot");
assert!(
out.join("form_8275.txt").exists(),
"export_snapshot emits the 8275 content by its OWN name (form_8275.txt)"
);
}
fn vault_with_promoted_disposal_via_cli_year(dir: &Path, year: i32) -> PathBuf {
let vault = dir.join("vault.pgp");
cmd::init::run(&vault, &pp(), &dir.join("k.asc")).unwrap();
let window_start = time::Date::from_calendar_date(year, time::Month::January, 1).unwrap();
let window_end = time::Date::from_calendar_date(year, time::Month::March, 31).unwrap();
let tranche_id = cmd::tranche::declare_tranche(
&vault,
&pp(),
40_000_000,
wallet(),
window_start,
window_end,
now(),
)
.unwrap();
cmd::promote::promote_tranche(
&vault,
&pp(),
&tranche_id.canonical(),
ProvenanceKind::Purchase,
"cash P2P purchase, no records; window bounded on-chain".into(),
Some(PROMOTE_ACK_PHRASE),
now(),
)
.unwrap();
let sell_ts = time::Date::from_calendar_date(year, time::Month::September, 1)
.unwrap()
.with_hms(0, 0, 0)
.unwrap()
.assume_utc();
let sell = imp(
"T16-SELL",
sell_ts,
EventPayload::Dispose(Dispose {
sat: 40_000_000,
usd_proceeds: dec!(20_000),
fee_usd: dec!(0),
kind: DisposeKind::Sell,
}),
);
let mut s = Session::open(&vault, &pp()).unwrap();
append_import_batch(s.conn(), &[sell]).unwrap();
s.save().unwrap();
vault
}
#[test]
fn a_promoted_2025_export_fills_the_8275_and_the_gate_passes() {
for year in [2025, 2017] {
let dir = tempfile::tempdir().unwrap();
let vault = vault_with_promoted_disposal_via_cli_year(dir.path(), year);
let out = dir.path().join("export_out");
let report = cmd::admin::export_irs_pdf(&vault, &pp(), &out, year, &[], None)
.unwrap_or_else(|e| panic!("year {year}: a complete promote must export cleanly: {e}"));
assert!(
out.join("form_8275.pdf").exists(),
"year {year}: the crypto-slice export must fill the official Form 8275 PDF"
);
assert_eq!(
report.form_8275_path,
Some(out.join("form_8275.pdf")),
"year {year}: the report names the written 8275 PDF"
);
assert!(
!report.watermarked,
"year {year}: a real (non-pseudo) ledger exports CLEAN"
);
}
}
#[test]
fn export_gate_now_refuses_when_the_8275_pdf_is_absent() {
let dir = tempfile::tempdir().unwrap();
let vault = raw_vault_promote_with_empty_part_ii(dir.path());
let out = dir.path().join("export_out");
let err = cmd::admin::export_irs_pdf(&vault, &pp(), &out, T14_YEAR, &[], None).unwrap_err();
assert!(
matches!(err, CliError::Usage(ref m) if m.contains("Form 8275")),
"an incomplete Form 8275 must still refuse the export post-T16: {err}"
);
assert!(
std::fs::read_dir(&out)
.map(|mut d| d.next().is_none())
.unwrap_or(true),
"a refused export leaves out_dir untouched — not even a partial form_8275.pdf"
);
assert!(!out.join("form_8275.pdf").exists());
assert!(!out.join("form_8275.txt").exists());
}
fn vault_with_n_promoted_disposal_legs(dir: &Path, year: i32, n: u32) -> PathBuf {
let vault = dir.join("vault.pgp");
cmd::init::run(&vault, &pp(), &dir.join("k.asc")).unwrap();
let window_start = time::Date::from_calendar_date(year, time::Month::January, 1).unwrap();
let window_end = time::Date::from_calendar_date(year, time::Month::March, 31).unwrap();
let sell_ts = time::Date::from_calendar_date(year, time::Month::September, 1)
.unwrap()
.with_hms(0, 0, 0)
.unwrap()
.assume_utc();
for i in 0..n {
let w = WalletId::Exchange {
provider: "coinbase".into(),
account: format!("overflow{i}"),
};
let tranche_id = cmd::tranche::declare_tranche(
&vault,
&pp(),
40_000_000,
w.clone(),
window_start,
window_end,
now(),
)
.unwrap();
cmd::promote::promote_tranche(
&vault,
&pp(),
&tranche_id.canonical(),
ProvenanceKind::Purchase,
format!("cash P2P purchase #{i}, no records; window bounded on-chain"),
Some(PROMOTE_ACK_PHRASE),
now(),
)
.unwrap();
let mut s = Session::open(&vault, &pp()).unwrap();
append_import_batch(
s.conn(),
&[LedgerEvent {
id: EventId::import(
Source::Coinbase,
SourceRef::new(format!("OVERFLOW-SELL-{i}")),
),
utc_timestamp: sell_ts,
original_tz: UtcOffset::UTC,
wallet: Some(w),
payload: EventPayload::Dispose(Dispose {
sat: 40_000_000,
usd_proceeds: dec!(20_000),
fee_usd: dec!(0),
kind: DisposeKind::Sell,
}),
}],
)
.unwrap();
s.save().unwrap();
}
vault
}
#[test]
fn promoted_export_with_more_than_6_legs_refuses_cleanly_not_panics() {
let dir = tempfile::tempdir().unwrap();
let vault = vault_with_n_promoted_disposal_legs(dir.path(), T14_YEAR, 7);
plant_full_return_ri(&vault, T14_YEAR);
let out = dir.path().join("export_out");
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
cmd::admin::export_irs_pdf(&vault, &pp(), &out, T14_YEAR, &[], None)
}));
let err = match result {
Ok(Err(e)) => e,
Ok(Ok(_)) => {
panic!("a 7-leg promoted year (> the 6-row capacity) must refuse, not succeed")
}
Err(_) => panic!("a 7-leg promoted year must refuse CLEANLY, not panic"),
};
assert!(
matches!(err, CliError::Usage(ref m)
if m.contains(&T14_YEAR.to_string()) && m.contains("7 promoted disposal leg")),
"the refusal names the year and the leg count: {err}"
);
assert!(
err.to_string().contains("void one of the promotes"),
"the refusal names a concrete remedy: {err}"
);
assert!(
std::fs::read_dir(&out)
.map(|mut d| d.next().is_none())
.unwrap_or(true),
"an overflow refusal leaves out_dir untouched (refuse-before-bytes) — no half-written packet"
);
}
#[test]
fn promoted_crypto_slice_export_with_more_than_6_legs_refuses_cleanly_not_panics() {
let dir = tempfile::tempdir().unwrap();
let vault = vault_with_n_promoted_disposal_legs(dir.path(), T14_YEAR, 7);
let out = dir.path().join("export_out");
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
cmd::admin::export_irs_pdf(&vault, &pp(), &out, T14_YEAR, &[], None)
}));
let err = match result {
Ok(Err(e)) => e,
Ok(Ok(_)) => {
panic!("a 7-leg promoted crypto-slice export (> the 6-row capacity) must refuse, not succeed")
}
Err(_) => panic!("a 7-leg promoted crypto-slice export must refuse CLEANLY, not panic"),
};
assert!(
matches!(err, CliError::Usage(ref m)
if m.contains(&T14_YEAR.to_string()) && m.contains("7 promoted disposal leg")),
"the crypto-slice refusal names the year and the leg count: {err}"
);
assert!(
err.to_string().contains("void one of the promotes"),
"the crypto-slice refusal names a concrete remedy: {err}"
);
assert!(
std::fs::read_dir(&out)
.map(|mut d| d.next().is_none())
.unwrap_or(true),
"a crypto-slice overflow refusal leaves out_dir untouched (refuse-before-bytes)"
);
}
fn vault_with_two_promoted_years(dir: &Path, year_a: i32, year_b: i32) -> PathBuf {
let vault = dir.join("vault.pgp");
cmd::init::run(&vault, &pp(), &dir.join("k.asc")).unwrap();
for (i, year) in [year_a, year_b].into_iter().enumerate() {
let w = WalletId::Exchange {
provider: "coinbase".into(),
account: format!("m2-{i}"),
};
let window_start = time::Date::from_calendar_date(year, time::Month::January, 1).unwrap();
let window_end = time::Date::from_calendar_date(year, time::Month::March, 31).unwrap();
let tranche_id = cmd::tranche::declare_tranche(
&vault,
&pp(),
40_000_000,
w.clone(),
window_start,
window_end,
now(),
)
.unwrap();
cmd::promote::promote_tranche(
&vault,
&pp(),
&tranche_id.canonical(),
ProvenanceKind::Purchase,
format!("cash P2P purchase, no records; {year} window bounded on-chain"),
Some(PROMOTE_ACK_PHRASE),
now(),
)
.unwrap();
let sell_ts = time::Date::from_calendar_date(year, time::Month::September, 1)
.unwrap()
.with_hms(0, 0, 0)
.unwrap()
.assume_utc();
let mut s = Session::open(&vault, &pp()).unwrap();
append_import_batch(
s.conn(),
&[LedgerEvent {
id: EventId::import(Source::Coinbase, SourceRef::new(format!("M2-SELL-{i}"))),
utc_timestamp: sell_ts,
original_tz: UtcOffset::UTC,
wallet: Some(w),
payload: EventPayload::Dispose(Dispose {
sat: 40_000_000,
usd_proceeds: dec!(20_000),
fee_usd: dec!(0),
kind: DisposeKind::Sell,
}),
}],
)
.unwrap();
s.save().unwrap();
}
vault
}
#[test]
fn all_years_snapshot_writes_one_8275_txt_per_promoted_year() {
let dir = tempfile::tempdir().unwrap();
let vault = vault_with_two_promoted_years(dir.path(), 2024, 2017);
let out = dir.path().join("export_out");
cmd::admin::export_snapshot(&vault, &pp(), &out, None, None)
.expect("an all-years snapshot with two complete promoted years must succeed");
assert!(
out.join("form_8275_2024.txt").exists(),
"the 2024 promoted year's disclosure is co-emitted"
);
assert!(
out.join("form_8275_2017.txt").exists(),
"the 2017 promoted year's disclosure is co-emitted"
);
assert!(
!out.join("form_8275.txt").exists(),
"the all-years dump never uses the bare, collision-prone name"
);
}
#[test]
fn characterization_crypto_slice_export_pins_the_shipped_file_set_and_report() {
let dir = tempfile::tempdir().unwrap();
let vault = vault_with_promoted_disposal_via_cli(dir.path());
let out = dir.path().join("export_out");
let report = cmd::admin::export_irs_pdf(&vault, &pp(), &out, T14_YEAR, &[], None)
.expect("the crypto-slice arm exports cleanly");
let mut names: Vec<String> = std::fs::read_dir(&out)
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.collect();
names.sort();
assert_eq!(
names,
vec![
"basis_methodology.txt".to_string(),
"f8949.pdf".to_string(),
"form_1040_capgains.pdf".to_string(),
"form_8275.pdf".to_string(),
"form_8275.txt".to_string(),
"schedule_d.pdf".to_string(),
],
"the crypto-slice arm's emitted file set is pinned exactly — the Approach-B experimental \
notice is INTERFACE-only (stderr) and must NEVER appear as a written file, even though this \
fixture's vault has a live promoted tranche"
);
assert_eq!(report.f8949_path, Some(out.join("f8949.pdf")));
assert_eq!(report.schedule_d_path, Some(out.join("schedule_d.pdf")));
assert_eq!(report.tax_year, T14_YEAR);
assert_eq!(report.unresolved_hard, 0);
assert_eq!(report.broker_reported_rows, 1);
assert!(
!report.watermarked,
"a real (non-pseudo) ledger exports CLEAN"
);
assert_eq!(report.schedule_se_path, None);
assert!(!report.se_below_floor);
assert_eq!(report.se_addl_medicare, None);
assert!(!report.se_income_without_profile);
assert_eq!(report.form_8283_path, None);
assert!(!report.form_8283_needs_review);
assert_eq!(report.form_8283_section_b, None);
assert_eq!(
report.form_1040_path,
Some(out.join("form_1040_capgains.pdf"))
);
assert!(report.form_1040_filled_7a);
assert!(!report.form_1040_loss);
assert_eq!(report.form_8275_path, Some(out.join("form_8275.pdf")));
assert!(
report.full_return_paths.is_empty(),
"the crypto-slice arm never populates the full-return fields"
);
assert_eq!(report.full_return_manifest, None);
assert!(
!report.forms_ignored_full_return,
"the crypto-slice arm always honors --forms"
);
assert!(
report.experimental_notice_active,
"this fixture's vault has a live promoted tranche — Approach-B is in use"
);
}
#[test]
fn characterization_full_return_export_pins_the_shipped_file_set_and_report() {
let dir = tempfile::tempdir().unwrap();
let vault = vault_with_promoted_disposal_via_cli(dir.path());
plant_full_return_ri(&vault, T14_YEAR);
let out = dir.path().join("export_out");
let report = cmd::admin::export_irs_pdf(&vault, &pp(), &out, T14_YEAR, &[], None)
.expect("the full-return arm exports cleanly");
let mut names: Vec<String> = std::fs::read_dir(&out)
.unwrap()
.map(|e| e.unwrap().file_name().to_string_lossy().into_owned())
.collect();
names.sort();
assert_eq!(
names,
vec![
"00_f1040.pdf".to_string(),
"12A_f8949.pdf".to_string(),
"12_schedule_d.pdf".to_string(),
"92_f8275.pdf".to_string(),
"basis_methodology.txt".to_string(),
"form_8275.txt".to_string(),
"manifest.txt".to_string(),
],
"the full-return arm's emitted file set is pinned exactly — the Approach-B experimental \
notice is INTERFACE-only (stderr) and must NEVER appear as a written file, even though this \
fixture's vault has a live promoted tranche"
);
assert_eq!(report.f8949_path, None);
assert_eq!(report.schedule_d_path, None);
assert_eq!(report.tax_year, T14_YEAR);
assert_eq!(report.unresolved_hard, 0);
assert_eq!(report.broker_reported_rows, 0);
assert!(
!report.watermarked,
"a real (non-pseudo) ledger exports CLEAN"
);
assert_eq!(report.schedule_se_path, None);
assert!(!report.se_below_floor);
assert_eq!(report.se_addl_medicare, None);
assert!(!report.se_income_without_profile);
assert_eq!(report.form_8283_path, None);
assert_eq!(report.form_1040_path, None);
assert!(!report.form_1040_filled_7a);
assert!(!report.form_1040_loss);
assert_eq!(report.form_8275_path, None);
assert!(
!report.forms_ignored_full_return,
"an EMPTY --forms slice is never reported as ignored"
);
assert_eq!(
report.full_return_paths,
vec![
out.join("00_f1040.pdf"),
out.join("12_schedule_d.pdf"),
out.join("12A_f8949.pdf"),
out.join("92_f8275.pdf"),
],
"the full-return packet's paths are pinned exactly, in this order"
);
assert_eq!(report.full_return_manifest, Some(out.join("manifest.txt")));
assert!(
report.experimental_notice_active,
"this fixture's vault has a live promoted tranche — Approach-B is in use"
);
}
fn seed_removal_reorder(vault: &Path, suffix: &str, donation_year: i32) -> WalletId {
let w = WalletId::SelfCustody {
label: format!("t3-flagged-years-{suffix}"),
};
let buy = LedgerEvent {
id: EventId::import(Source::Coinbase, SourceRef::new(format!("T3-BUY-{suffix}"))),
utc_timestamp: datetime!(2015-06-01 0:00 UTC),
original_tz: UtcOffset::UTC,
wallet: Some(w.clone()),
payload: EventPayload::Acquire(Acquire {
sat: 100_000_000,
usd_cost: dec!(1), fee_usd: dec!(0),
basis_source: BasisSource::ExchangeProvided,
}),
};
let give_rf = format!("T3-GIVEOUT-{suffix}");
let give_ts = time::Date::from_calendar_date(donation_year, time::Month::June, 1)
.unwrap()
.with_hms(0, 0, 0)
.unwrap()
.assume_utc();
let give = LedgerEvent {
id: EventId::import(Source::Coinbase, SourceRef::new(give_rf.clone())),
utc_timestamp: give_ts,
original_tz: UtcOffset::UTC,
wallet: Some(w.clone()),
payload: EventPayload::TransferOut(TransferOut {
sat: 40_000_000,
fee_sat: None,
dest_addr: None,
txid: None,
}),
};
{
let mut s = Session::open(vault, &pp()).unwrap();
append_import_batch(s.conn(), &[buy, give]).unwrap();
s.save().unwrap();
}
let tranche_id = cmd::tranche::declare_tranche(
vault,
&pp(),
40_000_000,
w.clone(),
date!(2016 - 01 - 01),
date!(2016 - 03 - 31),
now(),
)
.unwrap();
cmd::promote::promote_tranche(
vault,
&pp(),
&tranche_id.canonical(),
ProvenanceKind::Purchase,
format!("cash P2P purchase {suffix}, no records; window bounded on-chain"),
Some(PROMOTE_ACK_PHRASE),
now(),
)
.unwrap();
cmd::reconcile::reclassify_outflow(
vault,
&pp(),
&EventId::import(Source::Coinbase, SourceRef::new(give_rf)).canonical(),
OutflowClass::Donate {
appraisal_required: false,
},
dec!(5_000),
None,
None,
now(),
)
.unwrap();
w
}
#[test]
fn flagged_years_includes_a_removal_reordered_prior_year_with_no_promoted_disposal() {
let dir = tempfile::tempdir().unwrap();
let vault = dir.path().join("vault.pgp");
cmd::init::run(&vault, &pp(), &dir.path().join("k.asc")).unwrap();
seed_removal_reorder(&vault, "a", 2025);
let session = Session::open(&vault, &pp()).unwrap();
let (events, state, cfg) = session.load_events_and_project().unwrap();
let tables = btctax_adapters::BundledTaxTables::load();
let current = 2026;
let years = flagged_years(&events, &state, session.prices(), &tables, &cfg, current);
assert!(
years.contains(&2025),
"the removal-only reorder (no promoted disposal leg) must be flagged: {years:?}"
);
assert!(
!years.contains(¤t),
"the current year is not a REORDERED prior year and must not be flagged as one: {years:?}"
);
}
#[test]
fn flagged_years_includes_the_prior_year_a_zero_dollar_declare_alone_fixed() {
let dir = tempfile::tempdir().unwrap();
let vault = dir.path().join("vault.pgp");
cmd::init::run(&vault, &pp(), &dir.path().join("k.asc")).unwrap();
let w = WalletId::SelfCustody {
label: "t-declare-only".into(),
};
{
let mut s = Session::open(&vault, &pp()).unwrap();
append_import_batch(
s.conn(),
&[LedgerEvent {
id: EventId::import(Source::Coinbase, SourceRef::new("DECLARE-ONLY-SELL")),
utc_timestamp: datetime!(2024-06-01 0:00 UTC),
original_tz: UtcOffset::UTC,
wallet: Some(w.clone()),
payload: EventPayload::Dispose(Dispose {
sat: 40_000_000,
usd_proceeds: dec!(20_000),
fee_usd: dec!(0),
kind: DisposeKind::Sell,
}),
}],
)
.unwrap();
s.save().unwrap();
}
cmd::tranche::declare_tranche(
&vault,
&pp(),
40_000_000,
w,
date!(2016 - 01 - 01),
date!(2016 - 03 - 31),
now(),
)
.unwrap();
let session = Session::open(&vault, &pp()).unwrap();
let (events, state, cfg) = session.load_events_and_project().unwrap();
let tables = btctax_adapters::BundledTaxTables::load();
let current = 2026;
assert!(
state.promoted_origins.is_empty(),
"fixture: this vault holds a $0 declare and NO promote — the promote-only union must be empty"
);
let years = flagged_years(&events, &state, session.prices(), &tables, &cfg, current);
assert!(
years.contains(&2024),
"the year the $0 declare re-filed (proceeds re-split + UncoveredDisposal cleared) must be \
flagged, exactly as a promote's reorder year is: {years:?}"
);
}
#[test]
fn flagged_years_keeps_a_promoted_tranche_whose_declare_void_the_engine_made_inert() {
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();
append_import_batch(
s.conn(),
&[imp(
"INERT-VOID-SELL",
datetime!(2024-06-01 0:00 UTC),
EventPayload::Dispose(Dispose {
sat: 40_000_000,
usd_proceeds: dec!(500),
fee_usd: dec!(500),
kind: DisposeKind::Sell,
}),
)],
)
.unwrap();
s.save().unwrap();
}
let tranche_id = cmd::tranche::declare_tranche(
&vault,
&pp(),
40_000_000,
wallet(),
date!(2016 - 01 - 01),
date!(2016 - 03 - 31),
now(),
)
.unwrap();
cmd::promote::promote_tranche(
&vault,
&pp(),
&tranche_id.canonical(),
ProvenanceKind::Purchase,
"cash P2P purchase, no records; window bounded on-chain".into(),
Some(PROMOTE_ACK_PHRASE),
now(),
)
.unwrap();
{
let mut s = Session::open(&vault, &pp()).unwrap();
append_decision(
s.conn(),
EventPayload::VoidDecisionEvent(btctax_core::event::VoidDecisionEvent {
target_event_id: tranche_id.clone(),
}),
now(),
UtcOffset::UTC,
None,
)
.unwrap();
s.save().unwrap();
}
let session = Session::open(&vault, &pp()).unwrap();
let (events, state, cfg) = session.load_events_and_project().unwrap();
let tables = btctax_adapters::BundledTaxTables::load();
assert!(
state.promoted_origins.contains(&tranche_id),
"fixture: the engine must hold the voided tranche IN FORCE via its live promote (an inert void), \
else this KAT is testing nothing"
);
let years = flagged_years(&events, &state, session.prices(), &tables, &cfg, 2026);
assert!(
years.contains(&2024),
"a tranche the ENGINE holds in force must stay in the export year-set union — a naive \
'some void names it' filter would silently shorten the packet set: {years:?}"
);
}