use crate::CliError;
use btctax_core::{FeeTreatment, LotMethod, ProjectionConfig};
use rusqlite::{Connection, OptionalExtension};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CliConfig {
pub fee_treatment: FeeTreatment,
pub pre2025_method: LotMethod,
pub pre2025_method_attested: bool,
pub pseudo_reconcile: bool,
}
impl Default for CliConfig {
fn default() -> Self {
CliConfig {
fee_treatment: FeeTreatment::TreatmentC,
pre2025_method: LotMethod::Hifo,
pre2025_method_attested: false,
pseudo_reconcile: false,
}
}
}
impl CliConfig {
pub fn to_projection(self) -> ProjectionConfig {
ProjectionConfig {
self_transfer_fee: self.fee_treatment,
pre2025_method: self.pre2025_method,
pre2025_method_attested: self.pre2025_method_attested,
pseudo_reconcile: self.pseudo_reconcile,
}
}
}
pub fn init_config_table(conn: &Connection) -> Result<(), CliError> {
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS cli_config (key TEXT PRIMARY KEY, value TEXT NOT NULL);",
)?;
Ok(())
}
fn get(conn: &Connection, key: &str) -> Result<Option<String>, CliError> {
Ok(conn
.query_row("SELECT value FROM cli_config WHERE key=?1", [key], |r| {
r.get::<_, String>(0)
})
.optional()?)
}
fn lot_method_tag(m: LotMethod) -> &'static str {
match m {
LotMethod::Fifo => "fifo",
LotMethod::Lifo => "lifo",
LotMethod::Hifo => "hifo",
}
}
pub fn read_config(conn: &Connection) -> Result<CliConfig, CliError> {
init_config_table(conn)?;
let mut cfg = CliConfig::default();
if let Some(v) = get(conn, "fee_treatment")? {
cfg.fee_treatment = match v.as_str() {
"c" => FeeTreatment::TreatmentC,
"b" => FeeTreatment::TreatmentB,
_ => {
return Err(CliError::BadConfigValue {
key: "fee_treatment".into(),
value: v,
});
}
};
}
if let Some(v) = get(conn, "pre2025_method")? {
cfg.pre2025_method = match v.as_str() {
"fifo" => LotMethod::Fifo,
"lifo" => LotMethod::Lifo,
"hifo" => LotMethod::Hifo,
_ => {
return Err(CliError::BadConfigValue {
key: "pre2025_method".into(),
value: v,
})
}
};
}
if let Some(v) = get(conn, "pre2025_method_attested")? {
cfg.pre2025_method_attested = match v.as_str() {
"true" => true,
"false" => false,
_ => {
return Err(CliError::BadConfigValue {
key: "pre2025_method_attested".into(),
value: v,
})
}
};
}
if let Some(v) = get(conn, "pseudo_reconcile")? {
cfg.pseudo_reconcile = match v.as_str() {
"true" => true,
"false" => false,
_ => {
return Err(CliError::BadConfigValue {
key: "pseudo_reconcile".into(),
value: v,
})
}
};
}
Ok(cfg)
}
pub fn set_pseudo_reconcile(conn: &Connection, on: bool) -> Result<(), CliError> {
conn.execute(
"INSERT INTO cli_config(key,value) VALUES('pseudo_reconcile',?1)
ON CONFLICT(key) DO UPDATE SET value=excluded.value",
[if on { "true" } else { "false" }],
)?;
Ok(())
}
pub fn set_pre2025_method(conn: &Connection, m: LotMethod, attested: bool) -> Result<(), CliError> {
conn.execute(
"INSERT INTO cli_config(key,value) VALUES('pre2025_method',?1)
ON CONFLICT(key) DO UPDATE SET value=excluded.value",
[lot_method_tag(m)],
)?;
conn.execute(
"INSERT INTO cli_config(key,value) VALUES('pre2025_method_attested',?1)
ON CONFLICT(key) DO UPDATE SET value=excluded.value",
[if attested { "true" } else { "false" }],
)?;
Ok(())
}
pub fn set_fee_treatment(conn: &Connection, t: FeeTreatment) -> Result<(), CliError> {
let v = match t {
FeeTreatment::TreatmentC => "c",
FeeTreatment::TreatmentB => "b",
};
conn.execute(
"INSERT INTO cli_config(key,value) VALUES('fee_treatment',?1)
ON CONFLICT(key) DO UPDATE SET value=excluded.value",
[v],
)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use btctax_core::{FeeTreatment, LotMethod};
fn mem() -> rusqlite::Connection {
let c = rusqlite::Connection::open_in_memory().unwrap();
init_config_table(&c).unwrap();
c
}
#[test]
fn default_is_treatment_c_user_mandated() {
let c = mem();
assert_eq!(
read_config(&c).unwrap().fee_treatment,
FeeTreatment::TreatmentC
);
}
#[test]
fn set_then_read_b_opt_in_round_trips() {
let c = mem();
set_fee_treatment(&c, FeeTreatment::TreatmentB).unwrap();
assert_eq!(
read_config(&c).unwrap().fee_treatment,
FeeTreatment::TreatmentB
);
set_fee_treatment(&c, FeeTreatment::TreatmentC).unwrap();
assert_eq!(
read_config(&c).unwrap().fee_treatment,
FeeTreatment::TreatmentC
);
}
#[test]
fn default_pre2025_method_is_hifo_unattested() {
let c = mem();
let cfg = read_config(&c).unwrap();
assert!(!matches!(cfg.pre2025_method, LotMethod::Fifo));
assert_eq!(cfg.pre2025_method, LotMethod::Hifo);
assert!(!cfg.pre2025_method_attested);
assert_eq!(cfg.to_projection().pre2025_method, LotMethod::Hifo);
assert!(!cfg.to_projection().pre2025_method_attested);
}
#[test]
fn set_pre2025_method_round_trips_with_attestation() {
let c = mem();
set_pre2025_method(&c, LotMethod::Hifo, true).unwrap();
let cfg = read_config(&c).unwrap();
assert_eq!(cfg.pre2025_method, LotMethod::Hifo);
assert!(cfg.pre2025_method_attested);
assert_eq!(cfg.to_projection().pre2025_method, LotMethod::Hifo);
assert!(cfg.to_projection().pre2025_method_attested);
}
#[test]
fn bad_pre2025_method_value_is_an_error() {
let c = mem();
c.execute(
"INSERT INTO cli_config(key,value) VALUES('pre2025_method','zzz')",
[],
)
.unwrap();
assert!(matches!(read_config(&c).unwrap_err(),
CliError::BadConfigValue { ref key, .. } if key == "pre2025_method"));
}
#[test]
fn read_config_on_table_less_vault_returns_default() {
let c = rusqlite::Connection::open_in_memory().unwrap();
let cfg = read_config(&c).unwrap(); assert_eq!(cfg.fee_treatment, FeeTreatment::TreatmentC);
}
#[test]
fn pseudo_reconcile_defaults_false_and_round_trips() {
let c = mem();
assert!(!read_config(&c).unwrap().pseudo_reconcile);
assert!(!read_config(&c).unwrap().to_projection().pseudo_reconcile);
set_pseudo_reconcile(&c, true).unwrap();
assert!(read_config(&c).unwrap().pseudo_reconcile);
assert!(read_config(&c).unwrap().to_projection().pseudo_reconcile);
set_pseudo_reconcile(&c, false).unwrap();
assert!(!read_config(&c).unwrap().pseudo_reconcile);
}
#[test]
fn bad_pseudo_reconcile_value_is_an_error() {
let c = mem();
c.execute(
"INSERT INTO cli_config(key,value) VALUES('pseudo_reconcile','maybe')",
[],
)
.unwrap();
assert!(matches!(read_config(&c).unwrap_err(),
CliError::BadConfigValue { ref key, .. } if key == "pseudo_reconcile"));
}
#[test]
fn bad_stored_value_is_an_error_not_silent_default() {
let c = mem();
c.execute(
"INSERT INTO cli_config(key,value) VALUES('fee_treatment','z')",
[],
)
.unwrap();
let err = read_config(&c).unwrap_err();
assert!(
matches!(err, CliError::BadConfigValue { ref key, .. } if key == "fee_treatment"),
"expected BadConfigValue, got: {err}"
);
}
}