use fraction::Fraction; use rusqlite::{params, Connection, 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}; #[derive(PartialEq, Debug)] pub enum AccountType { Checking, SecurityEmitter, } impl AccountType { pub fn from_row(row: &Row) -> Result { let s: String = row.get("acc_account_type")?; if s == "security_emitter" { Ok(AccountType::SecurityEmitter) } else { Ok(AccountType::Checking) } } } pub struct Account { id: i64, amount: i64, timestamp: i64, security: Security, account_type: AccountType, } impl Account { pub fn id(&self) -> AccountId { AccountId::from_id(self.id) } pub fn from_row(row: &Row) -> Result { return Ok(Account { id: row.get("acc_id")?, amount: row.get("acc_amount")?, timestamp: row.get("acc_timestamp")?, security: Security::from_row(row)?, account_type: AccountType::from_row(row)?, }); } pub fn raw_amount(&self) -> i64 { self.amount } } #[derive(Debug)] struct AccountSnapshot { amount: i64, version: i64, timestamp: i64, } impl AccountSnapshot { pub fn from_row(row: &Row) -> Result { Ok(AccountSnapshot { amount: row.get("amount")?, version: row.get("account_version")?, timestamp: row.get("timestamp")?, }) } } struct AccountHistory { id: i64, security: Security, account_type: AccountType, snapshots: Vec, } impl AccountHistory { pub fn id(&self) -> AccountId { AccountId::from_id(self.id) } pub fn len(&self) -> usize { self.snapshots.len() } pub fn average_amount(&self) -> Fraction { let num: i64 = self.snapshots.iter().map(|x| x.amount).sum(); Fraction::from(num) / Fraction::from(self.len()) } pub fn new(account: Account, snapshots: Vec) -> CustomResult { Ok(AccountHistory { id: account.id, security: account.security, account_type: account.account_type, snapshots, }) } } pub fn list_accounts<'a>(conn: &Connection) -> CustomResult> { let q = "SELECT acc.id AS acc_id, acc.amount AS acc_amount, UNIXEPOCH(acc.timestamp) AS acc_timestamp, account_type AS acc_account_type, sec.id AS sec_id, sec.symbol AS sec_symbol, sec.security_type AS sec_security_type FROM accounts acc JOIN securities sec ON acc.security = sec.id ORDER BY acc.id ASC"; let mut stmt = conn.prepare(q).map_err(Error::SqlError)?; let ret: Vec = stmt .query_map([], &Account::from_row) .map_err(Error::SqlError)? .filter(Result::is_ok) .map(Result::unwrap) .collect(); return Ok(ret); } pub fn get_account_by_id(tx: &Connection, id: &AccountId) -> CustomResult { let q = "SELECT acc.id AS acc_id, acc.amount AS acc_amount, UNIXEPOCH(acc.timestamp) AS acc_timestamp, account_type AS acc_account_type, sec.id AS sec_id, sec.symbol AS sec_symbol, sec.security_type AS sec_security_type FROM accounts acc JOIN securities sec ON acc.security = sec.id WHERE acc.id = ?1"; return tx .query_row(q, params![id], Account::from_row) .map_err(Error::SqlError); } fn create_account( tx: &Connection, sec: &SecurityId, user: &UserId, account_type: AccountType, ) -> CustomResult { let q = "INSERT INTO accounts(security, version, amount, timestamp, account_type) VALUES (?1, 0, 0, CURRENT_TIMESTAMP, ?2)"; let account_type = match account_type { AccountType::Checking => "checking", AccountType::SecurityEmitter => "security_emitter", }; tx.execute(q, (sec, account_type)) .map_err(Error::SqlError)?; let id = AccountId::from_id(tx.last_insert_rowid() as i64); let q = "INSERT INTO account_user_permissions(user_id, account_id) VALUES(?1, ?2)"; tx.execute(q, params![user, id]).map_err(Error::SqlError)?; update_account_history(tx, &id)?; return get_account_by_id(tx, &id); } fn get_account_history(tx: &Connection, id: &AccountId) -> CustomResult { let q = " SELECT account_version, amount, UNIXEPOCH(timestamp) AS timestamp FROM accounts_history WHERE account_id = ?1 ORDER BY account_version DESC"; let mut stmt = tx.prepare(q).map_err(Error::SqlError)?; let snapshots: Vec = stmt .query_map(params![id], AccountSnapshot::from_row) .map_err(Error::SqlError)? .filter(Result::is_ok) .map(Result::unwrap) .collect(); return AccountHistory::new(get_account_by_id(tx, id)?, snapshots); } pub fn create_checking_account( tx: &Connection, sec: &SecurityId, user: &UserId, ) -> CustomResult { create_account(tx, sec, user, AccountType::Checking) } pub fn create_security_emitter_account( tx: &Connection, sec: &SecurityId, user: &UserId, ) -> CustomResult { create_account(tx, sec, user, AccountType::SecurityEmitter) } pub fn is_owner(tx: &Connection, account_id: &AccountId, user_id: &UserId) -> CustomResult { let q = "SELECT id FROM account_user_permissions WHERE account_id = ?1 AND user_id = ?2"; let params = params![account_id, user_id]; let ite = |row: &Row| row.get::(0); let option = tx .query_row(q, params, ite) .optional() .map_err(Error::SqlError)?; Ok(option.is_some()) } pub struct AccountChangeReceipt { pub account_id: AccountId, pub version: i64, pub timestamp: i64, pub amount: i64, } impl AccountChangeReceipt { pub fn from_row(row: &Row) -> Result { Ok(AccountChangeReceipt { account_id: AccountId::from_id(row.get("id")?), version: row.get("version")?, amount: row.get("amount")?, timestamp: row.get("timestamp")?, }) } } fn update_account_history(tx: &Connection, account_id: &AccountId) -> CustomResult<()> { let q = " INSERT INTO accounts_history SELECT id AS account_id, version AS account_version, amount, timestamp FROM accounts WHERE id = ?1 "; tx.execute(q, params![account_id]) .map_err(Error::SqlError)?; Ok(()) } pub fn add_to_account( tx: &Connection, account_id: &AccountId, amount: i64, ) -> CustomResult { let q = " UPDATE accounts SET amount = CASE account_type WHEN 'security_emitter' THEN amount ELSE amount + ?1 END, version = version + 1, timestamp = CURRENT_TIMESTAMP WHERE id = ?2 RETURNING id, version, amount, UNIXEPOCH(CURRENT_TIMESTAMP) as timestamp "; let params = params![amount, account_id]; let ret = tx .query_row(q, params, AccountChangeReceipt::from_row) .map_err(Error::SqlError); update_account_history(tx, account_id)?; return ret; } #[cfg(test)] mod test_accounts { use rusqlite::{Connection, Transaction}; use crate::db::_test_utils::setup_users_and_securities; use crate::db::users::User; use crate::migration; use super::*; fn setup() -> (Connection, Vec, Vec) { let mut conn = Connection::open_in_memory().unwrap(); migration::apply_migrations(&mut conn).unwrap(); let tx = conn.transaction().unwrap(); let (users, securities) = setup_users_and_securities(&tx).unwrap(); tx.commit().unwrap(); return (conn, users, securities); } fn create_all_accounts( tx: &Transaction, users: &Vec, secs: &Vec, ) -> CustomResult> { let mut ret: Vec = Vec::new(); for sec in secs.iter() { for user in users.iter() { ret.push(create_checking_account(&tx, &sec.id(), &user.id())?); } } return Ok(ret); } #[test] fn test_simplest_checking_account() -> CustomResult<()> { let (mut conn, users, secs) = setup(); let user = users.get(0).unwrap(); let not_owner = users.get(1).unwrap(); let sec = secs.get(0).unwrap(); let tx = conn.transaction().unwrap(); let acc = create_checking_account(&tx, &sec.id(), &user.id())?; assert_eq!(acc.account_type, AccountType::Checking); assert_eq!(0, acc.raw_amount()); assert!(is_owner(&tx, &acc.id(), &user.id())?); assert!(!is_owner(&tx, &acc.id(), ¬_owner.id())?); assert_eq!(get_account_history(&tx, &acc.id())?.len(), 1); Ok(()) } #[test] fn test_simplest_security_emitter_account() -> CustomResult<()> { let (mut conn, users, secs) = setup(); let user = users.get(0).unwrap(); let not_owner = users.get(1).unwrap(); let sec = secs.get(0).unwrap(); let tx = conn.transaction().unwrap(); let acc = create_security_emitter_account(&tx, &sec.id(), &user.id())?; assert_eq!(acc.account_type, AccountType::SecurityEmitter); assert_eq!(0, acc.raw_amount()); assert!(is_owner(&tx, &acc.id(), &user.id())?); assert!(!is_owner(&tx, &acc.id(), ¬_owner.id())?); Ok(()) } #[test] fn test_happy_path() -> CustomResult<()> { let (mut conn, users, secs) = setup(); let tx = conn.transaction().unwrap(); create_all_accounts(&tx, &users, &secs)?; tx.commit().unwrap(); let accs = list_accounts(&conn).unwrap(); assert_eq!(4, accs.len()); assert_eq!(1, accs.get(0).unwrap().id); return Ok(()); } #[test] fn test_happy_path_no_commit() -> CustomResult<()> { let (mut conn, users, secs) = setup(); create_all_accounts(&conn.transaction().unwrap(), &users, &secs)?; let accs = list_accounts(&conn)?; assert_eq!(0, accs.len()); return Ok(()); } #[test] fn test_is_owner() -> CustomResult<()> { let (mut conn, users, secs) = setup(); let tx = &conn.transaction().unwrap(); let _accounts = create_all_accounts(tx, &users, &secs)?; let user_1 = users.get(0).unwrap(); let user_2 = users.get(1).unwrap(); let sec = secs.get(0).unwrap(); let acc = create_checking_account(&tx, &sec.id(), &user_1.id())?; assert!(is_owner(tx, &acc.id(), &user_1.id())?); assert!(!is_owner(tx, &acc.id(), &user_2.id())?); Ok(()) } #[test] fn test_add_to_checking_account() -> CustomResult<()> { let (mut conn, users, secs) = setup(); 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).unwrap(); let receipt = add_to_account(&tx, &acc.id(), 33)?; assert_eq!(receipt.account_id, acc.id()); assert_eq!(receipt.amount, 33); tx.commit().unwrap(); let tx2 = conn.transaction().unwrap(); let acc = get_account_by_id(&tx2, &acc.id()).unwrap(); assert_eq!(acc.timestamp, receipt.timestamp); assert_eq!(acc.amount, 33); assert_eq!( get_account_history(&tx2, &acc.id())?.average_amount(), Fraction::from(0 + 33) / Fraction::from(2) ); Ok(()) } #[test] fn test_add_to_currency_emitter_account() -> CustomResult<()> { let (mut conn, users, secs) = setup(); 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)?; let receipt = add_to_account(&tx, &acc.id(), 33)?; assert_eq!(receipt.account_id, acc.id()); assert_eq!(receipt.amount, 0); assert_eq!(receipt.version, 1); tx.commit().unwrap(); let tx2 = conn.transaction().unwrap(); let acc = get_account_by_id(&tx2, &acc.id()).unwrap(); assert_eq!(acc.timestamp, receipt.timestamp); assert_eq!(acc.amount, 0); assert_eq!(get_account_history(&tx2, &acc.id())?.len(), 2); Ok(()) } }