diff --git a/src/db/_accounts.rs b/src/db/_accounts.rs index 14410d5..2c51fea 100644 --- a/src/db/_accounts.rs +++ b/src/db/_accounts.rs @@ -5,7 +5,7 @@ use crate::db::_external_id::{AccountId, Id, UserId}; use crate::db::_securities::Security; use crate::db::_users::User; use crate::db::external_id::SecurityId; -use crate::util::{CustomResult, TransactionHelper}; +use crate::util::{CustomResult, Error, TransactionHelper}; #[derive(PartialEq, Debug)] pub enum AccountType { @@ -73,7 +73,7 @@ pub fn list_accounts<'a>(conn: &Connection) -> Result> { return Ok(ret); } -pub fn get_account_by_id(tx: &Transaction, id: &AccountId) -> Result { +pub fn get_account_by_id(tx: &Connection, id: &AccountId) -> Result { let q = "SELECT acc.id AS acc_id, acc.amount AS acc_amount, UNIXEPOCH(acc.timestamp) AS acc_timestamp, @@ -88,7 +88,7 @@ pub fn get_account_by_id(tx: &Transaction, id: &AccountId) -> Result { return tx.query_row(q, params![id], Account::from_row); } -fn create_account(tx: &Transaction, sec: &SecurityId, user: &UserId, account_type: AccountType) -> Result { +fn create_account(tx: &Connection, sec: &SecurityId, user: &UserId, account_type: AccountType) -> Result { let q = "INSERT INTO accounts(security, amount, timestamp, account_type) VALUES (?1, 0, CURRENT_TIMESTAMP, ?2)"; let account_type = match account_type { @@ -104,15 +104,15 @@ fn create_account(tx: &Transaction, sec: &SecurityId, user: &UserId, account_typ return get_account_by_id(tx, &id); } -pub fn create_checking_account(tx: &Transaction, sec: &SecurityId, user: &UserId) -> Result { +pub fn create_checking_account(tx: &Connection, sec: &SecurityId, user: &UserId) -> Result { create_account(tx, sec, user, AccountType::Checking) } -pub fn create_security_emitter_account(tx: &Transaction, sec: &SecurityId, user: &UserId) -> Result { +pub fn create_security_emitter_account(tx: &Connection, sec: &SecurityId, user: &UserId) -> Result { create_account(tx, sec, user, AccountType::SecurityEmitter) } -pub fn is_owner(tx: &Transaction, account_id: &AccountId, user_id: &UserId) -> Result { +pub fn is_owner(tx: &Connection, account_id: &AccountId, user_id: &UserId) -> Result { 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); @@ -121,9 +121,9 @@ pub fn is_owner(tx: &Transaction, account_id: &AccountId, user_id: &UserId) -> R } pub struct Receipt { - account_id: AccountId, - timestamp: i64, - amount: i64, + pub account_id: AccountId, + pub timestamp: i64, + pub amount: i64, } impl Receipt { @@ -136,17 +136,20 @@ impl Receipt { } } -pub fn add_to_account(tx: &TransactionHelper, +pub fn add_to_account(tx: &Connection, account_id: &AccountId, amount: i64) -> CustomResult { let q = " - UPDATE accounts SET amount = amount + ?1, + UPDATE accounts SET amount = CASE account_type + WHEN 'security_emitter' + THEN amount + ELSE amount + ?1 END, timestamp = CURRENT_TIMESTAMP WHERE id = ?2 RETURNING id, amount, UNIXEPOCH(CURRENT_TIMESTAMP) as timestamp "; let params = params![amount, account_id]; - tx.query_row(q, params, Receipt::from_row) + tx.query_row(q, params, Receipt::from_row).map_err(Error::SqlError) } #[cfg(test)] @@ -254,14 +257,13 @@ mod test_accounts { } #[test] - fn test_add_to_account() -> CustomResult<()> { + fn test_add_to_checking_account() -> CustomResult<()> { let (mut conn, users, secs) = setup(); let tx = conn.transaction().unwrap(); - let mtx = TransactionHelper::new(&tx); 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(&mtx, &acc.id(), 33)?; + let receipt = add_to_account(&tx, &acc.id(), 33)?; assert_eq!(receipt.account_id, acc.id()); assert_eq!(receipt.amount, 33); tx.commit().unwrap(); @@ -273,5 +275,25 @@ mod test_accounts { 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).unwrap(); + let receipt = add_to_account(&tx, &acc.id(), 33)?; + assert_eq!(receipt.account_id, acc.id()); + assert_eq!(receipt.amount, 0); + 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); + + Ok(()) + } } diff --git a/src/services/_transactions.rs b/src/services/_transactions.rs index 7d59b91..10b61cd 100644 --- a/src/services/_transactions.rs +++ b/src/services/_transactions.rs @@ -1,4 +1,4 @@ -use rusqlite::{ffi, params, Params, Savepoint, Transaction}; +use rusqlite::{Connection, ffi, params, Params, Savepoint, Transaction}; use crate::db::{accounts, securities}; use crate::db::accounts::{Account, get_account_by_id}; @@ -7,15 +7,19 @@ use crate::db::external_id::AccountId; use crate::db::securities::get_security_by_id; use crate::util::*; -fn _move_securities(tx: &TransactionHelper, +fn _move_securities(tx: &Savepoint, cheque: &SecurityEmitterCheque, account_id: &AccountId) -> CustomResult<()> { - accounts::add_to_account(tx, &cheque.drawer_account_id, -cheque.amount)?; - accounts::add_to_account(tx, account_id, cheque.amount)?; + let drawer_receipt = accounts::add_to_account(tx, &cheque.drawer_account_id, -cheque.amount)?; + let payee_receipt = accounts::add_to_account(tx, account_id, cheque.amount)?; + if drawer_receipt.amount < 0 { + let m = format!("Drawer of {:?} does not have sufficient amount in account.", cheque); + return Err(Error::InvalidAmount(m)); + } Ok(()) } -fn _add_cheque_to_cashed_cheques_log(tx: &TransactionHelper, +fn _add_cheque_to_cashed_cheques_log(tx: &Connection, cheque: &SecurityEmitterCheque, account_id: &AccountId) -> Result<(), Error> { let q = " @@ -30,7 +34,7 @@ fn _add_cheque_to_cashed_cheques_log(tx: &TransactionHelper, let ret = tx.execute(q, params![cheque.drawer_user_id, cheque.cheque_number, cheque.amount, - cheque.security_id]); + cheque.security_id]).map_err(Error::SqlError); match ret { Ok(_) => Ok(()), Err(Error::SqlError(rusqlite::Error::SqliteFailure(ffi::Error { code: _, extended_code: 2067 }, _))) => @@ -39,27 +43,41 @@ fn _add_cheque_to_cashed_cheques_log(tx: &TransactionHelper, } } -fn cash_cheque(tx: &Transaction, - cheque: &SecurityEmitterCheque, - account_id: &AccountId) -> CustomResult<()> { - if !accounts::is_owner(&tx, &cheque.drawer_account_id, &cheque.drawer_user_id).map_err(Error::SqlError)? { +fn cash_cheque_unsafe(savepoint: &Savepoint, + cheque: &SecurityEmitterCheque, + account_id: &AccountId) -> CustomResult<()> { + if cheque.amount < 0 { + return Err(Error::InvalidAmount(format!("The cheque {:?} has a negative amount.", cheque))); + } + if !accounts::is_owner(&savepoint, &cheque.drawer_account_id, &cheque.drawer_user_id).map_err(Error::SqlError)? { return Err(Error::InvalidDrawer(format!("The drawer is the security owner: {:?}", cheque))); } - if !accounts::is_owner(&tx, account_id, &cheque.payee_user_id).map_err(Error::SqlError)? { + if !accounts::is_owner(&savepoint, account_id, &cheque.payee_user_id).map_err(Error::SqlError)? { return Err(Error::InvalidPayee(format!("Trying to cash the cheque {:?} in \ an account {:?} that is not owned by the payee.", cheque, account_id))); } - - let myx = TransactionHelper::new(tx); - _move_securities(&myx, cheque, account_id)?; - _add_cheque_to_cashed_cheques_log(&myx, cheque, account_id)?; - - // savepoint.rollback().map_err(Error::SqlError)?; - // create transaction loggetAllPurchaseHistory + _move_securities(&savepoint, cheque, account_id)?; + _add_cheque_to_cashed_cheques_log(&savepoint, cheque, account_id)?; Ok(()) } +fn cash_cheque(tx: &mut Transaction, + cheque: &SecurityEmitterCheque, + account_id: &AccountId) -> CustomResult<()> { + let mut savepoint = tx.savepoint().map_err(Error::SqlError)?; + let ret = cash_cheque_unsafe(&savepoint, cheque, account_id); + match ret { + Err(_) => { + savepoint.rollback().map_err(Error::SqlError)?; + ret + } + Ok(_) => { + savepoint.commit().map_err(Error::SqlError) + } + } +} + #[cfg(test)] mod test_caching_cheques { @@ -115,6 +133,21 @@ mod test_caching_cheques { Ok(()) } + #[test] + fn test_negative_cheque() -> CustomResult<()> { + let (mut conn, drawer, payee, payee_account) = setup_basic(); + let mut tx = conn.transaction().unwrap(); + + let cheque = SecurityEmitterCheque::new( + drawer, payee, -1000, 99); + + let ret = cash_cheque(&mut tx, &cheque, &payee_account); + match ret { + Err(Error::InvalidAmount(_)) => Ok(()), + _ => Err(Error::Testing("Expected double spending error here.".to_string())) + } + } + #[test] fn test_double_spend() -> CustomResult<()> { let (mut conn, drawer, payee, payee_account) = setup_basic(); diff --git a/src/util.rs b/src/util.rs index fdcf3fe..94f2b32 100644 --- a/src/util.rs +++ b/src/util.rs @@ -3,6 +3,7 @@ use rusqlite::{Params, Row, Transaction}; #[derive(Debug)] pub enum Error { SqlError(rusqlite::Error), + DoubleBooking(String), InvalidDrawer(String), InvalidAmount(String), InvalidPayee(String),