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 input_form_store;
12pub mod optimize_attest;
13pub mod price_cache;
14pub mod render;
15pub mod resolve;
16pub mod return_inputs;
17pub mod session;
18pub mod tax_profile;
19
20pub use cli::Cli;
21pub use config::CliConfig;
22pub use session::{
23    BulkFilter, BulkIncomeFilter, BulkIncomePlan, BulkIncomeRow, BulkLinkPlan, BulkLinkRow,
24    BulkReclassifyOutflowPlan, BulkReclassifyOutflowRow, BulkResolvePlan, BulkResolveRow,
25    BulkStiFilter, BulkStiPlan, BulkStiRow, BulkVoidPlan, BulkVoidRow, Frame, MatchAction,
26    MatchProposal, Session,
27};
28
29#[derive(Debug, thiserror::Error)]
30pub enum CliError {
31    #[error(transparent)]
32    Store(#[from] btctax_store::StoreError),
33    #[error(transparent)]
34    Core(#[from] btctax_core::CoreError),
35    #[error(transparent)]
36    Adapter(#[from] btctax_adapters::AdapterError),
37    #[error("sqlite: {0}")]
38    Sqlite(#[from] rusqlite::Error),
39    /// C1: `write_csv_exports` (Task 15) uses `?` on `csv::Writer` ops (→ `csv::Error`); `csv::Error`
40    /// is its own type (NOT covered by `Io(#[from] io::Error)`, whose `From` goes the other way), so it
41    /// needs its own variant or Task 15 will not compile.
42    #[error("csv: {0}")]
43    Csv(#[from] csv::Error),
44    #[error("io: {0}")]
45    Io(#[from] std::io::Error),
46    /// `export-irs-pdf`: an official IRS PDF fill failed — most importantly the geometric read-back
47    /// FAILING CLOSED on a mis-mapped cell, so no wrong tax form is ever written.
48    #[error("IRS form fill: {0}")]
49    FormFill(#[from] btctax_forms::FormsError),
50    /// A user-supplied event reference did not parse as a canonical `EventId` (eventref.rs).
51    #[error("not a valid event reference: {0:?}")]
52    BadEventRef(String),
53    /// A CLI argument was malformed (bad USD/date/enum/wallet spec, or a contradictory flag set).
54    #[error("usage: {0}")]
55    Usage(String),
56    /// M1: a `cli_config` row held an unrecognized value (corrupt DB, future-written value, or manual
57    /// edit gone wrong). Returning an error is safer than silently misreading the stored intent.
58    #[error("unrecognized stored config value: key={key:?} value={value:?}")]
59    BadConfigValue { key: String, value: String },
60    /// P9 §2.6: a stored `return_inputs` row predates the form-question registry (or was written by a newer
61    /// build) and this build does not migrate it. There is no user data yet, so the policy is refuse-and-
62    /// reimport rather than a per-key migration (a version check cannot forget a key). The remedy names all
63    /// THREE commands, in order — `clear` DISCARDS any computed carryover this row's prior reports wrote onto
64    /// it, so the rebuild step is not optional. (Retire this the moment real data exists — FOLLOWUPS, release
65    /// gate.)
66    #[error(
67        "the stored inputs for {year} predate the form-question registry (schema v{found}; this build reads \
68         v{expected}). Run `btctax income clear {year}` — which DISCARDS any carryover this row's prior \
69         reports computed onto it — then `btctax income import` for {year}; then, if this row carried a \
70         computed carryover, `btctax report --tax-year {prior} --write-carryover` to rebuild it.",
71        prior = year - 1
72    )]
73    StaleReturnInputs { year: i32, found: i64, expected: i64 },
74    /// §6.3 / C-1: a PARKED input-form draft is at a schema version this build does not read, and this
75    /// build does not migrate it. Unlike a stale WIP draft (regenerable → discarded), a parked draft may
76    /// hold irreplaceable carryover that exists ONLY in the draft — there is no committed row to re-import
77    /// from — so we REFUSE (fail closed) rather than discard. The remedy therefore is NOT `income import`
78    /// (that recovers a WIP row from committed state, which a parked draft has none of): the message must
79    /// tell the filer the data lives in the draft, must not be discarded, and to re-run on / export from the
80    /// app version that wrote it. (Retire alongside `StaleReturnInputs` the moment migrations exist.)
81    #[error(
82        "year {year}'s parked full return is schema v{found} but this build expects v{expected}; \
83         an upgrade changed the input format. Its data lives only in the draft — do not discard it. \
84         Re-run on the app version that wrote it, or export it there first."
85    )]
86    StaleParkedDraft { year: i32, found: i64, expected: i64 },
87    /// §6.2 draft-coherence: an authoritative committed-row write (`income import` / `income answer` /
88    /// carryover write-back / `income clear`) was attempted for a year whose input-form draft is PARKED.
89    /// A parked draft is the SOLE copy of a screened return (C-1) — clobbering it via the committed row
90    /// would silently destroy irreplaceable data — so the write is REFUSED (fail closed). The message
91    /// names BOTH in-form exits (M-d): re-commit it (`use full return`) or drop it (`discard parked
92    /// draft`, a confirmed delete); a WIP draft, by contrast, is regenerable and is cleared silently.
93    #[error(
94        "year {year} holds a parked full return — in the form, 'use full return' to re-commit it, or \
95         'discard parked draft' (a confirmed delete) to drop it; then re-run this command."
96    )]
97    ParkedDraftBlocksWrite { year: i32 },
98    /// Sub-project 3 attestation gate: an export was attempted while the ledger is pseudo-reconciled
99    /// (a synthetic, non-persisted default contributes to the projection) and NO attestation phrase was
100    /// supplied. Producing a form/data file from a fictional draft requires typing the exact phrase.
101    /// (Supersedes sub-2's interim [I3] blanket refusal.)
102    #[error(
103        "export refused: the ledger is pseudo-reconciled (a synthetic default contributes to the \
104         projection). To export this draft ON PURPOSE, attest the exact phrase {:?} (pass --attest, or \
105         type it at the prompt). Otherwise run `reconcile pseudo off` (or approve + attest the defaults).",
106        ATTEST_PHRASE
107    )]
108    AttestationRequired,
109    /// Sub-project 3 attestation gate: an export was attempted while the ledger is pseudo-reconciled and
110    /// the supplied attestation phrase did NOT match (trimmed, case-sensitive, exact). A wrong phrase is
111    /// FAILED regardless of environment [R0-I1] — no fictional form leaves the machine.
112    #[error(
113        "export refused: the attestation phrase did not match. The ledger is pseudo-reconciled; type the \
114         phrase EXACTLY (trimmed, case-sensitive): {:?}.",
115        ATTEST_PHRASE
116    )]
117    AttestationFailed,
118}
119
120/// The exact phrase a user must affirm to export a form/data file while the ledger is pseudo-reconciled
121/// (sub-project 3). Compared TRIMMED, case-SENSITIVE, exact. The prompt + both error strings are BUILT
122/// from this constant [R0-M1] so there is no drift (a KAT asserts they contain it). `pub` so btctax-tui
123/// shares it [R0-r2-N2].
124pub const ATTEST_PHRASE: &str = "I attest this is true";
125
126/// PURE exact-compare attestation gate — NO I/O, NO TTY read [R0-I2]. The interactive prompt lives in
127/// the caller (the `export-snapshot` main.rs arm / the btctax-tui export modal); this helper only
128/// compares, keeping the library I/O-explicit and the KATs deterministic (no env-dependent branch).
129///
130/// - `attest.map(str::trim) == Some(ATTEST_PHRASE)` → `Ok(())`.
131/// - `Some(_)` non-matching → `Err(AttestationFailed)` (a wrong phrase FAILS regardless of env) [R0-I1].
132/// - `None` → `Err(AttestationRequired)`.
133///
134/// `pub` so btctax-tui shares the exact-compare [R0-r2-N2].
135pub fn require_attestation(attest: Option<&str>) -> Result<(), CliError> {
136    match attest.map(str::trim) {
137        Some(p) if p == ATTEST_PHRASE => Ok(()),
138        Some(_) => Err(CliError::AttestationFailed),
139        None => Err(CliError::AttestationRequired),
140    }
141}