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 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
109pub 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
178pub struct JourneyFixture {
185 pub name: &'static str,
187 pub corpus: &'static [(&'static str, &'static str)],
189}
190
191pub 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}
237pub 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
254pub 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
279pub 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
301pub 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
327pub 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 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
372pub 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
413pub 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
440pub 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
458pub 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
486pub 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
532pub 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
566pub 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
581pub 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 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 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 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 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}