Skip to main content

btctax_cli/
lib.rs

1//! btctax-cli: the CLI + reconciliation library that wires the encrypted vault (btctax-store),
2//! ingest (btctax-adapters), and the pure projection (btctax-core) into the Phase-1 command surface
3//! (spec §11). The library is I/O-explicit and deterministic; the binary (`main.rs`) is a thin clap
4//! dispatch. PRIVACY: tests use only temp vaults + synthetic fixtures; no real user file is ever read.
5pub mod bulk_estimated;
6pub mod cli;
7pub mod cmd;
8pub mod config;
9pub mod donation_details;
10pub mod eventref;
11pub mod optimize_attest;
12pub mod price_cache;
13pub mod render;
14pub mod session;
15pub mod tax_profile;
16
17pub use cli::Cli;
18pub use config::CliConfig;
19pub use session::{
20    BulkFilter, BulkIncomeFilter, BulkIncomePlan, BulkIncomeRow, BulkLinkPlan, BulkLinkRow,
21    BulkReclassifyOutflowPlan, BulkReclassifyOutflowRow, BulkResolvePlan, BulkResolveRow,
22    BulkStiFilter, BulkStiPlan, BulkStiRow, BulkVoidPlan, BulkVoidRow, Frame, MatchAction,
23    MatchProposal, Session,
24};
25
26#[derive(Debug, thiserror::Error)]
27pub enum CliError {
28    #[error(transparent)]
29    Store(#[from] btctax_store::StoreError),
30    #[error(transparent)]
31    Core(#[from] btctax_core::CoreError),
32    #[error(transparent)]
33    Adapter(#[from] btctax_adapters::AdapterError),
34    #[error("sqlite: {0}")]
35    Sqlite(#[from] rusqlite::Error),
36    /// C1: `write_csv_exports` (Task 15) uses `?` on `csv::Writer` ops (→ `csv::Error`); `csv::Error`
37    /// is its own type (NOT covered by `Io(#[from] io::Error)`, whose `From` goes the other way), so it
38    /// needs its own variant or Task 15 will not compile.
39    #[error("csv: {0}")]
40    Csv(#[from] csv::Error),
41    #[error("io: {0}")]
42    Io(#[from] std::io::Error),
43    /// `export-irs-pdf`: an official IRS PDF fill failed — most importantly the geometric read-back
44    /// FAILING CLOSED on a mis-mapped cell, so no wrong tax form is ever written.
45    #[error("IRS form fill: {0}")]
46    FormFill(#[from] btctax_forms::FormsError),
47    /// A user-supplied event reference did not parse as a canonical `EventId` (eventref.rs).
48    #[error("not a valid event reference: {0:?}")]
49    BadEventRef(String),
50    /// A CLI argument was malformed (bad USD/date/enum/wallet spec, or a contradictory flag set).
51    #[error("usage: {0}")]
52    Usage(String),
53    /// M1: a `cli_config` row held an unrecognized value (corrupt DB, future-written value, or manual
54    /// edit gone wrong). Returning an error is safer than silently misreading the stored intent.
55    #[error("unrecognized stored config value: key={key:?} value={value:?}")]
56    BadConfigValue { key: String, value: String },
57    /// Sub-project 3 attestation gate: an export was attempted while the ledger is pseudo-reconciled
58    /// (a synthetic, non-persisted default contributes to the projection) and NO attestation phrase was
59    /// supplied. Producing a form/data file from a fictional draft requires typing the exact phrase.
60    /// (Supersedes sub-2's interim [I3] blanket refusal.)
61    #[error(
62        "export refused: the ledger is pseudo-reconciled (a synthetic default contributes to the \
63         projection). To export this draft ON PURPOSE, attest the exact phrase {:?} (pass --attest, or \
64         type it at the prompt). Otherwise run `reconcile pseudo off` (or approve + attest the defaults).",
65        ATTEST_PHRASE
66    )]
67    AttestationRequired,
68    /// Sub-project 3 attestation gate: an export was attempted while the ledger is pseudo-reconciled and
69    /// the supplied attestation phrase did NOT match (trimmed, case-sensitive, exact). A wrong phrase is
70    /// FAILED regardless of environment [R0-I1] — no fictional form leaves the machine.
71    #[error(
72        "export refused: the attestation phrase did not match. The ledger is pseudo-reconciled; type the \
73         phrase EXACTLY (trimmed, case-sensitive): {:?}.",
74        ATTEST_PHRASE
75    )]
76    AttestationFailed,
77}
78
79/// The exact phrase a user must affirm to export a form/data file while the ledger is pseudo-reconciled
80/// (sub-project 3). Compared TRIMMED, case-SENSITIVE, exact. The prompt + both error strings are BUILT
81/// from this constant [R0-M1] so there is no drift (a KAT asserts they contain it). `pub` so btctax-tui
82/// shares it [R0-r2-N2].
83pub const ATTEST_PHRASE: &str = "I attest this is true";
84
85/// PURE exact-compare attestation gate — NO I/O, NO TTY read [R0-I2]. The interactive prompt lives in
86/// the caller (the `export-snapshot` main.rs arm / the btctax-tui export modal); this helper only
87/// compares, keeping the library I/O-explicit and the KATs deterministic (no env-dependent branch).
88///
89/// - `attest.map(str::trim) == Some(ATTEST_PHRASE)` → `Ok(())`.
90/// - `Some(_)` non-matching → `Err(AttestationFailed)` (a wrong phrase FAILS regardless of env) [R0-I1].
91/// - `None` → `Err(AttestationRequired)`.
92///
93/// `pub` so btctax-tui shares the exact-compare [R0-r2-N2].
94pub fn require_attestation(attest: Option<&str>) -> Result<(), CliError> {
95    match attest.map(str::trim) {
96        Some(p) if p == ATTEST_PHRASE => Ok(()),
97        Some(_) => Err(CliError::AttestationFailed),
98        None => Err(CliError::AttestationRequired),
99    }
100}