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: the kitchen-sink household clears the 2024 Form-6251 AMT-screen worksheet by
50/// only a thin margin — a corpus editor who enlarges the sale, income, or donation must keep the household
51/// on the computable side of that screen.
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 under the AMT screen.
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// ── Per-journey fixtures ───────────────────────────────────────────────────────────────────────────
100
101/// One journey's synthetic input: the named corpus files a driver writes to a tempdir before `import`.
102/// The SAME fixture seeds both walkthrough capture halves (the editor emit test drives its flows; the
103/// viewer emit test re-seeds this corpus and replays the equivalent decisions via `btctax-cli`), so the
104/// two halves of a journey converge by construction on one source of truth (walkthrough spec §4.2).
105pub struct JourneyFixture {
106    /// Short journey id, e.g. `"j8"`.
107    pub name: &'static str,
108    /// `(filename, CRLF content)` pairs written to the tempdir before ingest.
109    pub corpus: &'static [(&'static str, &'static str)],
110}
111
112/// The nine journey fixtures, keyed by the same corpus filenames the examples journeys use.
113pub fn j1() -> JourneyFixture {
114    JourneyFixture {
115        name: "j1",
116        corpus: &[("coinbase.csv", J1_CSV)],
117    }
118}
119pub fn j2() -> JourneyFixture {
120    JourneyFixture {
121        name: "j2",
122        corpus: &[("coinbase.csv", J2_CSV)],
123    }
124}
125pub fn j3() -> JourneyFixture {
126    JourneyFixture {
127        name: "j3",
128        corpus: &[("coinbase.csv", J3_CSV)],
129    }
130}
131pub fn j4() -> JourneyFixture {
132    JourneyFixture {
133        name: "j4",
134        corpus: &[("river.csv", J4_CSV)],
135    }
136}
137pub fn j5() -> JourneyFixture {
138    JourneyFixture {
139        name: "j5",
140        corpus: &[("coinbase.csv", J5_CSV)],
141    }
142}
143pub fn j6() -> JourneyFixture {
144    JourneyFixture {
145        name: "j6",
146        corpus: &[
147            ("river.csv", J6_RIVER_CSV),
148            ("coinbase.csv", J6_COINBASE_CSV),
149        ],
150    }
151}
152pub fn j7() -> JourneyFixture {
153    JourneyFixture {
154        name: "j7",
155        corpus: &[("coinbase.csv", J7_CSV)],
156    }
157}
158/// J8 — the PoC journey: a cross-exchange self-transfer (River Withdrawal out → Coinbase Receive in).
159pub fn j8() -> JourneyFixture {
160    JourneyFixture {
161        name: "j8",
162        corpus: &[
163            ("river.csv", J8_RIVER_CSV),
164            ("coinbase.csv", J8_COINBASE_CSV),
165        ],
166    }
167}
168pub fn j9() -> JourneyFixture {
169    JourneyFixture {
170        name: "j9",
171        corpus: &[("coinbase.csv", J9_CSV)],
172    }
173}
174
175// ── Seeders (shared so a journey's editor + viewer walkthrough halves converge on ONE vault state) ──
176// These live HERE (btctax-cli) rather than in the TUI crates because the read-only viewer (btctax-tui)
177// forbids the write-path `cmd::`/`save(` tokens even in its tests (its e10 source gate) — so the viewer
178// half calls a testonly seeder to reach the post-decision state, then only Session::open + build_snapshot
179// (read-only) + render.
180
181/// Init a vault at `dir/vault.pgp` and import the journey `fx`'s corpus via the REAL adapter ingest.
182pub fn seed_journey(
183    dir: &std::path::Path,
184    pp: &btctax_store::Passphrase,
185    fx: &JourneyFixture,
186) -> std::path::PathBuf {
187    let vault = dir.join("vault.pgp");
188    let key = dir.join("key.asc");
189    crate::cmd::init::run(&vault, pp, &key).unwrap();
190    let mut files: Vec<std::path::PathBuf> = Vec::new();
191    for (name, content) in fx.corpus {
192        let p = dir.join(name);
193        std::fs::write(&p, content).unwrap();
194        files.push(p);
195    }
196    crate::cmd::import::run(&vault, pp, &files).unwrap();
197    vault
198}
199
200/// Seed J8 and apply its RELOCATE self-transfer (river out → coinbase in) — the post-match state the
201/// walkthrough's VIEWER half renders (BALANCED). Refs are J8's deterministic event refs; RELOCATE routes
202/// to `link_transfer` (out → in), the same decision the editor's confirm modal makes.
203pub fn seed_j8_relocated(
204    dir: &std::path::Path,
205    pp: &btctax_store::Passphrase,
206    now: time::OffsetDateTime,
207) -> std::path::PathBuf {
208    use btctax_core::event::TransferTarget;
209    let vault = seed_journey(dir, pp, &j8());
210    let in_id = crate::eventref::parse_event_id("import|coinbase|in|cb-recv").unwrap();
211    crate::cmd::reconcile::link_transfer(
212        &vault,
213        pp,
214        "import|river|out|1741608000000|withdrawal|10000000#0",
215        TransferTarget::InEvent(in_id),
216        now,
217    )
218    .unwrap();
219    vault
220}
221
222/// Seed J1 (single buyer: one Buy + one partial Sell, no transfers → no reconciliation) and set the 2025
223/// tax profile its walkthrough VIEWER needs for the Tax tab to compute — single filer, $100k ordinary
224/// income, mirroring the J1 worked example's `tax-profile` step. The viewer half then only opens + reads.
225pub fn seed_j1_with_profile(
226    dir: &std::path::Path,
227    pp: &btctax_store::Passphrase,
228) -> std::path::PathBuf {
229    use btctax_core::{Carryforward, FilingStatus, TaxProfile};
230    use rust_decimal::Decimal;
231    let vault = seed_journey(dir, pp, &j1());
232    let z = Decimal::ZERO;
233    let profile = TaxProfile {
234        filing_status: FilingStatus::Single,
235        ordinary_taxable_income: Decimal::from(100_000),
236        magi_excluding_crypto: Decimal::from(100_000),
237        qualified_dividends_and_other_pref_income: z,
238        other_net_capital_gain: z,
239        capital_loss_carryforward_in: Carryforward { short: z, long: z },
240        w2_ss_wages: z,
241        w2_medicare_wages: z,
242        schedule_c_expenses: z,
243    };
244    crate::cmd::tax::set_profile(&vault, pp, 2025, profile, false).unwrap();
245    vault
246}
247
248/// Seed J4 (two River staking receipts) with a 2025 profile AND both receipts reclassified as a trade or
249/// business (`--business true --kind staking`) — the post-reclassify state the walkthrough's VIEWER half
250/// renders (Income now on Schedule C/SE; Tax shows the self-employment tax). The editor half drives the
251/// reclassify itself, so it seeds only the raw import (`seed_journey(&j4())`). Made-date pinned for
252/// determinism. Refs embed the ms-timestamp of each receipt (not wall-clock).
253pub fn seed_j4_reclassified(
254    dir: &std::path::Path,
255    pp: &btctax_store::Passphrase,
256    now: time::OffsetDateTime,
257) -> std::path::PathBuf {
258    use btctax_core::{Carryforward, FilingStatus, IncomeKind, TaxProfile};
259    use rust_decimal::Decimal;
260    let vault = seed_journey(dir, pp, &j4());
261    let z = Decimal::ZERO;
262    let profile = TaxProfile {
263        filing_status: FilingStatus::Single,
264        ordinary_taxable_income: Decimal::from(100_000),
265        magi_excluding_crypto: Decimal::from(100_000),
266        qualified_dividends_and_other_pref_income: z,
267        other_net_capital_gain: z,
268        capital_loss_carryforward_in: Carryforward { short: z, long: z },
269        w2_ss_wages: z,
270        w2_medicare_wages: z,
271        schedule_c_expenses: z,
272    };
273    crate::cmd::tax::set_profile(&vault, pp, 2025, profile, false).unwrap();
274    // Made-date threaded from the caller so it matches the depicted editor session's pinned clock (J4
275    // review Minor 2 — the J8 `seed_j8_relocated(…, now)` pattern), not a divergent hardcoded literal.
276    for r in [
277        "import|river|in|1744718400000|income|5000000#0",
278        "import|river|in|1747742400000|income|3000000#0",
279    ] {
280        crate::cmd::reconcile::reclassify_income(
281            &vault,
282            pp,
283            r,
284            true,
285            Some(IncomeKind::Staking),
286            now,
287        )
288        .unwrap();
289    }
290    vault
291}
292
293/// Seed J7 (a single off-exchange Receive with no market price → a hard unknown-basis blocker) classified
294/// as staking INCOME with a hand-supplied FMV ($3,300), plus a 2024 profile — the post-classify state the
295/// walkthrough's VIEWER renders (Income + Tax). The editor half seeds only the raw import and drives the
296/// classify. Made-date threaded from the caller (matches the depicted editor session's clock).
297pub fn seed_j7_income(
298    dir: &std::path::Path,
299    pp: &btctax_store::Passphrase,
300    now: time::OffsetDateTime,
301) -> std::path::PathBuf {
302    use btctax_core::{Carryforward, FilingStatus, InboundClass, IncomeKind, TaxProfile};
303    use rust_decimal::Decimal;
304    let vault = seed_journey(dir, pp, &j7());
305    let z = Decimal::ZERO;
306    let profile = TaxProfile {
307        filing_status: FilingStatus::Single,
308        ordinary_taxable_income: Decimal::from(100_000),
309        magi_excluding_crypto: Decimal::from(100_000),
310        qualified_dividends_and_other_pref_income: z,
311        other_net_capital_gain: z,
312        capital_loss_carryforward_in: Carryforward { short: z, long: z },
313        w2_ss_wages: z,
314        w2_medicare_wages: z,
315        schedule_c_expenses: z,
316    };
317    crate::cmd::tax::set_profile(&vault, pp, 2024, profile, false).unwrap();
318    let fmv: Decimal = "3300.00".parse().unwrap();
319    crate::cmd::reconcile::classify_inbound(
320        &vault,
321        pp,
322        "import|coinbase|in|cb-recv",
323        InboundClass::Income {
324            kind: IncomeKind::Staking,
325            fmv: Some(fmv),
326            business: false,
327        },
328        now,
329    )
330    .unwrap();
331    vault
332}
333
334/// Seed J3 (an unknown-basis inbound Receive → a hard blocker) classified as a SELF-TRANSFER of the
335/// filer's own coins returning — non-taxable, carrying a supplied basis ($19,000) and acquisition date
336/// (2024-11-01, so the holding period runs from then). The post-classify state the VIEWER renders (Holdings).
337/// The editor half seeds only the raw import and drives the classify. Made-date threaded from the caller.
338pub fn seed_j3_self_transfer(
339    dir: &std::path::Path,
340    pp: &btctax_store::Passphrase,
341    now: time::OffsetDateTime,
342) -> std::path::PathBuf {
343    use btctax_core::InboundClass;
344    use rust_decimal::Decimal;
345    let vault = seed_journey(dir, pp, &j3());
346    let basis: Decimal = "19000.00".parse().unwrap();
347    crate::cmd::reconcile::classify_inbound(
348        &vault,
349        pp,
350        "import|coinbase|in|cb-recv",
351        InboundClass::SelfTransferMine {
352            basis: Some(basis),
353            acquired_at: Some(time::macros::date!(2024 - 11 - 01)),
354        },
355        now,
356    )
357    .unwrap();
358    vault
359}
360
361/// 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
362/// combined holdings, so specific-lot identification is meaningful) with the whole 0.50 IDENTIFIED against the
363/// cheap long-term `lot-a` (a deliberate per-disposal specific identification). The post-selection state
364/// the walkthrough's VIEWER renders (Disposals drawing from lot-a; Compliance now satisfied). The editor
365/// half seeds only the raw import and drives the select-lots flow. Made-date threaded from the caller
366/// (the identification is contemporaneous — before/at the sale).
367pub fn seed_j9_selected(
368    dir: &std::path::Path,
369    pp: &btctax_store::Passphrase,
370    now: time::OffsetDateTime,
371) -> std::path::PathBuf {
372    let vault = seed_journey(dir, pp, &j9());
373    let pick = crate::eventref::parse_lot_pick("import|coinbase|trade|lot-a#0:50000000").unwrap();
374    crate::cmd::reconcile::select_lots(&vault, pp, "import|coinbase|trade|sale", vec![pick], now)
375        .unwrap();
376    vault
377}
378
379/// Seed J2 with the outbound Send reclassified as a §170(e) charitable DONATION (FMV $217,992.34 = 2 BTC
380/// × the contribution-date close, donee "Habitat for Humanity") — but WITHOUT the Form 8283 appraiser/donee
381/// details yet. This is the state the walkthrough's `d` (set-donation-details) EDITOR frame drives from
382/// (the details form still empty). Made-date threaded from the caller.
383pub fn seed_j2_reclassified(
384    dir: &std::path::Path,
385    pp: &btctax_store::Passphrase,
386    now: time::OffsetDateTime,
387) -> std::path::PathBuf {
388    use btctax_core::OutflowClass;
389    let vault = seed_journey(dir, pp, &j2());
390    let principal: rust_decimal::Decimal = "217992.34".parse().unwrap();
391    crate::cmd::reconcile::reclassify_outflow(
392        &vault,
393        pp,
394        "import|coinbase|out|cb-donate",
395        OutflowClass::Donate {
396            appraisal_required: false,
397        },
398        principal,
399        None,
400        Some("Habitat for Humanity".to_string()),
401        now,
402    )
403    .unwrap();
404    vault
405}
406
407/// Seed J2's fully post-decision state — the outbound reclassified as a donation, the Form 8283
408/// appraiser/donee details recorded (Section B complete), and a 2025 profile so the viewer's Tax tab
409/// computes the charitable-deduction line. The walkthrough's VIEWER renders Forms (the two 8283 rows) +
410/// Tax. Made-date threaded from the caller.
411pub fn seed_j2_donated(
412    dir: &std::path::Path,
413    pp: &btctax_store::Passphrase,
414    now: time::OffsetDateTime,
415) -> std::path::PathBuf {
416    use btctax_core::{Carryforward, DonationDetails, FilingStatus, TaxProfile};
417    use rust_decimal::Decimal;
418    let vault = seed_j2_reclassified(dir, pp, now);
419    let z = Decimal::ZERO;
420    let profile = TaxProfile {
421        filing_status: FilingStatus::Single,
422        ordinary_taxable_income: Decimal::from(100_000),
423        magi_excluding_crypto: Decimal::from(100_000),
424        qualified_dividends_and_other_pref_income: z,
425        other_net_capital_gain: z,
426        capital_loss_carryforward_in: Carryforward { short: z, long: z },
427        w2_ss_wages: z,
428        w2_medicare_wages: z,
429        schedule_c_expenses: z,
430    };
431    crate::cmd::tax::set_profile(&vault, pp, 2025, profile, false).unwrap();
432    crate::cmd::reconcile::set_donation_details(
433        &vault,
434        pp,
435        "import|coinbase|out|cb-donate",
436        DonationDetails {
437            donee_name: "Habitat for Humanity".into(),
438            donee_address: None,
439            donee_ein: Some("98-7654321".into()),
440            appraiser_name: "Jane Appraiser".into(),
441            appraiser_address: None,
442            appraiser_tin: Some("12-3456789".into()),
443            appraiser_ptin: None,
444            appraiser_qualifications: Some("ASA-accredited digital-asset appraiser, 8 yrs".into()),
445            appraisal_date: Some(time::macros::date!(2025 - 09 - 15)),
446            fmv_method_override: None,
447        },
448    )
449    .unwrap();
450    vault
451}
452
453/// Seed J5 (an LT lot $30k + a higher-basis ST lot $80k + a 2025 sale $50k) with a 2025 profile AND a
454/// standing FIFO election — but NOT the optimizer's accept. This is the state the walkthrough's `z`
455/// (optimize-accept) EDITOR frames drive from: the optimizer proposes swapping to the ST lot to realize a
456/// loss. The election made-date is hardcoded 2025-01-01 (the engine blocks effective_from before the
457/// made-date, so a later threaded `now` would trip MethodElectionBackdated).
458pub fn seed_j5_elected(dir: &std::path::Path, pp: &btctax_store::Passphrase) -> std::path::PathBuf {
459    use btctax_core::{Carryforward, FilingStatus, LotMethod, TaxProfile};
460    use rust_decimal::Decimal;
461    let vault = seed_journey(dir, pp, &j5());
462    let z = Decimal::ZERO;
463    let profile = TaxProfile {
464        filing_status: FilingStatus::Single,
465        ordinary_taxable_income: Decimal::from(100_000),
466        magi_excluding_crypto: Decimal::from(100_000),
467        qualified_dividends_and_other_pref_income: z,
468        other_net_capital_gain: z,
469        capital_loss_carryforward_in: Carryforward { short: z, long: z },
470        w2_ss_wages: z,
471        w2_medicare_wages: z,
472        schedule_c_expenses: z,
473    };
474    crate::cmd::tax::set_profile(&vault, pp, 2025, profile, false).unwrap();
475    crate::cmd::reconcile::set_forward_method(
476        &vault,
477        pp,
478        LotMethod::Fifo,
479        None,
480        Some(time::macros::date!(2025 - 01 - 01)),
481        time::macros::datetime!(2025 - 01 - 01 00:00:00 UTC),
482    )
483    .unwrap();
484    vault
485}
486
487/// Seed J5's post-accept state — the FIFO election PLUS the optimizer's proposed lot pick accepted (the
488/// whole sale moved onto the short-term `opt-buy-st` lot to realize a loss). The walkthrough's VIEWER
489/// renders the resulting Disposals (ST loss) + Tax (§1211 deduction + carryforward). `now` is the accept
490/// made-date — MUST be ≤ the 2025-06-01 sale so the selection is Contemporaneous; the caller passes the
491/// same instant the editor frames pin.
492pub fn seed_j5_optimized(
493    dir: &std::path::Path,
494    pp: &btctax_store::Passphrase,
495    now: time::OffsetDateTime,
496) -> std::path::PathBuf {
497    let vault = seed_j5_elected(dir, pp);
498    crate::cmd::optimize::accept(&vault, pp, 2025, None, None, now).unwrap();
499    vault
500}
501
502/// Seed J6 — a COMPLETE Form 1040 (the kitchen-sink MFJ household). Combines the crypto reconciliation
503/// (mining income → a trade or business; the 0.10 BTC Send → a §170(e) donation with Form 8283 details)
504/// with the non-crypto household imported from the committed `kitchen_sink_household()` TOML. This is the
505/// state BOTH walkthrough halves render: the editor half opens the tax-inputs authoring form (`T`) over
506/// these committed inputs; the viewer shows Forms (the crypto forms) + Tax (the merged MFJ return). No raw
507/// tax-profile — the full-return inputs take precedence and `build_snapshot` derives the MFJ profile from
508/// them. Made-date threaded from the caller.
509pub fn seed_j6_full(
510    dir: &std::path::Path,
511    pp: &btctax_store::Passphrase,
512    now: time::OffsetDateTime,
513) -> std::path::PathBuf {
514    use btctax_core::{DonationDetails, IncomeKind, OutflowClass};
515    let vault = seed_journey(dir, pp, &j6());
516    // 1. mining income → a trade or business (Schedule C/SE).
517    crate::cmd::reconcile::reclassify_income(
518        &vault,
519        pp,
520        "import|river|in|1710504000000|income|5000000#0",
521        true,
522        Some(IncomeKind::Mining),
523        now,
524    )
525    .unwrap();
526    // 2. the 0.10 BTC Send → a §170(e) charitable donation, FMV $6,000.
527    let fmv: rust_decimal::Decimal = "6000.00".parse().unwrap();
528    crate::cmd::reconcile::reclassify_outflow(
529        &vault,
530        pp,
531        "import|coinbase|out|cb-donate",
532        OutflowClass::Donate {
533            appraisal_required: false,
534        },
535        fmv,
536        None,
537        Some("Habitat for Humanity".to_string()),
538        now,
539    )
540    .unwrap();
541    // 3. the Form 8283 Section-B appraiser/donee details.
542    crate::cmd::reconcile::set_donation_details(
543        &vault,
544        pp,
545        "import|coinbase|out|cb-donate",
546        DonationDetails {
547            donee_name: "Habitat for Humanity".into(),
548            donee_address: None,
549            donee_ein: Some("98-7654321".into()),
550            appraiser_name: "Jane Appraiser".into(),
551            appraiser_address: None,
552            appraiser_tin: Some("12-3456789".into()),
553            appraiser_ptin: None,
554            appraiser_qualifications: Some("ASA-accredited digital-asset appraiser, 8 yrs".into()),
555            appraisal_date: Some(time::macros::date!(2024 - 09 - 15)),
556            fmv_method_override: None,
557        },
558    )
559    .unwrap();
560    // 4. the non-crypto household — a programmatic `income import` (reads a file, so materialize the TOML).
561    let toml = dir.join("fullreturn.toml");
562    std::fs::write(&toml, J6_FULLRETURN_TOML).unwrap();
563    crate::cmd::tax::import_return_inputs(&vault, pp, 2024, &toml).unwrap();
564    vault
565}