Skip to main content

btctax_adapters/
lib.rs

1//! btctax-adapters: exchange-export parsers + the bundled daily-close price dataset (§9).
2//! Parses Coinbase/Gemini/River/Swan exports into `btctax_core::LedgerEvent`s — BTC-only (FR2),
3//! ingest-time FMV (FR3) over the bundled `PriceProvider` (§9.2). Exact arithmetic only (NFR5).
4//!
5//! PRIVACY: only SYNTHETIC fixtures are used in tests; the real exports in
6//! ~/Documents/BitcoinTax/ReadOnly are NEVER read by this crate or its tests.
7pub mod adapter;
8pub mod ingest;
9pub mod normalize;
10pub mod parse;
11pub mod price;
12pub mod read;
13pub mod sources;
14pub mod tax_tables;
15
16pub use adapter::{Adapter, FileGroup, FileReport, GroupOutput, IngestBatch, SourceFile};
17pub use ingest::{ingest_files, ingest_files_bundled};
18pub use price::BundledPrices;
19pub use tax_tables::BundledTaxTables;
20
21#[derive(Debug, thiserror::Error)]
22pub enum AdapterError {
23    #[error("io reading {path}: {source}")]
24    Io {
25        path: String,
26        #[source]
27        source: std::io::Error,
28    },
29    #[error("csv {path}: {source}")]
30    Csv {
31        path: String,
32        #[source]
33        source: csv::Error,
34    },
35    #[error("xlsx {path}: {source}")]
36    Xlsx {
37        path: String,
38        #[source]
39        source: calamine::Error,
40    },
41    /// Used by `read_xlsx` when the workbook has no worksheet (calamine returns `None`, not an error).
42    /// Distinct from `Xlsx` (which wraps a calamine error) so callers can pattern-match the cause.
43    #[error("xlsx {path}: no worksheet found")]
44    EmptyXlsx { path: String },
45    #[error("{adapter} row {line}: missing required column {column:?}")]
46    MissingColumn {
47        adapter: &'static str,
48        line: usize,
49        column: String,
50    },
51    #[error("{adapter} row {line}: cannot parse {field} from {value:?}: {reason}")]
52    Parse {
53        adapter: &'static str,
54        line: usize,
55        field: &'static str,
56        value: String,
57        reason: String,
58    },
59    #[error("unrecognized file (no adapter matched): {path}")]
60    UnknownSource { path: String },
61    /// A file was detected as Swan (matched at least one role signature) but its header did not
62    /// match any of the three confirmed roles (trades / transfers / withdrawals). The actual trigger
63    /// is an unrecognized role, not a missing file — hence the rename from `IncompleteSwanBatch`.
64    #[error(
65        "unrecognized Swan file role (header did not match trades/transfers/withdrawals): {path}"
66    )]
67    UnrecognizedSwanRole { path: String },
68    #[error("{adapter}: header signature not found in file")]
69    HeaderNotFound { adapter: &'static str },
70    #[error("price dataset: {0}")]
71    PriceDataset(String),
72}