diff --git a/.gitignore b/.gitignore index 72c5179..6005ab9 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /*.sqlite /result /.idea +.tmp diff --git a/flake.nix b/flake.nix index e98669c..f184136 100644 --- a/flake.nix +++ b/flake.nix @@ -41,7 +41,9 @@ pre-commit rustPackages.clippy cargo-tarpaulin - deno]; + deno + foreman + postgresql]; RUST_SRC_PATH = rustPlatform.rustLibSrc; }; }); diff --git a/src/db/_accounts.rs b/src/db/_accounts.rs index 9269fdc..28cbd50 100644 --- a/src/db/_accounts.rs +++ b/src/db/_accounts.rs @@ -1,12 +1,12 @@ use fraction::Fraction; -use rusqlite::{params, Connection, OptionalExtension, Result, Row}; +use rusqlite::{params, Connection, ErrorCode, OptionalExtension, Result, Row}; use crate::db::_external_id::{AccountId, Id, UserId}; use crate::db::_securities::Security; use crate::db::external_id::SecurityId; -use crate::util::{CustomResult, Error}; use crate::util::Error::FuckingError; +use crate::util::{CustomResult, Error}; #[derive(Debug)] pub enum AccountType { @@ -15,19 +15,18 @@ pub enum AccountType { } impl AccountType { - - fn from_string(s: & String) -> Option{ + fn from_string(s: &String) -> Option { if s == "security_emitter" { Some(AccountType::SecurityEmitter) - } else if s == "checking"{ + } else if s == "checking" { Some(AccountType::Checking) - }else{ + } else { None } } pub fn from_row(row: &Row) -> Result { let s: String = row.get("acc_account_type")?; - AccountType::from_string(& s).ok_or(rusqlite::Error::ExecuteReturnedResults) + AccountType::from_string(&s).ok_or(rusqlite::Error::ExecuteReturnedResults) } } @@ -41,12 +40,13 @@ impl PartialEq for AccountType { } } -impl ToString for AccountType{ +impl ToString for AccountType { fn to_string(&self) -> String { match self { AccountType::Checking => "checking", AccountType::SecurityEmitter => "security_emitter", - }.to_string() + } + .to_string() } } @@ -107,9 +107,9 @@ impl Account { } } -impl Clone for Account{ +impl Clone for Account { fn clone(&self) -> Self { - Account{ + Account { id: self.id, amount: self.amount, timestamp: self.timestamp, @@ -117,7 +117,6 @@ impl Clone for Account{ account_type: self.account_type.clone(), label: self.label.clone(), } - } } #[derive(Debug)] @@ -192,7 +191,7 @@ pub fn list_accounts<'a>(conn: &Connection) -> CustomResult> { return Ok(ret); } -pub fn list_user_accounts(conn: &Connection, user_id: & UserId) -> CustomResult> { +pub fn list_user_accounts(conn: &Connection, user_id: &UserId) -> CustomResult> { let q = "SELECT acc.id AS acc_id, acc.amount AS acc_amount, UNIXEPOCH(acc.timestamp) AS acc_timestamp, @@ -237,7 +236,10 @@ pub fn get_account_by_id(tx: &Connection, id: &AccountId) -> CustomResult CustomResult> { +pub fn get_checking_accounts_for_sec_id( + tx: &Connection, + sec_id: &SecurityId, +) -> CustomResult> { let q = " SELECT acc.id AS acc_id, acc.amount AS acc_amount, @@ -260,7 +262,6 @@ pub fn get_checking_accounts_for_sec_id(tx: &Connection, sec_id: &SecurityId) -> .map(Result::unwrap) .collect(); return Ok(ret); - } fn create_account( @@ -353,7 +354,7 @@ impl AccountChangeReceipt { amount: row.get("amount")?, timestamp: row.get("timestamp")?, security_id: SecurityId::from_id(row.get("security_id")?), - account_type: AccountType::from_row(row)? + account_type: AccountType::from_row(row)?, }) } } @@ -373,6 +374,13 @@ fn update_account_history(tx: &Connection, account_id: &AccountId) -> CustomResu Ok(()) } +fn is_invalid_amount_error(e: &rusqlite::Error) -> bool { + match e { + rusqlite::Error::SqliteFailure(uf, _) => uf.code == ErrorCode::ConstraintViolation, + _ => false, + } +} + /// Super low level functions that needs to be wrapped in something else. /// /// This method does NOT guarantee that all amount added into an account @@ -398,16 +406,23 @@ pub fn add_to_account( security AS security_id "; let params = params![amount, account_id]; - let ret = tx - .query_row(q, params, AccountChangeReceipt::from_row) - .map_err(Error::SqlError); + let res = tx.query_row(q, params, AccountChangeReceipt::from_row); + let ret = res.map_err(|e| { + if is_invalid_amount_error(&e) { + Error::InvalidAmount(format!("Invalid value {:} added to account {:}", amount, account_id)) + } else { + Error::SqlError(e) + } + })?; update_account_history(tx, account_id)?; - return ret; + return Ok(ret); } -pub fn get_number_of_accounts(tx: &Connection, - sec: &SecurityId, - account_type: AccountType) -> CustomResult{ +pub fn get_number_of_accounts( + tx: &Connection, + sec: &SecurityId, + account_type: AccountType, +) -> CustomResult { let q = " SELECT COUNT(*) AS count FROM accounts @@ -415,15 +430,15 @@ pub fn get_number_of_accounts(tx: &Connection, AND account_type = ? "; let params = params![sec, account_type.to_string()]; - tx.query_row(q, params, |row| row.get("count")).map_err(Error::SqlError) + tx.query_row(q, params, |row| row.get("count")) + .map_err(Error::SqlError) } /// Market cap is computer by summing the security emitter accounts. /// We use this technique because the sum of the security /// emitter accounts MUST equals (more or less very tiny interest /// amounts) the sum of the checking accounts. -pub fn get_security_market_cap(tx: &Connection, - sec: &SecurityId) -> CustomResult { +pub fn get_security_market_cap(tx: &Connection, sec: &SecurityId) -> CustomResult { let q = " SELECT COALESCE(-1 * SUM(amount), 0) AS count FROM accounts @@ -431,7 +446,8 @@ pub fn get_security_market_cap(tx: &Connection, AND account_type = ? "; let params = params![sec, AccountType::SecurityEmitter.to_string()]; - tx.query_row(q, params, |row| row.get("count")).map_err(Error::SqlError) + tx.query_row(q, params, |row| row.get("count")) + .map_err(Error::SqlError) } #[cfg(test)] @@ -472,14 +488,13 @@ mod test_accounts { return Ok(ret); } - fn assert_test_account_type_to_string_and_from_row_works(account_type: AccountType){ + fn assert_test_account_type_to_string_and_from_row_works(account_type: AccountType) { let serialized = account_type.to_string(); assert_eq!(AccountType::from_string(&serialized).unwrap(), account_type); - } #[test] - fn test_account_type_to_string_and_from_row(){ + fn test_account_type_to_string_and_from_row() { assert_test_account_type_to_string_and_from_row_works(AccountType::Checking); assert_test_account_type_to_string_and_from_row_works(AccountType::SecurityEmitter); } @@ -492,10 +507,12 @@ mod test_accounts { let sec = secs.get(0).unwrap(); let tx = conn.transaction().unwrap(); - let acc = create_checking_account(&tx, - &sec.id(), - &user.id(), - &format!("Account for {:} in {:}", sec.id(), user.id()))?; + let acc = create_checking_account( + &tx, + &sec.id(), + &user.id(), + &format!("Account for {:} in {:}", sec.id(), user.id()), + )?; assert_eq!(acc.account_type, AccountType::Checking); assert_eq!(0, acc.raw_amount()); @@ -567,7 +584,7 @@ mod test_accounts { let tx = conn.transaction().unwrap(); let sec = secs.get(0).unwrap().id(); let user = users.get(0).unwrap().id(); - let acc = create_checking_account(&tx, &sec, &user, & "acc".to_string()).unwrap(); + let acc = create_checking_account(&tx, &sec, &user, &"acc".to_string()).unwrap(); let receipt = add_to_account(&tx, &acc.id(), 33)?; assert_eq!(receipt.account_id, acc.id()); assert_eq!(receipt.amount, 33); @@ -591,7 +608,7 @@ mod test_accounts { let tx = conn.transaction().unwrap(); let sec = secs.get(0).unwrap().id(); let user = users.get(0).unwrap().id(); - let acc = create_security_emitter_account(&tx, &sec, &user, & "acc".to_string())?; + let acc = create_security_emitter_account(&tx, &sec, &user, &"acc".to_string())?; let receipt = add_to_account(&tx, &acc.id(), -33)?; assert_eq!(receipt.account_id, acc.id()); @@ -608,16 +625,30 @@ mod test_accounts { Ok(()) } + #[test] + fn test_add_to_account_unfounded_err() -> CustomResult<()> { + let (mut conn, users, secs) = setup(); + let sec = secs.get(0).unwrap().id(); + let user = users.get(0).unwrap().id(); + // let sec_emitter_acc = create_security_emitter_account(&conn, &sec, &user, & "sec emitter acc".to_string())?; + let checking_account = + create_checking_account(&conn, &sec, &user, &"checking acc".to_string())?; + match add_to_account(&conn, &checking_account.id(), -33) { + Err(Error::InvalidAmount(_)) => Ok(()), + _ => Err(Error::Testing("Expected InvalidAmount error".to_string())) + } + } + #[test] fn test_list_user_accounts() -> CustomResult<()> { let (mut conn, users, secs) = setup(); { let tx = conn.transaction().unwrap(); - create_all_accounts(& tx, &users, &secs)?; + create_all_accounts(&tx, &users, &secs)?; tx.commit().map_err(Error::SqlError)?; } - let res = list_user_accounts(& conn, & users[0].id())?; + let res = list_user_accounts(&conn, &users[0].id())?; assert_eq!(res.len(), 2); Ok(()) } @@ -625,15 +656,28 @@ mod test_accounts { #[test] fn test_get_number_of_accounts() -> CustomResult<()> { let (mut conn, users, secs) = setup(); - create_all_accounts(&conn, &users, &secs)?; + create_all_accounts(&conn, &users, &secs)?; let n = get_number_of_accounts(&conn, &secs.get(0).unwrap().id(), AccountType::Checking)?; assert_eq!(n, 2); - let n = get_number_of_accounts(&conn, &secs.get(0).unwrap().id(), AccountType::SecurityEmitter)?; + let n = get_number_of_accounts( + &conn, + &secs.get(0).unwrap().id(), + AccountType::SecurityEmitter, + )?; assert_eq!(n, 0); - create_security_emitter_account(&conn, &secs.get(0).unwrap().id(), &users.get(0).unwrap().id(), &"".to_string())?; - let n = get_number_of_accounts(&conn, &secs.get(0).unwrap().id(), AccountType::SecurityEmitter)?; + create_security_emitter_account( + &conn, + &secs.get(0).unwrap().id(), + &users.get(0).unwrap().id(), + &"".to_string(), + )?; + let n = get_number_of_accounts( + &conn, + &secs.get(0).unwrap().id(), + AccountType::SecurityEmitter, + )?; assert_eq!(n, 1); Ok(()) } @@ -641,9 +685,14 @@ mod test_accounts { #[test] fn test_get_security_market_cap() -> CustomResult<()> { let (mut conn, users, secs) = setup(); - let accounts = create_all_accounts(&conn, &users, &secs.get(0..1).unwrap().to_vec())?; + let accounts = create_all_accounts(&conn, &users, &secs.get(0..1).unwrap().to_vec())?; let sec_id = &secs.get(0).unwrap().id(); - let sec_emitter = create_security_emitter_account(&conn, sec_id, &users.get(0).unwrap().id(), &"".to_string())?; + let sec_emitter = create_security_emitter_account( + &conn, + sec_id, + &users.get(0).unwrap().id(), + &"".to_string(), + )?; let n = get_security_market_cap(&conn, sec_id)?; assert_eq!(n, 0);