Skip to main content

btctax_cli/cmd/
admin.rs

1//! `config`, `export-snapshot` (FR10), `backup-key` — administrative commands. Config surfaces the TP8
2//! (c)/(b) treatment + the pre-2025 lot method; export/backup arrive in Task 15.
3use crate::config::{set_fee_treatment, set_pre2025_method as config_set_pre2025_method};
4use crate::render::write_csv_exports;
5use crate::{require_attestation, CliConfig, CliError, Session};
6use btctax_adapters::BundledTaxTables;
7use btctax_core::{compute_se_tax, FeeTreatment, LotMethod, TaxTables};
8use btctax_store::Passphrase;
9use std::path::{Path, PathBuf};
10
11pub fn show_config(vault_path: &Path, pp: &Passphrase) -> Result<CliConfig, CliError> {
12    Session::open(vault_path, pp)?.config()
13}
14
15/// Persist a new TP8 fee treatment (None = leave unchanged), then return the resulting config.
16pub fn set_config(
17    vault_path: &Path,
18    pp: &Passphrase,
19    fee_treatment: Option<FeeTreatment>,
20) -> Result<CliConfig, CliError> {
21    let mut session = Session::open(vault_path, pp)?;
22    if let Some(t) = fee_treatment {
23        set_fee_treatment(session.conn(), t)?;
24        session.save()?;
25    }
26    session.config()
27}
28
29/// Persist the pre-2025 lot identification method and attestation flag, then return the resulting config.
30pub fn set_pre2025_method(
31    vault_path: &Path,
32    pp: &Passphrase,
33    m: LotMethod,
34    attested: bool,
35) -> Result<CliConfig, CliError> {
36    let mut session = Session::open(vault_path, pp)?;
37    config_set_pre2025_method(session.conn(), m, attested)?;
38    session.save()?;
39    session.config()
40}
41
42/// FR10 / NFR2 exception: decrypted SQLite image (via the store) + the projected ledger as CSV.
43/// When `tax_year` is `Some(y)`, the per-tax-year Form 8949 + Schedule D CSVs are also written,
44/// year-scoped to `y` (P2-B); when `None`, only the all-years CSVs are written.
45///
46/// Sub-project 3 attestation gate: when the projection is pseudo-active (a synthetic default
47/// contributes), producing any form/data file requires the exact `ATTEST_PHRASE` in `attest`
48/// (trimmed, case-sensitive). Checked FIRST — before any bytes are written — so a refused export
49/// leaves `out_dir` untouched. A fully-real (not-pseudo-active) ledger ignores `attest` entirely.
50pub fn export_snapshot(
51    vault_path: &Path,
52    pp: &Passphrase,
53    out_dir: &Path,
54    tax_year: Option<i32>,
55    attest: Option<&str>,
56) -> Result<PathBuf, CliError> {
57    let session = Session::open(vault_path, pp)?;
58    // Attestation gate: REFUSE before ANY bytes are written when a synthetic default contributes and the
59    // attestation is missing/wrong — no fictional snapshot/8949/Schedule D leaves the machine unguarded.
60    // Checked FIRST (before the vault snapshot / CSV writes), so a refused export leaves out_dir untouched.
61    let (state, _cfg) = session.project()?;
62    if state.pseudo_active() {
63        require_attestation(attest)?;
64    }
65    let sqlite = session.vault().export_snapshot(out_dir)?; // writes out_dir/snapshot.sqlite
66                                                            // P2-D: standalone Schedule SE §1401 figure for the year-scoped export. Needs the year's filing
67                                                            // status (profile) + the year's ss_wage_base (bundled table); `None` when either is absent or
68                                                            // there is no business SE income. The "present but no table" note is a text-report concern
69                                                            // (render_schedule_se) — the CSV carries the computed figure only.
70    let se_result = match tax_year {
71        Some(y) => {
72            let tables = BundledTaxTables::load();
73            session.tax_profile(y)?.and_then(|p| {
74                tables.table_for(y).and_then(|t| {
75                    compute_se_tax(
76                        &state,
77                        y,
78                        p.filing_status,
79                        t,
80                        p.w2_ss_wages,
81                        p.w2_medicare_wages,
82                        p.schedule_c_expenses,
83                    )
84                })
85            })
86        }
87        None => None,
88    };
89    let donation_details = session.donation_details()?;
90    write_csv_exports(
91        out_dir,
92        &state,
93        tax_year,
94        se_result.as_ref(),
95        &donation_details,
96    )?;
97    Ok(sqlite)
98}
99
100/// Probe: would an export be gated? `true` when the projection is pseudo-active (a synthetic default
101/// contributes). Used by the `export-snapshot` CLI arm to decide whether to PROMPT for the attestation
102/// phrase; the authoritative gate lives inside `export_snapshot` itself. Kept in the library so main.rs
103/// stays a thin dispatch (no session-open / projection business logic in the binary).
104pub fn export_pseudo_active(vault_path: &Path, pp: &Passphrase) -> Result<bool, CliError> {
105    let (state, _cfg) = Session::open(vault_path, pp)?.project()?;
106    Ok(state.pseudo_active())
107}
108
109/// §8: export the passphrase-protected key (escape hatch; HIGH-security write).
110pub fn backup_key(vault_path: &Path, pp: &Passphrase, out_path: &Path) -> Result<(), CliError> {
111    Session::open(vault_path, pp)?
112        .vault()
113        .backup_key(out_path)?;
114    Ok(())
115}