Skip to main content

btctax_cli/
testonly.rs

1//! Shared synthetic corpora + per-journey fixtures for the worked-example journeys (J1–J9).
2//!
3//! Hoisted here (from xtask, TUI-walkthrough spec §4.1) so ALL consumers share ONE source of truth: the
4//! `xtask` examples generator, and the `btctax-tui` / `btctax-tui-edit` screen-walkthrough emit tests. A
5//! plain `pub` module (like `btctax_forms::testonly`) — the corpora are tiny synthetic CSVs, and the
6//! generator's `generate()` is a non-test fn, so they cannot be `#[cfg(test)]`.
7//!
8//! The CSV corpora carry explicit CRLF: committed `.csv` files are force-LF'd by `.gitattributes` and would
9//! break the Coinbase parser, so they live as string consts (a driver writes them to a tempdir at runtime).
10//! These bytes are byte-identical to their previous home in `xtask/src/examples.rs` — the
11//! `examples_golden_matches_committed` gate proves the move changed nothing.
12
13// ── Synthetic corpora (explicit CRLF) ──────────────────────────────────────────────────────────────
14
15/// J1 corpus: a single-buyer happy path — one Buy + one partial Sell in 2025.
16pub const J1_CSV: &str = "\r\nTransactions\r\nUser,00000000-0000-0000-0000-000000000000\r\n\
17ID,Timestamp,Transaction Type,Asset,Quantity Transacted,Price Currency,Price at Transaction,Subtotal,Total (inclusive of fees and/or spread),Fees and/or Spread,Notes,Sender Address,Recipient Address\r\n\
18cb-buy,2025-03-01 12:00:00 UTC,Buy,BTC,0.10000000,USD,84000.00,8400.00,8450.00,50.00,,,\r\n\
19cb-sell,2025-06-15 12:00:00 UTC,Sell,BTC,0.02000000,USD,67500.00,1350.00,1340.00,10.00,,,\r\n";
20
21/// J2 corpus: an LT lot (2023) + an ST lot (2025) + a 2025 Send of 2 BTC donated to charity.
22pub const J2_CSV: &str = "\r\nTransactions\r\nUser,00000000-0000-0000-0000-000000000000\r\n\
23ID,Timestamp,Transaction Type,Asset,Quantity Transacted,Price Currency,Price at Transaction,Subtotal,Total (inclusive of fees and/or spread),Fees and/or Spread,Notes,Sender Address,Recipient Address\r\n\
24cb-buy-lt,2023-06-01 12:00:00 UTC,Buy,BTC,1.00000000,USD,5000.00,5000.00,5000.00,0.00,,,\r\n\
25cb-buy-st,2025-03-01 12:00:00 UTC,Buy,BTC,1.00000000,USD,2000.00,2000.00,2000.00,0.00,,,\r\n\
26cb-donate,2025-09-01 12:00:00 UTC,Send,BTC,2.00000000,USD,108996.17,,,,,,bc1qcharity\r\n";
27
28/// J3 corpus: a Buy + a Receive (an inbound transfer with unknown basis → a hard blocker until classified).
29pub const J3_CSV: &str = "\r\nTransactions\r\nUser,00000000-0000-0000-0000-000000000000\r\n\
30ID,Timestamp,Transaction Type,Asset,Quantity Transacted,Price Currency,Price at Transaction,Subtotal,Total (inclusive of fees and/or spread),Fees and/or Spread,Notes,Sender Address,Recipient Address\r\n\
31cb-buy,2025-02-01 12:00:00 UTC,Buy,BTC,0.50000000,USD,95000.00,47500.00,47550.00,50.00,,,\r\n\
32cb-recv,2025-08-01 12:00:00 UTC,Receive,BTC,0.20000000,USD,110000.00,,,,,,\r\n";
33
34/// J4 corpus: two River staking-income deposits in 2025 (FMV resolved from the bundled dataset).
35pub const J4_CSV: &str =
36    "Date,Sent Amount,Sent Currency,Received Amount,Received Currency,Fee Amount,Tag\r\n\
372025-04-15 12:00:00 UTC,,,0.05000000,BTC,,income\r\n\
382025-05-20 12:00:00 UTC,,,0.03000000,BTC,,income\r\n";
39
40/// J5 corpus: an LT lot + a higher-basis ST lot + a 2025 sell — a genuine changed-selection scenario
41/// (HIFO ≠ FIFO) so the optimizer has a tax-saving pick to propose.
42pub const J5_CSV: &str = "\r\nTransactions\r\nUser,00000000-0000-0000-0000-000000000000\r\n\
43ID,Timestamp,Transaction Type,Asset,Quantity Transacted,Price Currency,Price at Transaction,Subtotal,Total (inclusive of fees and/or spread),Fees and/or Spread,Notes,Sender Address,Recipient Address\r\n\
44opt-buy-lt,2023-01-01 12:00:00 UTC,Buy,BTC,1.00000000,USD,30000.00,30000.00,30000.00,0.00,,,\r\n\
45opt-buy-st,2025-01-02 12:00:00 UTC,Buy,BTC,1.00000000,USD,80000.00,80000.00,80000.00,0.00,,,\r\n\
46opt-sell,2025-06-01 12:00:00 UTC,Sell,BTC,1.00000000,USD,50000.00,50000.00,50000.00,0.00,,,\r\n";
47
48/// J6 River corpus: one small 2024 business mining-income deposit (FMV from the bundled dataset).
49/// Kept modest deliberately: a corpus editor who enlarges the sale, income, or donation must keep the
50/// household on the computable side of **Form 6251 line 7 <= line 10** (Who Must File condition 1), which
51/// is what refuses a return now — NOT the screening worksheet, which since v0.14.0 gates nothing.
52pub const J6_RIVER_CSV: &str =
53    "Date,Sent Amount,Sent Currency,Received Amount,Received Currency,Fee Amount,Tag\r\n\
542024-03-15 12:00:00 UTC,,,0.05000000,BTC,,income\r\n";
55
56/// J6 Coinbase corpus: a cheap 2020 long-term lot, a small 2024 sale, and a 2024 charitable Send of 0.1
57/// BTC (§170(e) donation ⇒ Form 8283). NOTE: HIFO reconstruction draws the 2024 sale from the higher-basis
58/// 2024 mining lot (not this 2020 lot) ⇒ it is SHORT-term (Schedule D Part I). Amounts kept small so the
59/// return stays clear of Form 6251's Who-Must-File test (line 7 <= line 10).
60pub const J6_COINBASE_CSV: &str = "\r\nTransactions\r\nUser,00000000-0000-0000-0000-000000000000\r\n\
61ID,Timestamp,Transaction Type,Asset,Quantity Transacted,Price Currency,Price at Transaction,Subtotal,Total (inclusive of fees and/or spread),Fees and/or Spread,Notes,Sender Address,Recipient Address\r\n\
62cb-buy,2020-01-01 12:00:00 UTC,Buy,BTC,0.30000000,USD,30000.00,9000.00,9000.00,0.00,,,\r\n\
63cb-sell,2024-05-01 12:00:00 UTC,Sell,BTC,0.05000000,USD,63000.00,3150.00,3130.00,20.00,,,\r\n\
64cb-donate,2024-09-01 12:00:00 UTC,Send,BTC,0.10000000,USD,60000.00,,,,,,bc1qcharity\r\n";
65
66/// The committed full-return ReturnInputs (the `kitchen_sink_household()` oracle, TOML-serialized). J6
67/// imports it via `income import`. The fixture lives in this crate (self-contained), so this is a
68/// SAME-crate `include_str!` — retiring the cross-crate include the xtask copy carried (M-5 exception).
69pub const J6_FULLRETURN_TOML: &str =
70    include_str!("../tests/fixtures/examples/fullreturn_inputs.toml");
71
72/// J7 corpus (UX-P1-7): a single 2024 Coinbase Receive of staking rewards — an unknown-basis inbound the
73/// single-event `classify-inbound-income` command values only from a hand-supplied `--fmv`.
74pub const J7_CSV: &str = "\r\nTransactions\r\nUser,00000000-0000-0000-0000-000000000000\r\n\
75ID,Timestamp,Transaction Type,Asset,Quantity Transacted,Price Currency,Price at Transaction,Subtotal,Total (inclusive of fees and/or spread),Fees and/or Spread,Notes,Sender Address,Recipient Address\r\n\
76cb-recv,2024-06-15 12:00:00 UTC,Receive,BTC,0.05000000,USD,,,,,,,\r\n";
77
78/// J8 River corpus (UX-P1-8): a buy (to give the coins a basis) then a Withdrawal OUT of 0.10 BTC — the
79/// out-leg of a cross-exchange self-transfer whose in-leg lands on Coinbase below.
80pub const J8_RIVER_CSV: &str =
81    "Date,Sent Amount,Sent Currency,Received Amount,Received Currency,Fee Amount,Tag\r\n\
822025-01-05 12:00:00 UTC,4000.00,USD,0.10000000,BTC,,buy\r\n\
832025-03-10 12:00:00 UTC,0.10000000,BTC,,,,withdrawal\r\n";
84
85/// J8 Coinbase corpus (UX-P1-8): the matching inbound Receive of 0.10 BTC — the SAME coins landing at a
86/// second exchange, so the pair is a cross-wallet RELOCATE (not a same-wallet DROP).
87pub const J8_COINBASE_CSV: &str = "\r\nTransactions\r\nUser,00000000-0000-0000-0000-000000000000\r\n\
88ID,Timestamp,Transaction Type,Asset,Quantity Transacted,Price Currency,Price at Transaction,Subtotal,Total (inclusive of fees and/or spread),Fees and/or Spread,Notes,Sender Address,Recipient Address\r\n\
89cb-recv,2025-03-10 12:00:00 UTC,Receive,BTC,0.10000000,USD,,,,,,,\r\n";
90
91/// J9 corpus (UX-P1-10): a cheap 2023 long-term lot (0.60) + a pricier 2024 lot (0.40), then a 2025 sale of
92/// only 0.50 — less than the combined holdings, so specific-lot identification is meaningful.
93pub const J9_CSV: &str = "\r\nTransactions\r\nUser,00000000-0000-0000-0000-000000000000\r\n\
94ID,Timestamp,Transaction Type,Asset,Quantity Transacted,Price Currency,Price at Transaction,Subtotal,Total (inclusive of fees and/or spread),Fees and/or Spread,Notes,Sender Address,Recipient Address\r\n\
95lot-a,2023-01-01 12:00:00 UTC,Buy,BTC,0.60000000,USD,25000.00,15000.00,15000.00,0.00,,,\r\n\
96lot-b,2024-01-02 12:00:00 UTC,Buy,BTC,0.40000000,USD,60000.00,24000.00,24000.00,0.00,,,\r\n\
97sale,2025-06-01 12:00:00 UTC,Sell,BTC,0.50000000,USD,47500.00,47500.00,47500.00,0.00,,,\r\n";
98
99/// J10 corpus — a household the AMT SCREEN flags but that owes no alternative minimum tax. One 2021 lot
100/// sold in 2024 for a $200,000 long-term gain; paired with `J10_FULLRETURN_TOML`'s $400,000 of wages it
101/// reaches $600,000 of AGI, which trips the Form 6251 screening worksheet (its line 11 = $466,700, over
102/// the $232,600 breakpoint). Before Form 6251 was computed, btctax REFUSED this return outright and
103/// wrote no forms at all.
104pub const J10_CSV: &str = "\r\nTransactions\r\nUser,00000000-0000-0000-0000-000000000000\r\n\
105ID,Timestamp,Transaction Type,Asset,Quantity Transacted,Price Currency,Price at Transaction,Subtotal,Total (inclusive of fees and/or spread),Fees and/or Spread,Notes,Sender Address,Recipient Address\r\n\
106j10-buy,2021-03-15 12:00:00 UTC,Buy,BTC,2.00000000,USD,25000.00,50000.00,50000.00,0.00,,,\r\n\
107j10-sell,2024-09-10 12:00:00 UTC,Sell,BTC,2.00000000,USD,125000.00,250000.00,250000.00,0.00,,,\r\n";
108
109/// J10's non-crypto side: MFJ, $400,000 of wages, the standard deduction, no Schedule A. Deliberately
110/// carries NO mortgage interest and NO capital-loss carryforward, so neither Form 6251 declaration
111/// (lines 3 and 2k) is live — the journey demonstrates the AMT computation, not the questions.
112pub const J10_FULLRETURN_TOML: &str = r#"filing_status = "Mfj"
113foreign_accounts = false
114foreign_trust = false
115foreign_country_names = ""
116dual_status_alien = false
117itemize_election = "auto"
118charitable_carryover_in = []
119
120[capital_loss_carryforward_in]
121long = "0"
122short = "0"
123
124[header]
125address_street = "88 Larkspur Way"
126address_city = "Boulder"
127address_state = "CO"
128address_zip = "80301"
129can_be_claimed_as_dependent_taxpayer = false
130can_be_claimed_as_dependent_spouse = false
131presidential_fund_taxpayer = false
132presidential_fund_spouse = false
133
134[header.taxpayer]
135first_name = "Nina"
136last_name = "Okafor"
137occupation = "Anesthesiologist"
138ssn = "123-45-6789"
139
140[header.spouse]
141first_name = "Tomas"
142last_name = "Okafor"
143occupation = "Structural Engineer"
144ssn = "987-65-4321"
145
146[payments]
147estimated_tax_payments = "0"
148extension_payment = "0"
149other_withholding = "0"
150
151[qbi]
152reit_ptp_carryforward_in = "0"
153reit_ptp_carryforward_in_provenance = "user"
154
155[sch1]
156hsa_activity = false
157ira_deduction_claimed = "0"
158state_refund_taxable = "0"
159student_loan_interest_paid = "0"
160
161[[w2s]]
162owner = "taxpayer"
163employer = "Front Range Health"
164box1_wages = "400000"
165box2_fed_withheld = "96000"
166box3_ss_wages = "168600"
167box4_ss_withheld = "10453.20"
168box5_medicare_wages = "400000"
169box6_medicare_withheld = "7150"
170box7_ss_tips = "0"
171box8_allocated_tips = "0"
172box10_dependent_care = "0"
173box12 = []
174box17_state_tax_withheld = "0"
175box19_local_tax = "0"
176"#;
177
178// ── Per-journey fixtures ───────────────────────────────────────────────────────────────────────────
179
180/// One journey's synthetic input: the named corpus files a driver writes to a tempdir before `import`.
181/// The SAME fixture seeds both walkthrough capture halves (the editor emit test drives its flows; the
182/// viewer emit test re-seeds this corpus and replays the equivalent decisions via `btctax-cli`), so the
183/// two halves of a journey converge by construction on one source of truth (walkthrough spec §4.2).
184pub struct JourneyFixture {
185    /// Short journey id, e.g. `"j8"`.
186    pub name: &'static str,
187    /// `(filename, CRLF content)` pairs written to the tempdir before ingest.
188    pub corpus: &'static [(&'static str, &'static str)],
189}
190
191/// The nine journey fixtures, keyed by the same corpus filenames the examples journeys use.
192pub fn j1() -> JourneyFixture {
193    JourneyFixture {
194        name: "j1",
195        corpus: &[("coinbase.csv", J1_CSV)],
196    }
197}
198pub fn j2() -> JourneyFixture {
199    JourneyFixture {
200        name: "j2",
201        corpus: &[("coinbase.csv", J2_CSV)],
202    }
203}
204pub fn j3() -> JourneyFixture {
205    JourneyFixture {
206        name: "j3",
207        corpus: &[("coinbase.csv", J3_CSV)],
208    }
209}
210pub fn j4() -> JourneyFixture {
211    JourneyFixture {
212        name: "j4",
213        corpus: &[("river.csv", J4_CSV)],
214    }
215}
216pub fn j5() -> JourneyFixture {
217    JourneyFixture {
218        name: "j5",
219        corpus: &[("coinbase.csv", J5_CSV)],
220    }
221}
222pub fn j6() -> JourneyFixture {
223    JourneyFixture {
224        name: "j6",
225        corpus: &[
226            ("river.csv", J6_RIVER_CSV),
227            ("coinbase.csv", J6_COINBASE_CSV),
228        ],
229    }
230}
231pub fn j7() -> JourneyFixture {
232    JourneyFixture {
233        name: "j7",
234        corpus: &[("coinbase.csv", J7_CSV)],
235    }
236}
237/// J8 — the PoC journey: a cross-exchange self-transfer (River Withdrawal out → Coinbase Receive in).
238pub fn j8() -> JourneyFixture {
239    JourneyFixture {
240        name: "j8",
241        corpus: &[
242            ("river.csv", J8_RIVER_CSV),
243            ("coinbase.csv", J8_COINBASE_CSV),
244        ],
245    }
246}
247pub fn j9() -> JourneyFixture {
248    JourneyFixture {
249        name: "j9",
250        corpus: &[("coinbase.csv", J9_CSV)],
251    }
252}
253
254// ── Seeders (shared so a journey's editor + viewer walkthrough halves converge on ONE vault state) ──
255// These live HERE (btctax-cli) rather than in the TUI crates because the read-only viewer (btctax-tui)
256// forbids the write-path `cmd::`/`save(` tokens even in its tests (its e10 source gate) — so the viewer
257// half calls a testonly seeder to reach the post-decision state, then only Session::open + build_snapshot
258// (read-only) + render.
259
260/// Init a vault at `dir/vault.pgp` and import the journey `fx`'s corpus via the REAL adapter ingest.
261pub fn seed_journey(
262    dir: &std::path::Path,
263    pp: &btctax_store::Passphrase,
264    fx: &JourneyFixture,
265) -> std::path::PathBuf {
266    let vault = dir.join("vault.pgp");
267    let key = dir.join("key.asc");
268    crate::cmd::init::run(&vault, pp, &key).unwrap();
269    let mut files: Vec<std::path::PathBuf> = Vec::new();
270    for (name, content) in fx.corpus {
271        let p = dir.join(name);
272        std::fs::write(&p, content).unwrap();
273        files.push(p);
274    }
275    crate::cmd::import::run(&vault, pp, &files).unwrap();
276    vault
277}
278
279/// Seed J8 and apply its RELOCATE self-transfer (river out → coinbase in) — the post-match state the
280/// walkthrough's VIEWER half renders (BALANCED). Refs are J8's deterministic event refs; RELOCATE routes
281/// to `link_transfer` (out → in), the same decision the editor's confirm modal makes.
282pub fn seed_j8_relocated(
283    dir: &std::path::Path,
284    pp: &btctax_store::Passphrase,
285    now: time::OffsetDateTime,
286) -> std::path::PathBuf {
287    use btctax_core::event::TransferTarget;
288    let vault = seed_journey(dir, pp, &j8());
289    let in_id = crate::eventref::parse_event_id("import|coinbase|in|cb-recv").unwrap();
290    crate::cmd::reconcile::link_transfer(
291        &vault,
292        pp,
293        "import|river|out|1741608000000|withdrawal|10000000#0",
294        TransferTarget::InEvent(in_id),
295        now,
296    )
297    .unwrap();
298    vault
299}
300
301/// Seed J1 (single buyer: one Buy + one partial Sell, no transfers → no reconciliation) and set the 2025
302/// tax profile its walkthrough VIEWER needs for the Tax tab to compute — single filer, $100k ordinary
303/// income, mirroring the J1 worked example's `tax-profile` step. The viewer half then only opens + reads.
304pub fn seed_j1_with_profile(
305    dir: &std::path::Path,
306    pp: &btctax_store::Passphrase,
307) -> std::path::PathBuf {
308    use btctax_core::{Carryforward, FilingStatus, TaxProfile};
309    use rust_decimal::Decimal;
310    let vault = seed_journey(dir, pp, &j1());
311    let z = Decimal::ZERO;
312    let profile = TaxProfile {
313        filing_status: FilingStatus::Single,
314        ordinary_taxable_income: Decimal::from(100_000),
315        magi_excluding_crypto: Decimal::from(100_000),
316        qualified_dividends_and_other_pref_income: z,
317        other_net_capital_gain: z,
318        capital_loss_carryforward_in: Carryforward { short: z, long: z },
319        w2_ss_wages: z,
320        w2_medicare_wages: z,
321        schedule_c_expenses: z,
322    };
323    crate::cmd::tax::set_profile(&vault, pp, 2025, profile, false).unwrap();
324    vault
325}
326
327/// Seed J4 (two River staking receipts) with a 2025 profile AND both receipts reclassified as a trade or
328/// business (`--business true --kind staking`) — the post-reclassify state the walkthrough's VIEWER half
329/// renders (Income now on Schedule C/SE; Tax shows the self-employment tax). The editor half drives the
330/// reclassify itself, so it seeds only the raw import (`seed_journey(&j4())`). Made-date pinned for
331/// determinism. Refs embed the ms-timestamp of each receipt (not wall-clock).
332pub fn seed_j4_reclassified(
333    dir: &std::path::Path,
334    pp: &btctax_store::Passphrase,
335    now: time::OffsetDateTime,
336) -> std::path::PathBuf {
337    use btctax_core::{Carryforward, FilingStatus, IncomeKind, TaxProfile};
338    use rust_decimal::Decimal;
339    let vault = seed_journey(dir, pp, &j4());
340    let z = Decimal::ZERO;
341    let profile = TaxProfile {
342        filing_status: FilingStatus::Single,
343        ordinary_taxable_income: Decimal::from(100_000),
344        magi_excluding_crypto: Decimal::from(100_000),
345        qualified_dividends_and_other_pref_income: z,
346        other_net_capital_gain: z,
347        capital_loss_carryforward_in: Carryforward { short: z, long: z },
348        w2_ss_wages: z,
349        w2_medicare_wages: z,
350        schedule_c_expenses: z,
351    };
352    crate::cmd::tax::set_profile(&vault, pp, 2025, profile, false).unwrap();
353    // Made-date threaded from the caller so it matches the depicted editor session's pinned clock (J4
354    // review Minor 2 — the J8 `seed_j8_relocated(…, now)` pattern), not a divergent hardcoded literal.
355    for r in [
356        "import|river|in|1744718400000|income|5000000#0",
357        "import|river|in|1747742400000|income|3000000#0",
358    ] {
359        crate::cmd::reconcile::reclassify_income(
360            &vault,
361            pp,
362            r,
363            true,
364            Some(IncomeKind::Staking),
365            now,
366        )
367        .unwrap();
368    }
369    vault
370}
371
372/// Seed J7 (a single off-exchange Receive with no market price → a hard unknown-basis blocker) classified
373/// as staking INCOME with a hand-supplied FMV ($3,300), plus a 2024 profile — the post-classify state the
374/// walkthrough's VIEWER renders (Income + Tax). The editor half seeds only the raw import and drives the
375/// classify. Made-date threaded from the caller (matches the depicted editor session's clock).
376pub fn seed_j7_income(
377    dir: &std::path::Path,
378    pp: &btctax_store::Passphrase,
379    now: time::OffsetDateTime,
380) -> std::path::PathBuf {
381    use btctax_core::{Carryforward, FilingStatus, InboundClass, IncomeKind, TaxProfile};
382    use rust_decimal::Decimal;
383    let vault = seed_journey(dir, pp, &j7());
384    let z = Decimal::ZERO;
385    let profile = TaxProfile {
386        filing_status: FilingStatus::Single,
387        ordinary_taxable_income: Decimal::from(100_000),
388        magi_excluding_crypto: Decimal::from(100_000),
389        qualified_dividends_and_other_pref_income: z,
390        other_net_capital_gain: z,
391        capital_loss_carryforward_in: Carryforward { short: z, long: z },
392        w2_ss_wages: z,
393        w2_medicare_wages: z,
394        schedule_c_expenses: z,
395    };
396    crate::cmd::tax::set_profile(&vault, pp, 2024, profile, false).unwrap();
397    let fmv: Decimal = "3300.00".parse().unwrap();
398    crate::cmd::reconcile::classify_inbound(
399        &vault,
400        pp,
401        "import|coinbase|in|cb-recv",
402        InboundClass::Income {
403            kind: IncomeKind::Staking,
404            fmv: Some(fmv),
405            business: false,
406        },
407        now,
408    )
409    .unwrap();
410    vault
411}
412
413/// Seed J3 (an unknown-basis inbound Receive → a hard blocker) classified as a SELF-TRANSFER of the
414/// filer's own coins returning — non-taxable, carrying a supplied basis ($19,000) and acquisition date
415/// (2024-11-01, so the holding period runs from then). The post-classify state the VIEWER renders (Holdings).
416/// The editor half seeds only the raw import and drives the classify. Made-date threaded from the caller.
417pub fn seed_j3_self_transfer(
418    dir: &std::path::Path,
419    pp: &btctax_store::Passphrase,
420    now: time::OffsetDateTime,
421) -> std::path::PathBuf {
422    use btctax_core::InboundClass;
423    use rust_decimal::Decimal;
424    let vault = seed_journey(dir, pp, &j3());
425    let basis: Decimal = "19000.00".parse().unwrap();
426    crate::cmd::reconcile::classify_inbound(
427        &vault,
428        pp,
429        "import|coinbase|in|cb-recv",
430        InboundClass::SelfTransferMine {
431            basis: Some(basis),
432            acquired_at: Some(time::macros::date!(2024 - 11 - 01)),
433        },
434        now,
435    )
436    .unwrap();
437    vault
438}
439
440/// Seed J9 (a cheap 2023 LT lot 0.60 + a pricier 2024 lot 0.40, then a 2025 sale of 0.50 — less than the
441/// combined holdings, so specific-lot identification is meaningful) with the whole 0.50 IDENTIFIED against the
442/// cheap long-term `lot-a` (a deliberate per-disposal specific identification). The post-selection state
443/// the walkthrough's VIEWER renders (Disposals drawing from lot-a; Compliance now satisfied). The editor
444/// half seeds only the raw import and drives the select-lots flow. Made-date threaded from the caller
445/// (the identification is contemporaneous — before/at the sale).
446pub fn seed_j9_selected(
447    dir: &std::path::Path,
448    pp: &btctax_store::Passphrase,
449    now: time::OffsetDateTime,
450) -> std::path::PathBuf {
451    let vault = seed_journey(dir, pp, &j9());
452    let pick = crate::eventref::parse_lot_pick("import|coinbase|trade|lot-a#0:50000000").unwrap();
453    crate::cmd::reconcile::select_lots(&vault, pp, "import|coinbase|trade|sale", vec![pick], now)
454        .unwrap();
455    vault
456}
457
458/// Seed J2 with the outbound Send reclassified as a §170(e) charitable DONATION (FMV $217,992.34 = 2 BTC
459/// × the contribution-date close, donee "Habitat for Humanity") — but WITHOUT the Form 8283 appraiser/donee
460/// details yet. This is the state the walkthrough's `d` (set-donation-details) EDITOR frame drives from
461/// (the details form still empty). Made-date threaded from the caller.
462pub fn seed_j2_reclassified(
463    dir: &std::path::Path,
464    pp: &btctax_store::Passphrase,
465    now: time::OffsetDateTime,
466) -> std::path::PathBuf {
467    use btctax_core::OutflowClass;
468    let vault = seed_journey(dir, pp, &j2());
469    let principal: rust_decimal::Decimal = "217992.34".parse().unwrap();
470    crate::cmd::reconcile::reclassify_outflow(
471        &vault,
472        pp,
473        "import|coinbase|out|cb-donate",
474        OutflowClass::Donate {
475            appraisal_required: false,
476        },
477        principal,
478        None,
479        Some("Habitat for Humanity".to_string()),
480        now,
481    )
482    .unwrap();
483    vault
484}
485
486/// Seed J2's fully post-decision state — the outbound reclassified as a donation, the Form 8283
487/// appraiser/donee details recorded (Section B complete), and a 2025 profile so the viewer's Tax tab
488/// computes the charitable-deduction line. The walkthrough's VIEWER renders Forms (the two 8283 rows) +
489/// Tax. Made-date threaded from the caller.
490pub fn seed_j2_donated(
491    dir: &std::path::Path,
492    pp: &btctax_store::Passphrase,
493    now: time::OffsetDateTime,
494) -> std::path::PathBuf {
495    use btctax_core::{Carryforward, DonationDetails, FilingStatus, TaxProfile};
496    use rust_decimal::Decimal;
497    let vault = seed_j2_reclassified(dir, pp, now);
498    let z = Decimal::ZERO;
499    let profile = TaxProfile {
500        filing_status: FilingStatus::Single,
501        ordinary_taxable_income: Decimal::from(100_000),
502        magi_excluding_crypto: Decimal::from(100_000),
503        qualified_dividends_and_other_pref_income: z,
504        other_net_capital_gain: z,
505        capital_loss_carryforward_in: Carryforward { short: z, long: z },
506        w2_ss_wages: z,
507        w2_medicare_wages: z,
508        schedule_c_expenses: z,
509    };
510    crate::cmd::tax::set_profile(&vault, pp, 2025, profile, false).unwrap();
511    crate::cmd::reconcile::set_donation_details(
512        &vault,
513        pp,
514        "import|coinbase|out|cb-donate",
515        DonationDetails {
516            donee_name: "Habitat for Humanity".into(),
517            donee_address: None,
518            donee_ein: Some("98-7654321".into()),
519            appraiser_name: "Jane Appraiser".into(),
520            appraiser_address: None,
521            appraiser_tin: Some("12-3456789".into()),
522            appraiser_ptin: None,
523            appraiser_qualifications: Some("ASA-accredited digital-asset appraiser, 8 yrs".into()),
524            appraisal_date: Some(time::macros::date!(2025 - 09 - 15)),
525            fmv_method_override: None,
526        },
527    )
528    .unwrap();
529    vault
530}
531
532/// Seed J5 (an LT lot $30k + a higher-basis ST lot $80k + a 2025 sale $50k) with a 2025 profile AND a
533/// standing FIFO election — but NOT the optimizer's accept. This is the state the walkthrough's `z`
534/// (optimize-accept) EDITOR frames drive from: the optimizer proposes swapping to the ST lot to realize a
535/// loss. The election made-date is hardcoded 2025-01-01 (the engine blocks effective_from before the
536/// made-date, so a later threaded `now` would trip MethodElectionBackdated).
537pub fn seed_j5_elected(dir: &std::path::Path, pp: &btctax_store::Passphrase) -> std::path::PathBuf {
538    use btctax_core::{Carryforward, FilingStatus, LotMethod, TaxProfile};
539    use rust_decimal::Decimal;
540    let vault = seed_journey(dir, pp, &j5());
541    let z = Decimal::ZERO;
542    let profile = TaxProfile {
543        filing_status: FilingStatus::Single,
544        ordinary_taxable_income: Decimal::from(100_000),
545        magi_excluding_crypto: Decimal::from(100_000),
546        qualified_dividends_and_other_pref_income: z,
547        other_net_capital_gain: z,
548        capital_loss_carryforward_in: Carryforward { short: z, long: z },
549        w2_ss_wages: z,
550        w2_medicare_wages: z,
551        schedule_c_expenses: z,
552    };
553    crate::cmd::tax::set_profile(&vault, pp, 2025, profile, false).unwrap();
554    crate::cmd::reconcile::set_forward_method(
555        &vault,
556        pp,
557        LotMethod::Fifo,
558        None,
559        Some(time::macros::date!(2025 - 01 - 01)),
560        time::macros::datetime!(2025 - 01 - 01 00:00:00 UTC),
561    )
562    .unwrap();
563    vault
564}
565
566/// Seed J5's post-accept state — the FIFO election PLUS the optimizer's proposed lot pick accepted (the
567/// whole sale moved onto the short-term `opt-buy-st` lot to realize a loss). The walkthrough's VIEWER
568/// renders the resulting Disposals (ST loss) + Tax (§1211 deduction + carryforward). `now` is the accept
569/// made-date — MUST be ≤ the 2025-06-01 sale so the selection is Contemporaneous; the caller passes the
570/// same instant the editor frames pin.
571pub fn seed_j5_optimized(
572    dir: &std::path::Path,
573    pp: &btctax_store::Passphrase,
574    now: time::OffsetDateTime,
575) -> std::path::PathBuf {
576    let vault = seed_j5_elected(dir, pp);
577    crate::cmd::optimize::accept(&vault, pp, 2025, None, None, now).unwrap();
578    vault
579}
580
581/// Seed J6 — a COMPLETE Form 1040 (the kitchen-sink MFJ household). Combines the crypto reconciliation
582/// (mining income → a trade or business; the 0.10 BTC Send → a §170(e) donation with Form 8283 details)
583/// with the non-crypto household imported from the committed `kitchen_sink_household()` TOML. This is the
584/// state BOTH walkthrough halves render: the editor half opens the tax-inputs authoring form (`T`) over
585/// these committed inputs; the viewer shows Forms (the crypto forms) + Tax (the merged MFJ return). No raw
586/// tax-profile — the full-return inputs take precedence and `build_snapshot` derives the MFJ profile from
587/// them. Made-date threaded from the caller.
588pub fn seed_j6_full(
589    dir: &std::path::Path,
590    pp: &btctax_store::Passphrase,
591    now: time::OffsetDateTime,
592) -> std::path::PathBuf {
593    use btctax_core::{DonationDetails, IncomeKind, OutflowClass};
594    let vault = seed_journey(dir, pp, &j6());
595    // 1. mining income → a trade or business (Schedule C/SE).
596    crate::cmd::reconcile::reclassify_income(
597        &vault,
598        pp,
599        "import|river|in|1710504000000|income|5000000#0",
600        true,
601        Some(IncomeKind::Mining),
602        now,
603    )
604    .unwrap();
605    // 2. the 0.10 BTC Send → a §170(e) charitable donation, FMV $6,000.
606    let fmv: rust_decimal::Decimal = "6000.00".parse().unwrap();
607    crate::cmd::reconcile::reclassify_outflow(
608        &vault,
609        pp,
610        "import|coinbase|out|cb-donate",
611        OutflowClass::Donate {
612            appraisal_required: false,
613        },
614        fmv,
615        None,
616        Some("Habitat for Humanity".to_string()),
617        now,
618    )
619    .unwrap();
620    // 3. the Form 8283 Section-B appraiser/donee details.
621    crate::cmd::reconcile::set_donation_details(
622        &vault,
623        pp,
624        "import|coinbase|out|cb-donate",
625        DonationDetails {
626            donee_name: "Habitat for Humanity".into(),
627            donee_address: None,
628            donee_ein: Some("98-7654321".into()),
629            appraiser_name: "Jane Appraiser".into(),
630            appraiser_address: None,
631            appraiser_tin: Some("12-3456789".into()),
632            appraiser_ptin: None,
633            appraiser_qualifications: Some("ASA-accredited digital-asset appraiser, 8 yrs".into()),
634            appraisal_date: Some(time::macros::date!(2024 - 09 - 15)),
635            fmv_method_override: None,
636        },
637    )
638    .unwrap();
639    // 4. the non-crypto household — a programmatic `income import` (reads a file, so materialize the TOML).
640    let toml = dir.join("fullreturn.toml");
641    std::fs::write(&toml, J6_FULLRETURN_TOML).unwrap();
642    crate::cmd::tax::import_return_inputs(&vault, pp, 2024, &toml).unwrap();
643    vault
644}