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}
16
17impl Default for CliConfig {
18 fn default() -> Self {
19 CliConfig {
21 fee_treatment: FeeTreatment::TreatmentC,
22 pre2025_method: LotMethod::Fifo,
23 pre2025_method_attested: false,
24 }
25 }
26}
27
28impl CliConfig {
29 pub fn to_projection(self) -> ProjectionConfig {
31 ProjectionConfig {
32 self_transfer_fee: self.fee_treatment,
33 pre2025_method: self.pre2025_method,
34 pre2025_method_attested: self.pre2025_method_attested,
35 }
36 }
37}
38
39pub fn init_config_table(conn: &Connection) -> Result<(), CliError> {
44 conn.execute_batch(
45 "CREATE TABLE IF NOT EXISTS cli_config (key TEXT PRIMARY KEY, value TEXT NOT NULL);",
46 )?;
47 Ok(())
48}
49
50fn get(conn: &Connection, key: &str) -> Result<Option<String>, CliError> {
51 Ok(conn
52 .query_row("SELECT value FROM cli_config WHERE key=?1", [key], |r| {
53 r.get::<_, String>(0)
54 })
55 .optional()?)
56}
57
58fn lot_method_tag(m: LotMethod) -> &'static str {
59 match m {
60 LotMethod::Fifo => "fifo",
61 LotMethod::Lifo => "lifo",
62 LotMethod::Hifo => "hifo",
63 }
64}
65
66pub fn read_config(conn: &Connection) -> Result<CliConfig, CliError> {
77 init_config_table(conn)?;
79
80 let mut cfg = CliConfig::default();
81 if let Some(v) = get(conn, "fee_treatment")? {
82 cfg.fee_treatment = match v.as_str() {
83 "c" => FeeTreatment::TreatmentC,
84 "b" => FeeTreatment::TreatmentB,
85 _ => {
86 return Err(CliError::BadConfigValue {
88 key: "fee_treatment".into(),
89 value: v,
90 });
91 }
92 };
93 }
94 if let Some(v) = get(conn, "pre2025_method")? {
95 cfg.pre2025_method = match v.as_str() {
96 "fifo" => LotMethod::Fifo,
97 "lifo" => LotMethod::Lifo,
98 "hifo" => LotMethod::Hifo,
99 _ => {
100 return Err(CliError::BadConfigValue {
101 key: "pre2025_method".into(),
102 value: v,
103 })
104 }
105 };
106 }
107 if let Some(v) = get(conn, "pre2025_method_attested")? {
108 cfg.pre2025_method_attested = match v.as_str() {
109 "true" => true,
110 "false" => false,
111 _ => {
112 return Err(CliError::BadConfigValue {
113 key: "pre2025_method_attested".into(),
114 value: v,
115 })
116 }
117 };
118 }
119 Ok(cfg)
120}
121
122pub fn set_pre2025_method(conn: &Connection, m: LotMethod, attested: bool) -> Result<(), CliError> {
124 conn.execute(
125 "INSERT INTO cli_config(key,value) VALUES('pre2025_method',?1)
126 ON CONFLICT(key) DO UPDATE SET value=excluded.value",
127 [lot_method_tag(m)],
128 )?;
129 conn.execute(
130 "INSERT INTO cli_config(key,value) VALUES('pre2025_method_attested',?1)
131 ON CONFLICT(key) DO UPDATE SET value=excluded.value",
132 [if attested { "true" } else { "false" }],
133 )?;
134 Ok(())
135}
136
137pub fn set_fee_treatment(conn: &Connection, t: FeeTreatment) -> Result<(), CliError> {
140 let v = match t {
141 FeeTreatment::TreatmentC => "c",
142 FeeTreatment::TreatmentB => "b",
143 };
144 conn.execute(
145 "INSERT INTO cli_config(key,value) VALUES('fee_treatment',?1)
146 ON CONFLICT(key) DO UPDATE SET value=excluded.value",
147 [v],
148 )?;
149 Ok(())
150}
151
152#[cfg(test)]
153mod tests {
154 use super::*;
155 use btctax_core::{FeeTreatment, LotMethod};
156
157 fn mem() -> rusqlite::Connection {
158 let c = rusqlite::Connection::open_in_memory().unwrap();
159 init_config_table(&c).unwrap();
160 c
161 }
162
163 #[test]
164 fn default_is_treatment_c_user_mandated() {
165 let c = mem();
166 assert_eq!(
167 read_config(&c).unwrap().fee_treatment,
168 FeeTreatment::TreatmentC
169 );
170 }
171
172 #[test]
173 fn set_then_read_b_opt_in_round_trips() {
174 let c = mem();
175 set_fee_treatment(&c, FeeTreatment::TreatmentB).unwrap();
176 assert_eq!(
177 read_config(&c).unwrap().fee_treatment,
178 FeeTreatment::TreatmentB
179 );
180 set_fee_treatment(&c, FeeTreatment::TreatmentC).unwrap();
182 assert_eq!(
183 read_config(&c).unwrap().fee_treatment,
184 FeeTreatment::TreatmentC
185 );
186 }
187
188 #[test]
189 fn default_pre2025_method_is_fifo_unattested() {
190 let c = mem();
191 let cfg = read_config(&c).unwrap();
192 assert!(!matches!(cfg.pre2025_method, LotMethod::Hifo));
193 assert_eq!(cfg.pre2025_method, LotMethod::Fifo);
194 assert!(!cfg.pre2025_method_attested);
195 assert_eq!(cfg.to_projection().pre2025_method, LotMethod::Fifo);
196 assert!(!cfg.to_projection().pre2025_method_attested);
198 }
199 #[test]
200 fn set_pre2025_method_round_trips_with_attestation() {
201 let c = mem();
202 set_pre2025_method(&c, LotMethod::Hifo, true).unwrap();
203 let cfg = read_config(&c).unwrap();
204 assert_eq!(cfg.pre2025_method, LotMethod::Hifo);
205 assert!(cfg.pre2025_method_attested);
206 assert_eq!(cfg.to_projection().pre2025_method, LotMethod::Hifo);
207 assert!(cfg.to_projection().pre2025_method_attested);
209 }
210 #[test]
211 fn bad_pre2025_method_value_is_an_error() {
212 let c = mem();
213 c.execute(
214 "INSERT INTO cli_config(key,value) VALUES('pre2025_method','zzz')",
215 [],
216 )
217 .unwrap();
218 assert!(matches!(read_config(&c).unwrap_err(),
219 CliError::BadConfigValue { ref key, .. } if key == "pre2025_method"));
220 }
221
222 #[test]
225 fn read_config_on_table_less_vault_returns_default() {
226 let c = rusqlite::Connection::open_in_memory().unwrap();
227 let cfg = read_config(&c).unwrap(); assert_eq!(cfg.fee_treatment, FeeTreatment::TreatmentC);
230 }
231
232 #[test]
234 fn bad_stored_value_is_an_error_not_silent_default() {
235 let c = mem();
236 c.execute(
238 "INSERT INTO cli_config(key,value) VALUES('fee_treatment','z')",
239 [],
240 )
241 .unwrap();
242 let err = read_config(&c).unwrap_err();
243 assert!(
244 matches!(err, CliError::BadConfigValue { ref key, .. } if key == "fee_treatment"),
245 "expected BadConfigValue, got: {err}"
246 );
247 }
248}