pub const J1_CSV: &str = "\r\nTransactions\r\nUser,00000000-0000-0000-0000-000000000000\r\n\
ID,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\
cb-buy,2025-03-01 12:00:00 UTC,Buy,BTC,0.10000000,USD,84000.00,8400.00,8450.00,50.00,,,\r\n\
cb-sell,2025-06-15 12:00:00 UTC,Sell,BTC,0.02000000,USD,67500.00,1350.00,1340.00,10.00,,,\r\n";
pub const J2_CSV: &str = "\r\nTransactions\r\nUser,00000000-0000-0000-0000-000000000000\r\n\
ID,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\
cb-buy-lt,2023-06-01 12:00:00 UTC,Buy,BTC,1.00000000,USD,5000.00,5000.00,5000.00,0.00,,,\r\n\
cb-buy-st,2025-03-01 12:00:00 UTC,Buy,BTC,1.00000000,USD,2000.00,2000.00,2000.00,0.00,,,\r\n\
cb-donate,2025-09-01 12:00:00 UTC,Send,BTC,2.00000000,USD,108996.17,,,,,,bc1qcharity\r\n";
pub const J3_CSV: &str = "\r\nTransactions\r\nUser,00000000-0000-0000-0000-000000000000\r\n\
ID,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\
cb-buy,2025-02-01 12:00:00 UTC,Buy,BTC,0.50000000,USD,95000.00,47500.00,47550.00,50.00,,,\r\n\
cb-recv,2025-08-01 12:00:00 UTC,Receive,BTC,0.20000000,USD,110000.00,,,,,,\r\n";
pub const J4_CSV: &str =
"Date,Sent Amount,Sent Currency,Received Amount,Received Currency,Fee Amount,Tag\r\n\
2025-04-15 12:00:00 UTC,,,0.05000000,BTC,,income\r\n\
2025-05-20 12:00:00 UTC,,,0.03000000,BTC,,income\r\n";
pub const J5_CSV: &str = "\r\nTransactions\r\nUser,00000000-0000-0000-0000-000000000000\r\n\
ID,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\
opt-buy-lt,2023-01-01 12:00:00 UTC,Buy,BTC,1.00000000,USD,30000.00,30000.00,30000.00,0.00,,,\r\n\
opt-buy-st,2025-01-02 12:00:00 UTC,Buy,BTC,1.00000000,USD,80000.00,80000.00,80000.00,0.00,,,\r\n\
opt-sell,2025-06-01 12:00:00 UTC,Sell,BTC,1.00000000,USD,50000.00,50000.00,50000.00,0.00,,,\r\n";
pub const J6_RIVER_CSV: &str =
"Date,Sent Amount,Sent Currency,Received Amount,Received Currency,Fee Amount,Tag\r\n\
2024-03-15 12:00:00 UTC,,,0.05000000,BTC,,income\r\n";
pub const J6_COINBASE_CSV: &str = "\r\nTransactions\r\nUser,00000000-0000-0000-0000-000000000000\r\n\
ID,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\
cb-buy,2020-01-01 12:00:00 UTC,Buy,BTC,0.30000000,USD,30000.00,9000.00,9000.00,0.00,,,\r\n\
cb-sell,2024-05-01 12:00:00 UTC,Sell,BTC,0.05000000,USD,63000.00,3150.00,3130.00,20.00,,,\r\n\
cb-donate,2024-09-01 12:00:00 UTC,Send,BTC,0.10000000,USD,60000.00,,,,,,bc1qcharity\r\n";
pub const J6_FULLRETURN_TOML: &str =
include_str!("../tests/fixtures/examples/fullreturn_inputs.toml");
pub const J7_CSV: &str = "\r\nTransactions\r\nUser,00000000-0000-0000-0000-000000000000\r\n\
ID,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\
cb-recv,2024-06-15 12:00:00 UTC,Receive,BTC,0.05000000,USD,,,,,,,\r\n";
pub const J8_RIVER_CSV: &str =
"Date,Sent Amount,Sent Currency,Received Amount,Received Currency,Fee Amount,Tag\r\n\
2025-01-05 12:00:00 UTC,4000.00,USD,0.10000000,BTC,,buy\r\n\
2025-03-10 12:00:00 UTC,0.10000000,BTC,,,,withdrawal\r\n";
pub const J8_COINBASE_CSV: &str = "\r\nTransactions\r\nUser,00000000-0000-0000-0000-000000000000\r\n\
ID,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\
cb-recv,2025-03-10 12:00:00 UTC,Receive,BTC,0.10000000,USD,,,,,,,\r\n";
pub const J9_CSV: &str = "\r\nTransactions\r\nUser,00000000-0000-0000-0000-000000000000\r\n\
ID,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\
lot-a,2023-01-01 12:00:00 UTC,Buy,BTC,0.60000000,USD,25000.00,15000.00,15000.00,0.00,,,\r\n\
lot-b,2024-01-02 12:00:00 UTC,Buy,BTC,0.40000000,USD,60000.00,24000.00,24000.00,0.00,,,\r\n\
sale,2025-06-01 12:00:00 UTC,Sell,BTC,0.50000000,USD,47500.00,47500.00,47500.00,0.00,,,\r\n";
pub struct JourneyFixture {
pub name: &'static str,
pub corpus: &'static [(&'static str, &'static str)],
}
pub fn j1() -> JourneyFixture {
JourneyFixture {
name: "j1",
corpus: &[("coinbase.csv", J1_CSV)],
}
}
pub fn j2() -> JourneyFixture {
JourneyFixture {
name: "j2",
corpus: &[("coinbase.csv", J2_CSV)],
}
}
pub fn j3() -> JourneyFixture {
JourneyFixture {
name: "j3",
corpus: &[("coinbase.csv", J3_CSV)],
}
}
pub fn j4() -> JourneyFixture {
JourneyFixture {
name: "j4",
corpus: &[("river.csv", J4_CSV)],
}
}
pub fn j5() -> JourneyFixture {
JourneyFixture {
name: "j5",
corpus: &[("coinbase.csv", J5_CSV)],
}
}
pub fn j6() -> JourneyFixture {
JourneyFixture {
name: "j6",
corpus: &[
("river.csv", J6_RIVER_CSV),
("coinbase.csv", J6_COINBASE_CSV),
],
}
}
pub fn j7() -> JourneyFixture {
JourneyFixture {
name: "j7",
corpus: &[("coinbase.csv", J7_CSV)],
}
}
pub fn j8() -> JourneyFixture {
JourneyFixture {
name: "j8",
corpus: &[
("river.csv", J8_RIVER_CSV),
("coinbase.csv", J8_COINBASE_CSV),
],
}
}
pub fn j9() -> JourneyFixture {
JourneyFixture {
name: "j9",
corpus: &[("coinbase.csv", J9_CSV)],
}
}
pub fn seed_journey(
dir: &std::path::Path,
pp: &btctax_store::Passphrase,
fx: &JourneyFixture,
) -> std::path::PathBuf {
let vault = dir.join("vault.pgp");
let key = dir.join("key.asc");
crate::cmd::init::run(&vault, pp, &key).unwrap();
let mut files: Vec<std::path::PathBuf> = Vec::new();
for (name, content) in fx.corpus {
let p = dir.join(name);
std::fs::write(&p, content).unwrap();
files.push(p);
}
crate::cmd::import::run(&vault, pp, &files).unwrap();
vault
}
pub fn seed_j8_relocated(
dir: &std::path::Path,
pp: &btctax_store::Passphrase,
now: time::OffsetDateTime,
) -> std::path::PathBuf {
use btctax_core::event::TransferTarget;
let vault = seed_journey(dir, pp, &j8());
let in_id = crate::eventref::parse_event_id("import|coinbase|in|cb-recv").unwrap();
crate::cmd::reconcile::link_transfer(
&vault,
pp,
"import|river|out|1741608000000|withdrawal|10000000#0",
TransferTarget::InEvent(in_id),
now,
)
.unwrap();
vault
}
pub fn seed_j1_with_profile(
dir: &std::path::Path,
pp: &btctax_store::Passphrase,
) -> std::path::PathBuf {
use btctax_core::{Carryforward, FilingStatus, TaxProfile};
use rust_decimal::Decimal;
let vault = seed_journey(dir, pp, &j1());
let z = Decimal::ZERO;
let profile = TaxProfile {
filing_status: FilingStatus::Single,
ordinary_taxable_income: Decimal::from(100_000),
magi_excluding_crypto: Decimal::from(100_000),
qualified_dividends_and_other_pref_income: z,
other_net_capital_gain: z,
capital_loss_carryforward_in: Carryforward { short: z, long: z },
w2_ss_wages: z,
w2_medicare_wages: z,
schedule_c_expenses: z,
};
crate::cmd::tax::set_profile(&vault, pp, 2025, profile, false).unwrap();
vault
}
pub fn seed_j4_reclassified(
dir: &std::path::Path,
pp: &btctax_store::Passphrase,
now: time::OffsetDateTime,
) -> std::path::PathBuf {
use btctax_core::{Carryforward, FilingStatus, IncomeKind, TaxProfile};
use rust_decimal::Decimal;
let vault = seed_journey(dir, pp, &j4());
let z = Decimal::ZERO;
let profile = TaxProfile {
filing_status: FilingStatus::Single,
ordinary_taxable_income: Decimal::from(100_000),
magi_excluding_crypto: Decimal::from(100_000),
qualified_dividends_and_other_pref_income: z,
other_net_capital_gain: z,
capital_loss_carryforward_in: Carryforward { short: z, long: z },
w2_ss_wages: z,
w2_medicare_wages: z,
schedule_c_expenses: z,
};
crate::cmd::tax::set_profile(&vault, pp, 2025, profile, false).unwrap();
for r in [
"import|river|in|1744718400000|income|5000000#0",
"import|river|in|1747742400000|income|3000000#0",
] {
crate::cmd::reconcile::reclassify_income(
&vault,
pp,
r,
true,
Some(IncomeKind::Staking),
now,
)
.unwrap();
}
vault
}
pub fn seed_j7_income(
dir: &std::path::Path,
pp: &btctax_store::Passphrase,
now: time::OffsetDateTime,
) -> std::path::PathBuf {
use btctax_core::{Carryforward, FilingStatus, InboundClass, IncomeKind, TaxProfile};
use rust_decimal::Decimal;
let vault = seed_journey(dir, pp, &j7());
let z = Decimal::ZERO;
let profile = TaxProfile {
filing_status: FilingStatus::Single,
ordinary_taxable_income: Decimal::from(100_000),
magi_excluding_crypto: Decimal::from(100_000),
qualified_dividends_and_other_pref_income: z,
other_net_capital_gain: z,
capital_loss_carryforward_in: Carryforward { short: z, long: z },
w2_ss_wages: z,
w2_medicare_wages: z,
schedule_c_expenses: z,
};
crate::cmd::tax::set_profile(&vault, pp, 2024, profile, false).unwrap();
let fmv: Decimal = "3300.00".parse().unwrap();
crate::cmd::reconcile::classify_inbound(
&vault,
pp,
"import|coinbase|in|cb-recv",
InboundClass::Income {
kind: IncomeKind::Staking,
fmv: Some(fmv),
business: false,
},
now,
)
.unwrap();
vault
}
pub fn seed_j3_self_transfer(
dir: &std::path::Path,
pp: &btctax_store::Passphrase,
now: time::OffsetDateTime,
) -> std::path::PathBuf {
use btctax_core::InboundClass;
use rust_decimal::Decimal;
let vault = seed_journey(dir, pp, &j3());
let basis: Decimal = "19000.00".parse().unwrap();
crate::cmd::reconcile::classify_inbound(
&vault,
pp,
"import|coinbase|in|cb-recv",
InboundClass::SelfTransferMine {
basis: Some(basis),
acquired_at: Some(time::macros::date!(2024 - 11 - 01)),
},
now,
)
.unwrap();
vault
}
pub fn seed_j9_selected(
dir: &std::path::Path,
pp: &btctax_store::Passphrase,
now: time::OffsetDateTime,
) -> std::path::PathBuf {
let vault = seed_journey(dir, pp, &j9());
let pick = crate::eventref::parse_lot_pick("import|coinbase|trade|lot-a#0:50000000").unwrap();
crate::cmd::reconcile::select_lots(&vault, pp, "import|coinbase|trade|sale", vec![pick], now)
.unwrap();
vault
}
pub fn seed_j2_reclassified(
dir: &std::path::Path,
pp: &btctax_store::Passphrase,
now: time::OffsetDateTime,
) -> std::path::PathBuf {
use btctax_core::OutflowClass;
let vault = seed_journey(dir, pp, &j2());
let principal: rust_decimal::Decimal = "217992.34".parse().unwrap();
crate::cmd::reconcile::reclassify_outflow(
&vault,
pp,
"import|coinbase|out|cb-donate",
OutflowClass::Donate {
appraisal_required: false,
},
principal,
None,
Some("Habitat for Humanity".to_string()),
now,
)
.unwrap();
vault
}
pub fn seed_j2_donated(
dir: &std::path::Path,
pp: &btctax_store::Passphrase,
now: time::OffsetDateTime,
) -> std::path::PathBuf {
use btctax_core::{Carryforward, DonationDetails, FilingStatus, TaxProfile};
use rust_decimal::Decimal;
let vault = seed_j2_reclassified(dir, pp, now);
let z = Decimal::ZERO;
let profile = TaxProfile {
filing_status: FilingStatus::Single,
ordinary_taxable_income: Decimal::from(100_000),
magi_excluding_crypto: Decimal::from(100_000),
qualified_dividends_and_other_pref_income: z,
other_net_capital_gain: z,
capital_loss_carryforward_in: Carryforward { short: z, long: z },
w2_ss_wages: z,
w2_medicare_wages: z,
schedule_c_expenses: z,
};
crate::cmd::tax::set_profile(&vault, pp, 2025, profile, false).unwrap();
crate::cmd::reconcile::set_donation_details(
&vault,
pp,
"import|coinbase|out|cb-donate",
DonationDetails {
donee_name: "Habitat for Humanity".into(),
donee_address: None,
donee_ein: Some("98-7654321".into()),
appraiser_name: "Jane Appraiser".into(),
appraiser_address: None,
appraiser_tin: Some("12-3456789".into()),
appraiser_ptin: None,
appraiser_qualifications: Some("ASA-accredited digital-asset appraiser, 8 yrs".into()),
appraisal_date: Some(time::macros::date!(2025 - 09 - 15)),
fmv_method_override: None,
},
)
.unwrap();
vault
}
pub fn seed_j5_elected(dir: &std::path::Path, pp: &btctax_store::Passphrase) -> std::path::PathBuf {
use btctax_core::{Carryforward, FilingStatus, LotMethod, TaxProfile};
use rust_decimal::Decimal;
let vault = seed_journey(dir, pp, &j5());
let z = Decimal::ZERO;
let profile = TaxProfile {
filing_status: FilingStatus::Single,
ordinary_taxable_income: Decimal::from(100_000),
magi_excluding_crypto: Decimal::from(100_000),
qualified_dividends_and_other_pref_income: z,
other_net_capital_gain: z,
capital_loss_carryforward_in: Carryforward { short: z, long: z },
w2_ss_wages: z,
w2_medicare_wages: z,
schedule_c_expenses: z,
};
crate::cmd::tax::set_profile(&vault, pp, 2025, profile, false).unwrap();
crate::cmd::reconcile::set_forward_method(
&vault,
pp,
LotMethod::Fifo,
None,
Some(time::macros::date!(2025 - 01 - 01)),
time::macros::datetime!(2025 - 01 - 01 00:00:00 UTC),
)
.unwrap();
vault
}
pub fn seed_j5_optimized(
dir: &std::path::Path,
pp: &btctax_store::Passphrase,
now: time::OffsetDateTime,
) -> std::path::PathBuf {
let vault = seed_j5_elected(dir, pp);
crate::cmd::optimize::accept(&vault, pp, 2025, None, None, now).unwrap();
vault
}
pub fn seed_j6_full(
dir: &std::path::Path,
pp: &btctax_store::Passphrase,
now: time::OffsetDateTime,
) -> std::path::PathBuf {
use btctax_core::{DonationDetails, IncomeKind, OutflowClass};
let vault = seed_journey(dir, pp, &j6());
crate::cmd::reconcile::reclassify_income(
&vault,
pp,
"import|river|in|1710504000000|income|5000000#0",
true,
Some(IncomeKind::Mining),
now,
)
.unwrap();
let fmv: rust_decimal::Decimal = "6000.00".parse().unwrap();
crate::cmd::reconcile::reclassify_outflow(
&vault,
pp,
"import|coinbase|out|cb-donate",
OutflowClass::Donate {
appraisal_required: false,
},
fmv,
None,
Some("Habitat for Humanity".to_string()),
now,
)
.unwrap();
crate::cmd::reconcile::set_donation_details(
&vault,
pp,
"import|coinbase|out|cb-donate",
DonationDetails {
donee_name: "Habitat for Humanity".into(),
donee_address: None,
donee_ein: Some("98-7654321".into()),
appraiser_name: "Jane Appraiser".into(),
appraiser_address: None,
appraiser_tin: Some("12-3456789".into()),
appraiser_ptin: None,
appraiser_qualifications: Some("ASA-accredited digital-asset appraiser, 8 yrs".into()),
appraisal_date: Some(time::macros::date!(2024 - 09 - 15)),
fmv_method_override: None,
},
)
.unwrap();
let toml = dir.join("fullreturn.toml");
std::fs::write(&toml, J6_FULLRETURN_TOML).unwrap();
crate::cmd::tax::import_return_inputs(&vault, pp, 2024, &toml).unwrap();
vault
}