1use crate::CliError;
6use btctax_core::{FeeTreatment, LotMethod, ProjectionConfig};
7use rusqlite::{Connection, OptionalExtension};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub struct CliConfig {
12 pub fee_treatment: FeeTreatment,
13 pub pre2025_method: LotMethod,
14 pub pre2025_method_attested: bool,
15 pub pseudo_reconcile: bool,
19}
20
21impl Default for CliConfig {
22 fn default() -> Self {
23 CliConfig {
25 fee_treatment: FeeTreatment::TreatmentC,
26 pre2025_method: LotMethod::Hifo,
30 pre2025_method_attested: false,
31 pseudo_reconcile: false,
32 }
33 }
34}
35
36impl CliConfig {
37 pub fn to_projection(self) -> ProjectionConfig {
39 ProjectionConfig {
40 self_transfer_fee: self.fee_treatment,
41 pre2025_method: self.pre2025_method,
42 pre2025_method_attested: self.pre2025_method_attested,
43 pseudo_reconcile: self.pseudo_reconcile,
44 }
45 }
46}
47
48pub fn init_config_table(conn: &Connection) -> Result<(), CliError> {
53 conn.execute_batch(
54 "CREATE TABLE IF NOT EXISTS cli_config (key TEXT PRIMARY KEY, value TEXT NOT NULL);",
55 )?;
56 Ok(())
57}
58
59fn get(conn: &Connection, key: &str) -> Result<Option<String>, CliError> {
60 Ok(conn
61 .query_row("SELECT value FROM cli_config WHERE key=?1", [key], |r| {
62 r.get::<_, String>(0)
63 })
64 .optional()?)
65}
66
67fn lot_method_tag(m: LotMethod) -> &'static str {
68 match m {
69 LotMethod::Fifo => "fifo",
70 LotMethod::Lifo => "lifo",
71 LotMethod::Hifo => "hifo",
72 }
73}
74
75pub fn read_config(conn: &Connection) -> Result<CliConfig, CliError> {
86 init_config_table(conn)?;
88
89 let mut cfg = CliConfig::default();
90 if let Some(v) = get(conn, "fee_treatment")? {
91 cfg.fee_treatment = match v.as_str() {
92 "c" => FeeTreatment::TreatmentC,
93 "b" => FeeTreatment::TreatmentB,
94 _ => {
95 return Err(CliError::BadConfigValue {
97 key: "fee_treatment".into(),
98 value: v,
99 });
100 }
101 };
102 }
103 if let Some(v) = get(conn, "pre2025_method")? {
104 cfg.pre2025_method = match v.as_str() {
105 "fifo" => LotMethod::Fifo,
106 "lifo" => LotMethod::Lifo,
107 "hifo" => LotMethod::Hifo,
108 _ => {
109 return Err(CliError::BadConfigValue {
110 key: "pre2025_method".into(),
111 value: v,
112 })
113 }
114 };
115 }
116 if let Some(v) = get(conn, "pre2025_method_attested")? {
117 cfg.pre2025_method_attested = match v.as_str() {
118 "true" => true,
119 "false" => false,
120 _ => {
121 return Err(CliError::BadConfigValue {
122 key: "pre2025_method_attested".into(),
123 value: v,
124 })
125 }
126 };
127 }
128 if let Some(v) = get(conn, "pseudo_reconcile")? {
129 cfg.pseudo_reconcile = match v.as_str() {
130 "true" => true,
131 "false" => false,
132 _ => {
133 return Err(CliError::BadConfigValue {
134 key: "pseudo_reconcile".into(),
135 value: v,
136 })
137 }
138 };
139 }
140 Ok(cfg)
141}
142
143pub fn set_pseudo_reconcile(conn: &Connection, on: bool) -> Result<(), CliError> {
145 conn.execute(
146 "INSERT INTO cli_config(key,value) VALUES('pseudo_reconcile',?1)
147 ON CONFLICT(key) DO UPDATE SET value=excluded.value",
148 [if on { "true" } else { "false" }],
149 )?;
150 Ok(())
151}
152
153pub fn set_pre2025_method(conn: &Connection, m: LotMethod, attested: bool) -> Result<(), CliError> {
155 conn.execute(
156 "INSERT INTO cli_config(key,value) VALUES('pre2025_method',?1)
157 ON CONFLICT(key) DO UPDATE SET value=excluded.value",
158 [lot_method_tag(m)],
159 )?;
160 conn.execute(
161 "INSERT INTO cli_config(key,value) VALUES('pre2025_method_attested',?1)
162 ON CONFLICT(key) DO UPDATE SET value=excluded.value",
163 [if attested { "true" } else { "false" }],
164 )?;
165 Ok(())
166}
167
168pub fn set_fee_treatment(conn: &Connection, t: FeeTreatment) -> Result<(), CliError> {
171 let v = match t {
172 FeeTreatment::TreatmentC => "c",
173 FeeTreatment::TreatmentB => "b",
174 };
175 conn.execute(
176 "INSERT INTO cli_config(key,value) VALUES('fee_treatment',?1)
177 ON CONFLICT(key) DO UPDATE SET value=excluded.value",
178 [v],
179 )?;
180 Ok(())
181}
182
183#[cfg(test)]
184mod tests {
185 use super::*;
186 use btctax_core::{FeeTreatment, LotMethod};
187
188 fn mem() -> rusqlite::Connection {
189 let c = rusqlite::Connection::open_in_memory().unwrap();
190 init_config_table(&c).unwrap();
191 c
192 }
193
194 #[test]
195 fn default_is_treatment_c_user_mandated() {
196 let c = mem();
197 assert_eq!(
198 read_config(&c).unwrap().fee_treatment,
199 FeeTreatment::TreatmentC
200 );
201 }
202
203 #[test]
204 fn set_then_read_b_opt_in_round_trips() {
205 let c = mem();
206 set_fee_treatment(&c, FeeTreatment::TreatmentB).unwrap();
207 assert_eq!(
208 read_config(&c).unwrap().fee_treatment,
209 FeeTreatment::TreatmentB
210 );
211 set_fee_treatment(&c, FeeTreatment::TreatmentC).unwrap();
213 assert_eq!(
214 read_config(&c).unwrap().fee_treatment,
215 FeeTreatment::TreatmentC
216 );
217 }
218
219 #[test]
220 fn default_pre2025_method_is_hifo_unattested() {
221 let c = mem();
224 let cfg = read_config(&c).unwrap();
225 assert!(!matches!(cfg.pre2025_method, LotMethod::Fifo));
226 assert_eq!(cfg.pre2025_method, LotMethod::Hifo);
227 assert!(!cfg.pre2025_method_attested);
228 assert_eq!(cfg.to_projection().pre2025_method, LotMethod::Hifo);
229 assert!(!cfg.to_projection().pre2025_method_attested);
231 }
232 #[test]
233 fn set_pre2025_method_round_trips_with_attestation() {
234 let c = mem();
235 set_pre2025_method(&c, LotMethod::Hifo, true).unwrap();
236 let cfg = read_config(&c).unwrap();
237 assert_eq!(cfg.pre2025_method, LotMethod::Hifo);
238 assert!(cfg.pre2025_method_attested);
239 assert_eq!(cfg.to_projection().pre2025_method, LotMethod::Hifo);
240 assert!(cfg.to_projection().pre2025_method_attested);
242 }
243 #[test]
244 fn bad_pre2025_method_value_is_an_error() {
245 let c = mem();
246 c.execute(
247 "INSERT INTO cli_config(key,value) VALUES('pre2025_method','zzz')",
248 [],
249 )
250 .unwrap();
251 assert!(matches!(read_config(&c).unwrap_err(),
252 CliError::BadConfigValue { ref key, .. } if key == "pre2025_method"));
253 }
254
255 #[test]
258 fn read_config_on_table_less_vault_returns_default() {
259 let c = rusqlite::Connection::open_in_memory().unwrap();
260 let cfg = read_config(&c).unwrap(); assert_eq!(cfg.fee_treatment, FeeTreatment::TreatmentC);
263 }
264
265 #[test]
267 fn pseudo_reconcile_defaults_false_and_round_trips() {
268 let c = mem();
269 assert!(!read_config(&c).unwrap().pseudo_reconcile);
271 assert!(!read_config(&c).unwrap().to_projection().pseudo_reconcile);
272 set_pseudo_reconcile(&c, true).unwrap();
274 assert!(read_config(&c).unwrap().pseudo_reconcile);
275 assert!(read_config(&c).unwrap().to_projection().pseudo_reconcile);
276 set_pseudo_reconcile(&c, false).unwrap();
278 assert!(!read_config(&c).unwrap().pseudo_reconcile);
279 }
280
281 #[test]
282 fn bad_pseudo_reconcile_value_is_an_error() {
283 let c = mem();
284 c.execute(
285 "INSERT INTO cli_config(key,value) VALUES('pseudo_reconcile','maybe')",
286 [],
287 )
288 .unwrap();
289 assert!(matches!(read_config(&c).unwrap_err(),
290 CliError::BadConfigValue { ref key, .. } if key == "pseudo_reconcile"));
291 }
292
293 #[test]
295 fn bad_stored_value_is_an_error_not_silent_default() {
296 let c = mem();
297 c.execute(
299 "INSERT INTO cli_config(key,value) VALUES('fee_treatment','z')",
300 [],
301 )
302 .unwrap();
303 let err = read_config(&c).unwrap_err();
304 assert!(
305 matches!(err, CliError::BadConfigValue { ref key, .. } if key == "fee_treatment"),
306 "expected BadConfigValue, got: {err}"
307 );
308 }
309}