1pub 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
21pub 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
28pub 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
34pub 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
40pub 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
48pub 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
56pub 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
66pub const J6_FULLRETURN_TOML: &str =
70 include_str!("../tests/fixtures/examples/fullreturn_inputs.toml");
71
72pub 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
78pub 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
85pub 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
91pub 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
99pub struct JourneyFixture {
106 pub name: &'static str,
108 pub corpus: &'static [(&'static str, &'static str)],
110}
111
112pub 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}
158pub 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
175pub 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
200pub 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
222pub 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
248pub 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 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
293pub 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
334pub 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
361pub 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
379pub 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
407pub 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
453pub 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
487pub 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
502pub 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 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 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 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 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}