Files
Voiced/src-tauri/src/commands/settings.rs
T
soubarna b6cf46b883 style(src-tauri): cargo fmt
Pure mechanical rustfmt reformat (default config, no rustfmt.toml).
2026-10-05 17:19:13 +05:30

357 lines
13 KiB
Rust

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, logo_original_path, logo_knockout_path, logo_meta,
logo_includes_name";
pub(crate) fn map_settings(row: &Row) -> rusqlite::Result<Settings> {
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)?,
logo_original_path: row.get(22)?,
logo_knockout_path: row.get(23)?,
logo_meta: row.get(24)?,
logo_includes_name: row.get(25)?,
})
}
#[tauri::command]
pub fn get_settings(state: State<AppState>) -> Result<Settings, String> {
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<AppState>, settings: Settings) -> Result<Settings, String> {
if !matches!(settings.logo_includes_name.as_str(), "auto" | "yes" | "no") {
return Err("Logo includes name must be auto, yes or no".into());
}
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,
logo_original_path = ?22, logo_knockout_path = ?23, logo_meta = ?24,
logo_includes_name = ?25
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,
settings.logo_original_path,
settings.logo_knockout_path,
settings.logo_meta,
settings.logo_includes_name,
],
)
.map_err(|e| e.to_string())?;
drop(conn);
get_settings(state)
}
pub(crate) fn map_bank(row: &Row) -> rusqlite::Result<BankAccount> {
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<AppState>) -> Result<Vec<BankAccount>, 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::<rusqlite::Result<Vec<_>>>()
.map_err(|e| e.to_string())?;
Ok(rows)
}
#[tauri::command]
pub fn save_bank(state: State<AppState>, bank: BankAccount) -> Result<BankAccount, String> {
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<i64> = 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<AppState>, 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<i64>, Vec<i64>) {
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 settings_carry_logo_columns() {
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()
};
let fresh = read(&conn);
assert_eq!(fresh.logo_original_path, None);
assert_eq!(fresh.logo_knockout_path, None);
assert_eq!(fresh.logo_meta, "");
assert_eq!(fresh.logo_includes_name, "auto");
conn.execute(
"UPDATE app_settings SET logo_original_path = 'assets/o.png', logo_knockout_path = 'assets/k.png',
logo_meta = '{}', logo_includes_name = 'no' WHERE id = 1",
[],
)
.unwrap();
let json = serde_json::to_value(read(&conn)).unwrap();
assert_eq!(json["logoOriginalPath"], "assets/o.png");
assert_eq!(json["logoKnockoutPath"], "assets/k.png");
assert_eq!(json["logoMeta"], "{}");
assert_eq!(json["logoIncludesName"], "no");
}
#[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]));
}
}