357 lines
13 KiB
Rust
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]));
|
|
}
|
|
}
|