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