use crate::models::{BankAccount, Settings}; use crate::AppState; use rusqlite::{params, Connection, Row}; use tauri::State; pub(crate) const SETTINGS_COLS: &str = "vendor_name, vendor_address, vendor_email, vendor_phone, vendor_pan, vendor_gstin, vendor_state_code, logo_path, signature_path, default_bank_id, default_tax_rate, default_tax_type, payment_terms_days, currency, onboarded, theme, gst_registration, default_hsn_sac, signatory_name, signatory_designation, render_prefs, last_export_dir"; pub(crate) fn map_settings(row: &Row) -> rusqlite::Result { Ok(Settings { vendor_name: row.get(0)?, vendor_address: row.get(1)?, vendor_email: row.get(2)?, vendor_phone: row.get(3)?, vendor_pan: row.get(4)?, vendor_gstin: row.get(5)?, vendor_state_code: row.get(6)?, logo_path: row.get(7)?, signature_path: row.get(8)?, default_bank_id: row.get(9)?, default_tax_rate: row.get(10)?, default_tax_type: row.get(11)?, payment_terms_days: row.get(12)?, currency: row.get(13)?, onboarded: row.get::<_, i64>(14)? != 0, theme: row.get(15)?, gst_registration: row.get(16)?, default_hsn_sac: row.get(17)?, signatory_name: row.get(18)?, signatory_designation: row.get(19)?, render_prefs: row.get(20)?, last_export_dir: row.get(21)?, }) } #[tauri::command] pub fn get_settings(state: State) -> Result { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.query_row( &format!("SELECT {} FROM app_settings WHERE id = 1", SETTINGS_COLS), [], map_settings, ) .map_err(|e| e.to_string()) } #[tauri::command] pub fn save_settings(state: State, settings: Settings) -> Result { let conn = state.db.lock().map_err(|e| e.to_string())?; conn.execute( r#"UPDATE app_settings SET vendor_name = ?1, vendor_address = ?2, vendor_email = ?3, vendor_phone = ?4, vendor_pan = ?5, vendor_gstin = ?6, vendor_state_code = ?7, logo_path = ?8, signature_path = ?9, default_bank_id = ?10, default_tax_rate = ?11, default_tax_type = ?12, payment_terms_days = ?13, currency = ?14, onboarded = ?15, theme = ?16, gst_registration = ?17, default_hsn_sac = ?18, signatory_name = ?19, signatory_designation = ?20, render_prefs = ?21 WHERE id = 1"#, params![ settings.vendor_name, settings.vendor_address, settings.vendor_email, settings.vendor_phone, settings.vendor_pan, settings.vendor_gstin, settings.vendor_state_code, settings.logo_path, settings.signature_path, settings.default_bank_id, settings.default_tax_rate, settings.default_tax_type, settings.payment_terms_days, settings.currency, if settings.onboarded { 1 } else { 0 }, settings.theme, settings.gst_registration, settings.default_hsn_sac, settings.signatory_name, settings.signatory_designation, settings.render_prefs, ], ) .map_err(|e| e.to_string())?; drop(conn); get_settings(state) } pub(crate) fn map_bank(row: &Row) -> rusqlite::Result { Ok(BankAccount { id: Some(row.get(0)?), label: row.get(1)?, bank_name: row.get(2)?, account_name: row.get(3)?, account_no: row.get(4)?, branch: row.get(5)?, ifsc: row.get(6)?, is_default: row.get::<_, i64>(7)? != 0, }) } #[tauri::command] pub fn list_banks(state: State) -> Result, String> { let conn = state.db.lock().map_err(|e| e.to_string())?; let mut stmt = conn .prepare( "SELECT id, label, bank_name, account_name, account_no, branch, ifsc, is_default FROM bank_accounts ORDER BY is_default DESC, id ASC", ) .map_err(|e| e.to_string())?; let rows = stmt .query_map([], map_bank) .map_err(|e| e.to_string())? .collect::>>() .map_err(|e| e.to_string())?; Ok(rows) } #[tauri::command] pub fn save_bank(state: State, bank: BankAccount) -> Result { let conn = state.db.lock().map_err(|e| e.to_string())?; let is_default = if bank.is_default { 1 } else { 0 }; let id = match bank.id { Some(id) => { conn.execute( r#"UPDATE bank_accounts SET label=?1, bank_name=?2, account_name=?3, account_no=?4, branch=?5, ifsc=?6, is_default=?7 WHERE id=?8"#, params![ bank.label, bank.bank_name, bank.account_name, bank.account_no, bank.branch, bank.ifsc, is_default, id ], ) .map_err(|e| e.to_string())?; id } None => { conn.execute( r#"INSERT INTO bank_accounts (label, bank_name, account_name, account_no, branch, ifsc, is_default) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7)"#, params![ bank.label, bank.bank_name, bank.account_name, bank.account_no, bank.branch, bank.ifsc, is_default ], ) .map_err(|e| e.to_string())?; conn.last_insert_rowid() } }; if is_default == 1 { conn.execute( "UPDATE bank_accounts SET is_default = 0 WHERE id <> ?1", params![id], ) .map_err(|e| e.to_string())?; conn.execute( "UPDATE app_settings SET default_bank_id = ?1 WHERE id = 1", params![id], ) .map_err(|e| e.to_string())?; } conn.query_row( "SELECT id, label, bank_name, account_name, account_no, branch, ifsc, is_default FROM bank_accounts WHERE id = ?1", params![id], map_bank, ) .map_err(|e| e.to_string()) } /// Deletes a bank account. If it was the default, the lowest remaining account /// becomes the default (or the default is cleared when none is left). Issued invoices /// keep their own `bank_snapshot`, so they are unaffected. pub fn delete_bank_impl(conn: &mut Connection, id: i64) -> Result<(), String> { let tx = conn.transaction().map_err(|e| e.to_string())?; let was_default: bool = tx .query_row( "SELECT COALESCE((SELECT is_default FROM bank_accounts WHERE id = ?1), 0) <> 0 OR COALESCE((SELECT default_bank_id FROM app_settings WHERE id = 1), 0) = ?1", params![id], |r| r.get(0), ) .map_err(|e| e.to_string())?; tx.execute("DELETE FROM bank_accounts WHERE id = ?1", params![id]) .map_err(|e| e.to_string())?; if was_default { let next: Option = tx .query_row("SELECT MIN(id) FROM bank_accounts", [], |r| r.get(0)) .map_err(|e| e.to_string())?; tx.execute("UPDATE bank_accounts SET is_default = 0", []) .map_err(|e| e.to_string())?; if let Some(next) = next { tx.execute("UPDATE bank_accounts SET is_default = 1 WHERE id = ?1", params![next]) .map_err(|e| e.to_string())?; } tx.execute("UPDATE app_settings SET default_bank_id = ?1 WHERE id = 1", params![next]) .map_err(|e| e.to_string())?; } tx.commit().map_err(|e| e.to_string()) } #[tauri::command] pub fn delete_bank(state: State, id: i64) -> Result<(), String> { let mut conn = state.db.lock().map_err(|e| e.to_string())?; delete_bank_impl(&mut conn, id) } #[cfg(test)] mod tests { use super::*; fn add_bank(conn: &Connection, name: &str, is_default: bool) -> i64 { conn.execute( "INSERT INTO bank_accounts (bank_name, is_default) VALUES (?1, ?2)", params![name, is_default], ) .unwrap(); conn.last_insert_rowid() } fn default_state(conn: &Connection) -> (Option, Vec) { let setting = conn .query_row("SELECT default_bank_id FROM app_settings WHERE id = 1", [], |r| r.get(0)) .unwrap(); let mut stmt = conn .prepare("SELECT id FROM bank_accounts WHERE is_default = 1 ORDER BY id") .unwrap(); let flagged = stmt.query_map([], |r| r.get(0)).unwrap().map(Result::unwrap).collect(); (setting, flagged) } #[test] fn settings_carry_render_prefs() { let conn = crate::db::open_in_memory().unwrap(); let read = |conn: &Connection| { conn.query_row( &format!("SELECT {} FROM app_settings WHERE id = 1", SETTINGS_COLS), [], map_settings, ) .unwrap() }; assert_eq!(read(&conn).render_prefs, ""); conn.execute("UPDATE app_settings SET render_prefs = ?1 WHERE id = 1", params!["{\"version\":1}"]) .unwrap(); let settings = read(&conn); assert_eq!(settings.render_prefs, "{\"version\":1}"); let json = serde_json::to_value(&settings).unwrap(); assert_eq!(json["renderPrefs"], "{\"version\":1}"); } #[test] fn deleting_the_default_bank_promotes_the_lowest_remaining() { let mut conn = crate::db::open_in_memory().unwrap(); // The seeded "Primary" bank is the default; add two more. let seeded: i64 = conn .query_row("SELECT id FROM bank_accounts", [], |r| r.get(0)) .unwrap(); let b = add_bank(&conn, "B", false); let c = add_bank(&conn, "C", false); assert_eq!(default_state(&conn), (Some(seeded), vec![seeded])); delete_bank_impl(&mut conn, seeded).unwrap(); assert_eq!(default_state(&conn), (Some(b), vec![b])); delete_bank_impl(&mut conn, b).unwrap(); assert_eq!(default_state(&conn), (Some(c), vec![c])); delete_bank_impl(&mut conn, c).unwrap(); assert_eq!(default_state(&conn), (None, vec![])); } #[test] fn deleting_a_non_default_bank_keeps_the_default() { let mut conn = crate::db::open_in_memory().unwrap(); let seeded: i64 = conn .query_row("SELECT id FROM bank_accounts", [], |r| r.get(0)) .unwrap(); let b = add_bank(&conn, "B", false); delete_bank_impl(&mut conn, b).unwrap(); assert_eq!(default_state(&conn), (Some(seeded), vec![seeded])); } }