use crate::read::RawRow;
use btctax_core::price::fmv_of;
use btctax_core::{FmvStatus, PriceProvider, Sat, Source, SourceRef, TaxDate, Usd, WalletId};
use std::collections::HashMap;
pub fn resolve_fmv(
export_usd: Option<Usd>,
date: TaxDate,
sat: Sat,
prices: &dyn PriceProvider,
) -> (Option<Usd>, FmvStatus) {
if let Some(u) = export_usd {
return (Some(u), FmvStatus::ExchangeProvided);
}
match fmv_of(prices, date, sat.abs()) {
Some(u) => (Some(u), FmvStatus::PriceDataset),
None => (None, FmvStatus::Missing),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
In,
Out,
Trade,
}
impl Direction {
pub fn tag(self) -> &'static str {
match self {
Direction::In => "in",
Direction::Out => "out",
Direction::Trade => "trade",
}
}
}
#[derive(Debug, Default)]
pub struct SourceRefMint {
seen: HashMap<String, u32>,
}
impl SourceRefMint {
pub fn native(&self, dir: Direction, id: &str) -> SourceRef {
SourceRef::new(format!("{}|{}", dir.tag(), id))
}
pub fn semantic(&mut self, dir: Direction, utc_ms: i64, type_tag: &str, sat: Sat) -> SourceRef {
let key = format!("{}|{}|{}|{}", dir.tag(), utc_ms, type_tag, sat);
let occ = self.seen.entry(key.clone()).or_insert(0);
let r = SourceRef::new(format!("{key}#{occ}"));
*occ += 1;
r
}
}
pub fn exchange_wallet(source: Source) -> WalletId {
WalletId::Exchange {
provider: source.tag().to_string(),
account: "default".to_string(),
}
}
pub fn raw_of(row: &RawRow) -> String {
row.cells
.iter()
.map(|(k, v)| format!("{k}={v}"))
.collect::<Vec<_>>()
.join("; ")
}
#[cfg(test)]
mod tests {
use super::*;
use btctax_core::price::StaticPrices;
use btctax_core::FmvStatus;
use rust_decimal_macros::dec;
use time::macros::date;
fn prices() -> StaticPrices {
let mut m = std::collections::BTreeMap::new();
m.insert(date!(2025 - 03 - 01), dec!(84000.00));
StaticPrices(m)
}
#[test]
fn fr3_prefers_export_then_dataset_then_missing() {
let p = prices();
let (v, s) = resolve_fmv(Some(dec!(123.45)), date!(2025 - 03 - 01), 50_000_000, &p);
assert_eq!((v, s), (Some(dec!(123.45)), FmvStatus::ExchangeProvided));
let (v, s) = resolve_fmv(None, date!(2025 - 03 - 01), 50_000_000, &p);
assert_eq!((v, s), (Some(dec!(42000.00)), FmvStatus::PriceDataset));
let (v, s) = resolve_fmv(None, date!(2025 - 06 - 15), 50_000_000, &p);
assert_eq!((v, s), (None, FmvStatus::Missing));
}
#[test]
fn native_source_ref_is_direction_scoped_id() {
let mint = SourceRefMint::default();
assert_eq!(mint.native(Direction::Out, "TX-9").0, "out|TX-9");
}
#[test]
fn semantic_source_ref_disambiguates_identical_rows_by_occurrence() {
let mut mint = SourceRefMint::default();
let a = mint.semantic(Direction::In, 1_700_000_000_000, "income", 1000);
let b = mint.semantic(Direction::In, 1_700_000_000_000, "income", 1000); assert_eq!(a.0, "in|1700000000000|income|1000#0");
assert_eq!(b.0, "in|1700000000000|income|1000#1"); assert_ne!(a, b);
}
}