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}