use crate::CliError;
use btctax_core::EventId;
use rusqlite::Connection;
use std::collections::BTreeMap;
pub fn init_table(conn: &Connection) -> Result<(), CliError> {
conn.execute_batch(
"CREATE TABLE IF NOT EXISTS bulk_estimated_proceeds \
(out_event TEXT PRIMARY KEY, date_marked TEXT NOT NULL);",
)?;
Ok(())
}
pub fn mark(conn: &Connection, out_event: &EventId, date_marked: &str) -> Result<(), CliError> {
init_table(conn)?;
conn.execute(
"INSERT INTO bulk_estimated_proceeds(out_event,date_marked) VALUES(?1,?2) \
ON CONFLICT(out_event) DO UPDATE SET date_marked=excluded.date_marked",
rusqlite::params![out_event.canonical(), date_marked],
)?;
Ok(())
}
pub fn clear(conn: &Connection, out_event: &EventId) -> Result<(), CliError> {
init_table(conn)?;
conn.execute(
"DELETE FROM bulk_estimated_proceeds WHERE out_event=?1",
[out_event.canonical()],
)?;
Ok(())
}
pub fn all(conn: &Connection) -> Result<BTreeMap<EventId, String>, CliError> {
init_table(conn)?;
let mut stmt = conn
.prepare("SELECT out_event, date_marked FROM bulk_estimated_proceeds ORDER BY out_event")?;
let rows = stmt.query_map([], |r| Ok((r.get::<_, String>(0)?, r.get::<_, String>(1)?)))?;
let mut out = BTreeMap::new();
for row in rows {
let (ev_str, date_marked) = row?;
out.insert(crate::eventref::parse_event_id(&ev_str)?, date_marked);
}
Ok(out)
}
#[cfg(test)]
mod tests {
use super::*;
use btctax_core::{EventId, Source, SourceRef};
fn mem() -> rusqlite::Connection {
let c = rusqlite::Connection::open_in_memory().unwrap();
init_table(&c).unwrap();
c
}
fn eid(label: &str) -> EventId {
EventId::import(Source::Coinbase, SourceRef::new(label.to_string()))
}
#[test]
fn mark_then_all_contains_the_key() {
let c = mem();
let e = eid("out|bulk-est-1");
mark(&c, &e, "2026-07-03").unwrap();
let m = all(&c).unwrap();
assert_eq!(m.get(&e).map(String::as_str), Some("2026-07-03"));
}
#[test]
fn clear_removes_the_flag_and_is_idempotent() {
let c = mem();
let e = eid("out|bulk-est-clear");
mark(&c, &e, "2026-07-03").unwrap();
assert!(all(&c).unwrap().contains_key(&e));
clear(&c, &e).unwrap();
assert!(!all(&c).unwrap().contains_key(&e));
clear(&c, &e).unwrap();
assert!(!all(&c).unwrap().contains_key(&e));
}
#[test]
fn clear_absent_key_is_ok() {
let c = mem();
clear(&c, &eid("out|never-marked")).unwrap();
assert!(all(&c).unwrap().is_empty());
}
#[test]
fn mark_upserts_last_write_wins() {
let c = mem();
let e = eid("out|bulk-est-upsert");
mark(&c, &e, "2026-01-01").unwrap();
mark(&c, &e, "2026-07-03").unwrap();
assert_eq!(
all(&c).unwrap().get(&e).map(String::as_str),
Some("2026-07-03")
);
}
#[test]
fn all_on_tableless_vault_returns_empty() {
let c = rusqlite::Connection::open_in_memory().unwrap(); assert!(all(&c).unwrap().is_empty());
}
#[test]
fn defensive_guard_in_mark_creates_table() {
let c = rusqlite::Connection::open_in_memory().unwrap(); let e = eid("out|guard");
mark(&c, &e, "2026-07-03").unwrap();
assert!(all(&c).unwrap().contains_key(&e));
}
#[test]
fn all_returns_btreemap_in_deterministic_order() {
let c = mem();
let e1 = eid("out|alpha");
let e2 = eid("out|beta");
mark(&c, &e2, "2026-07-03").unwrap();
mark(&c, &e1, "2026-07-02").unwrap();
let m = all(&c).unwrap();
assert_eq!(m.len(), 2);
assert!(m.contains_key(&e1) && m.contains_key(&e2));
}
}