use btctax_core::conservative::Coverage;
use btctax_core::event::{Acknowledgment, BasisSource, DisposeKind, FloorMethod, PromoteTranche};
use btctax_core::forms::{Form8283HowAcquired, Form8283Row, Form8283Section};
use btctax_core::identity::{EventId, LotId, WalletId};
use btctax_core::state::{Disposal, DisposalLeg, Term};
use btctax_core::tax::form8275::Part1Item;
use btctax_core::tax::packet::{assemble_printed_return, PrintedReturn};
use btctax_core::tax::printed::{form_8283_printed, Printed8275};
use btctax_core::tax::return_1040::assemble_absolute;
use btctax_core::tax::testonly::{
kitchen_sink_household, ty2024_params, ty2024_table, w2_only_household,
};
use btctax_core::EventPayload;
use btctax_forms::fill_full_return;
use rust_decimal_macros::dec;
use std::collections::{BTreeMap, BTreeSet};
use time::macros::date;
const CENSUS_KEYS: [&str; 15] = [
"f1040",
"f1040s1",
"f1040s2",
"f1040s3",
"f1040sa",
"f1040sb",
"f1040sc",
"schedule_d",
"f8949",
"schedule_se",
"f8995",
"f8959",
"f8960",
"f8283",
"f8275",
];
fn injected_8283_row() -> Form8283Row {
Form8283Row {
section: Some(Form8283Section::A),
description: "0.05000000 BTC".to_string(),
how_acquired: Form8283HowAcquired::Purchased,
date_acquired: date!(2020 - 01 - 01),
date_contributed: date!(2024 - 09 - 01),
cost_basis: dec!(400),
fmv: dec!(600),
claimed_deduction: Some(dec!(600)),
fmv_method: String::new(),
donee: "Test Charity".to_string(),
appraiser: String::new(),
needs_review: true,
details: None,
}
}
fn injected_8275() -> Printed8275 {
Printed8275 {
part_i: vec![Part1Item {
form: "8949".to_string(),
line: "Part I \u{2014} column (e)".to_string(),
description: "basis estimated at the minimum daily closing price over the attested \
acquisition window (Cohan; the bearing-heavily minimum)"
.to_string(),
amount: dec!(300),
}],
part_ii: "cash P2P purchase, no records; window bounded on-chain".to_string(),
}
}
fn all_arms_return() -> PrintedReturn {
let (ri, state) = kitchen_sink_household();
let table = ty2024_table();
let ar = assemble_absolute(&ri, &state, &ty2024_params(), &table, 2024);
let details: BTreeMap<_, _> = BTreeMap::new();
let mut pr = assemble_printed_return(&ri, &state, &details, &ar, &table, 2024, &[])
.expect("kitchen_sink assembles");
assert!(
pr.forms.f8283.is_none(),
"premise: kitchen_sink has no f8283 — if this fires the fixture assumption changed"
);
assert!(
pr.forms.f8275.is_none(),
"premise: kitchen_sink has no promoted disposal leg (no f8275) — if this fires the fixture \
assumption changed"
);
pr.forms.f8283 = form_8283_printed(&[injected_8283_row()]);
pr.forms.f8275 = Some(injected_8275());
assert!(
pr.forms.f8283.is_some(),
"the injected f8283 arm must be Some"
);
assert!(
pr.forms.f8275.is_some(),
"the injected f8275 arm must be Some"
);
pr
}
#[test]
fn census_is_exactly_15_forms_including_8275_when_a_promote_is_present() {
let pr = all_arms_return();
let forms = fill_full_return(&pr, 2024).expect("the all-arms packet must fill");
let emitted: BTreeSet<&str> = forms.iter().map(|f| f.name.as_str()).collect();
let expected: BTreeSet<&str> = CENSUS_KEYS.iter().copied().collect();
assert_eq!(
forms.len(),
15,
"fill_full_return must emit EXACTLY 15 forms; got {} ({emitted:?})",
forms.len()
);
assert_eq!(
emitted, expected,
"the emitted form-name set must equal the §6.1 census keys exactly — a difference is a new or \
renamed form the census does not yet track"
);
let f8275 = forms
.iter()
.find(|f| f.name == "f8275")
.expect("f8275 is in the packet");
assert_eq!(
f8275.attachment_sequence,
Some("92"),
"Form 8275's IRS Attachment Sequence No. is 92"
);
}
#[test]
fn full_return_packet_emits_8275_iff_a_promoted_leg_is_filed() {
let (ri, mut state) = w2_only_household();
let table = ty2024_table();
let params = ty2024_params();
let origin = EventId::decision(1);
state.disposals.push(Disposal {
event: EventId::decision(2),
kind: DisposeKind::Sell,
disposed_at: date!(2024 - 06 - 01),
legs: vec![DisposalLeg {
lot_id: LotId {
origin_event_id: origin.clone(),
split_sequence: 0,
},
sat: 1_000_000,
proceeds: dec!(500),
basis: dec!(300),
gain: dec!(200),
term: Term::ShortTerm,
basis_source: BasisSource::ExchangeProvided,
gift_zone: None,
acquired_at: date!(2024 - 01 - 01),
wallet: WalletId::SelfCustody {
label: "cold".into(),
},
pseudo: false,
}],
fee_mini_disposition: false,
});
let ar = assemble_absolute(&ri, &state, ¶ms, &table, 2024);
let pr_unpromoted =
assemble_printed_return(&ri, &state, &BTreeMap::new(), &ar, &table, 2024, &[])
.expect("the fixture assembles");
assert!(
pr_unpromoted.forms.f8275.is_none(),
"a plain (non-promoted) disposal leg discloses nothing"
);
let packet_unpromoted =
fill_full_return(&pr_unpromoted, 2024).expect("the packet without a promote must fill");
assert!(
!packet_unpromoted.iter().any(|f| f.name == "f8275"),
"no f8275 in the packet when no leg is promoted"
);
state.promoted_origins.insert(origin.clone());
let events = vec![btctax_core::LedgerEvent {
id: EventId::decision(3),
utc_timestamp: time::OffsetDateTime::UNIX_EPOCH,
original_tz: time::UtcOffset::UTC,
wallet: None,
payload: EventPayload::PromoteTranche(PromoteTranche {
target: origin.clone(),
method: FloorMethod::WindowLowClose,
filed_basis: dec!(300),
coverage: Coverage::Full,
provenance_attested: true,
acknowledgment: Acknowledgment {
phrase: "I understand and accept this estimated-basis 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(),
}),
}];
let ar = assemble_absolute(&ri, &state, ¶ms, &table, 2024);
let pr_promoted =
assemble_printed_return(&ri, &state, &BTreeMap::new(), &ar, &table, 2024, &events)
.expect("the promoted fixture assembles");
let f8275 = pr_promoted
.forms
.f8275
.as_ref()
.expect("a promoted disposal leg discloses via f8275");
assert_eq!(f8275.part_i.len(), 1, "one promoted leg ⇒ one Part I item");
assert_eq!(
f8275.part_i[0].amount,
dec!(300),
"the AS-FILED col (e) basis"
);
assert_eq!(
f8275.part_ii,
"cash P2P purchase, no records; window bounded on-chain"
);
let packet_promoted =
fill_full_return(&pr_promoted, 2024).expect("the promoted packet must fill");
let f8275_form = packet_promoted
.iter()
.find(|f| f.name == "f8275")
.expect("f8275 is in the packet when a leg is promoted");
assert_eq!(f8275_form.attachment_sequence, Some("92"));
assert!(!f8275_form.bytes.is_empty());
}
fn golden() -> String {
let path = concat!(
env!("CARGO_MANIFEST_DIR"),
"/../../docs/examples/examples.md"
);
std::fs::read_to_string(path).unwrap_or_else(|e| {
panic!("read {path}: {e} — regenerate with `cargo run -p xtask -- examples > docs/examples/examples.md`")
})
}
fn j6_packet_names(golden: &str) -> BTreeSet<String> {
let j6_start = golden.find("## J6").expect("golden has a J6 section");
let j6_body = &golden[j6_start..];
let end = j6_body[2..]
.find("\n## ")
.map(|i| i + 2)
.unwrap_or(j6_body.len());
let j6 = &j6_body[..end];
let pkt_start = j6
.find("Full-return packet —")
.expect("J6 shows the `Full-return packet —` block");
let mut names = BTreeSet::new();
for line in j6[pkt_start..].lines() {
let base = line.trim().rsplit('/').next().unwrap_or("");
if let Some(stem) = base.strip_suffix(".pdf") {
if let Some((seq, name)) = stem.split_once('_') {
if seq.bytes().next().is_some_and(|b| b.is_ascii_digit()) {
names.insert(name.to_string());
}
}
}
}
names
}
#[test]
fn every_census_form_demonstrated_in_j6() {
let names = j6_packet_names(&golden());
let expected: BTreeSet<String> = CENSUS_KEYS.iter().map(|s| s.to_string()).collect();
let missing: Vec<&String> = expected.difference(&names).collect();
assert!(
missing.is_empty(),
"census forms undemonstrated in J6's packet: {missing:?} (present: {names:?})"
);
assert_eq!(
names, expected,
"J6's full-return packet stems must be EXACTLY the 15 census keys — got {names:?}"
);
}
#[test]
fn the_newest_era_preset_reaches_the_newest_filable_tax_year() {
let newest_filable = *btctax_forms::SUPPORTED_YEARS
.iter()
.max()
.expect("SUPPORTED_YEARS is non-empty");
let newest_preset_end = btctax_core::defensive::era::ALL_PRESETS
.iter()
.map(|p| btctax_core::defensive::era::era_window(*p).1)
.max()
.expect("ALL_PRESETS is non-empty");
assert!(
newest_preset_end.year() >= newest_filable,
"the newest era preset ends {newest_preset_end}, but btctax can file TY{newest_filable}: a filer \
with a TY{newest_filable} shortfall has no reachable acquisition-era preset. Extend the newest \
bucket in crates/btctax-core/src/defensive/era.rs (and update its defensive_era.rs KATs)."
);
}