From 048ac31c2712bcdc26be5aa1baa2119a7a90a030 Mon Sep 17 00:00:00 2001 From: didier amyot Date: Wed, 5 Apr 2023 11:02:35 -0400 Subject: [PATCH] Something is working.. --- src/db/_accounts.rs | 8 +- src/db/_cheques.rs | 20 +++ src/db/mod.rs | 2 +- src/services/_transactions.rs | 250 +++++++++++++++++++++++++++++----- 4 files changed, 242 insertions(+), 38 deletions(-) diff --git a/src/db/_accounts.rs b/src/db/_accounts.rs index 898875c..3521d7e 100644 --- a/src/db/_accounts.rs +++ b/src/db/_accounts.rs @@ -292,6 +292,7 @@ pub fn is_owner(tx: &Connection, account_id: &AccountId, user_id: &UserId) -> Cu pub struct AccountChangeReceipt { pub account_id: AccountId, + pub security_id: SecurityId, pub version: i64, pub timestamp: i64, pub amount: i64, @@ -304,6 +305,7 @@ impl AccountChangeReceipt { version: row.get("version")?, amount: row.get("amount")?, timestamp: row.get("timestamp")?, + security_id: SecurityId::from_id(row.get("security_id")?), }) } } @@ -336,7 +338,11 @@ pub fn add_to_account( version = version + 1, timestamp = CURRENT_TIMESTAMP WHERE id = ?2 - RETURNING id, version, amount, UNIXEPOCH(CURRENT_TIMESTAMP) as timestamp + RETURNING id, + version, + amount, + UNIXEPOCH(CURRENT_TIMESTAMP) as timestamp, + security as security_id "; let params = params![amount, account_id]; let ret = tx diff --git a/src/db/_cheques.rs b/src/db/_cheques.rs index 50d3304..0dbaf7a 100644 --- a/src/db/_cheques.rs +++ b/src/db/_cheques.rs @@ -25,3 +25,23 @@ impl Cheque { } } } + +#[derive(Debug)] +pub struct WireTransferRequest { + pub drawer_account_id: AccountId, + pub drawer_user_id: UserId, + pub payee_account_id: AccountId, + pub amount: i64, +} + +impl WireTransferRequest { + pub fn new(drawer: Drawer, payee: & AccountId, amount: i64) -> WireTransferRequest { + WireTransferRequest { + drawer_user_id: drawer.0, + drawer_account_id: drawer.1, + payee_account_id: payee.clone(), + amount, + } + } + +} \ No newline at end of file diff --git a/src/db/mod.rs b/src/db/mod.rs index 7edfb4d..04730e6 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -34,7 +34,7 @@ pub mod accounts { } pub mod cheques { - pub use super::_cheques::{Cheque, Drawer, Payee}; + pub use super::_cheques::{Cheque, Drawer, Payee, WireTransferRequest}; } pub mod test_utils { diff --git a/src/services/_transactions.rs b/src/services/_transactions.rs index e2b97e6..1f1f1e6 100644 --- a/src/services/_transactions.rs +++ b/src/services/_transactions.rs @@ -2,11 +2,41 @@ use rusqlite::{ffi, params, Connection, Savepoint, Transaction}; use crate::db::accounts; -use crate::db::cheques::Cheque; +use crate::db::cheques::{Cheque, WireTransferRequest}; use crate::db::external_id::AccountId; use crate::util::*; -fn _move_securities( +fn _transfer_securities_from_one_account_to_another( + tx: &Savepoint, + drawer_account_id: &AccountId, + payee_account_id: &AccountId, + amount: i64, +) -> CustomResult<( + accounts::AccountChangeReceipt, + accounts::AccountChangeReceipt, +)> { + let drawer_receipt = accounts::add_to_account(tx, drawer_account_id, -amount)?; + let payee_receipt = accounts::add_to_account(tx, payee_account_id, amount)?; + if drawer_receipt.amount < 0 { + let m = format!( + "Drawer of {:?} does not have sufficient amount in account.", + drawer_account_id + ); + return Err(Error::InvalidAmount(m)); + } + if drawer_receipt.security_id != drawer_receipt.security_id { + let m = format!( + "Incompatible security type between accounts {:} (drawer) and {:} (payee).", + drawer_account_id, + payee_account_id + ); + return Err(Error::InvalidAmount(m)); + + } + Ok((drawer_receipt, payee_receipt)) +} + +fn _transfer_cheque_securities( tx: &Savepoint, cheque: &Cheque, account_id: &AccountId, @@ -14,16 +44,22 @@ fn _move_securities( accounts::AccountChangeReceipt, accounts::AccountChangeReceipt, )> { - 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((drawer_receipt, payee_receipt)) + _transfer_securities_from_one_account_to_another(tx, &cheque.drawer_account_id, account_id, cheque.amount) +} + + + +fn _transfer_wire_transfer_securities( + tx: &Savepoint, + wire_transfer_request: &WireTransferRequest, +) -> CustomResult<( + accounts::AccountChangeReceipt, + accounts::AccountChangeReceipt, +)> { + _transfer_securities_from_one_account_to_another(tx, + &wire_transfer_request.drawer_account_id, + &wire_transfer_request.payee_account_id, + wire_transfer_request.amount) } fn _add_cheque_to_cashed_cheques_log( @@ -77,7 +113,7 @@ fn _add_cheque_to_cashed_cheques_log( fn cash_cheque_unsafe( savepoint: &Savepoint, cheque: &Cheque, - account_id: &AccountId, + payee_account_id: &AccountId, ) -> CustomResult<()> { if cheque.amount < 0 { return Err(Error::InvalidAmount(format!( @@ -96,21 +132,71 @@ fn cash_cheque_unsafe( ))); } - if !accounts::is_owner(&savepoint, account_id, &cheque.payee_user_id)? { + if !accounts::is_owner(&savepoint, payee_account_id, &cheque.payee_user_id)? { return Err(Error::InvalidPayee(format!( "Trying to cash the cheque {:?} in \ an account {:?} that is not owned by the payee.", - cheque, account_id + cheque, payee_account_id ))); } - let (drawer_receipt, payee_receipt) = _move_securities(&savepoint, cheque, account_id)?; + + let drawer_account_sec = accounts::get_account_by_id(&savepoint, &cheque.drawer_account_id)?.security().id(); + if drawer_account_sec != cheque.security_id{ + return Err(Error::InvalidDrawer(format!( + "Impossible to use an account containing {:} to emit a cheque in {:}", + drawer_account_sec, cheque.security_id + ))); + } + + let payee_account_sec = accounts::get_account_by_id(&savepoint, &payee_account_id)?.security().id(); + if payee_account_sec != cheque.security_id{ + return Err(Error::InvalidPayee(format!( + "Impossible to cache a {:} cheque in {:} account .", + cheque.security_id, payee_account_sec + ))); + } + + let (drawer_receipt, payee_receipt) = _transfer_cheque_securities(&savepoint, cheque, payee_account_id)?; _add_cheque_to_cashed_cheques_log(&savepoint, cheque, &drawer_receipt, &payee_receipt)?; Ok(()) } -fn cash_cheque(tx: &mut Transaction, cheque: &Cheque, account_id: &AccountId) -> CustomResult<()> { +fn cash_wire_transfer_request_unsafe( + savepoint: &Savepoint, + wire_transfer: &WireTransferRequest, +) -> CustomResult<()> { + if wire_transfer.amount < 0 { + return Err(Error::InvalidAmount(format!( + "The cheque {:?} has a negative amount.", + wire_transfer + ))); + } + let (drawer_receipt, payee_receipt) = _transfer_wire_transfer_securities(&savepoint, wire_transfer)?; + Ok(()) +} + +fn cash_cheque( + tx: &mut Transaction, + cheque: &Cheque, + payee_account_id: &AccountId, +) -> CustomResult<()> { let mut savepoint = tx.savepoint().map_err(Error::SqlError)?; - let ret = cash_cheque_unsafe(&savepoint, cheque, account_id); + let ret = cash_cheque_unsafe(&savepoint, cheque, payee_account_id); + match ret { + Err(_) => { + savepoint.rollback().map_err(Error::SqlError)?; + ret + } + Ok(_) => savepoint.commit().map_err(Error::SqlError), + } +} + +fn cash_wire_transfer_request( + tx: &mut Transaction, + wire_transfer: &WireTransferRequest, +) -> CustomResult<()> { + let mut savepoint = tx.savepoint().map_err(Error::SqlError)?; + let ret = cash_wire_transfer_request_unsafe(& savepoint, wire_transfer); match ret { Err(_) => { savepoint.rollback().map_err(Error::SqlError)?; @@ -121,7 +207,7 @@ fn cash_cheque(tx: &mut Transaction, cheque: &Cheque, account_id: &AccountId) -> } #[cfg(test)] -mod test_caching_cheques { +mod test_utils { use rusqlite::Connection; use crate::db::accounts::{ @@ -134,13 +220,13 @@ mod test_caching_cheques { use super::*; - fn setup_conn() -> Connection { + pub fn setup_conn() -> Connection { let mut conn = Connection::open_in_memory().unwrap(); migration::apply_migrations(&mut conn).unwrap(); return conn; } - fn setup_security_emitter() -> (Connection, Drawer, Payee, AccountId) { + pub fn setup_security_emitter() -> (Connection, Drawer, Payee, AccountId) { let mut conn = setup_conn(); let tx = conn.transaction().unwrap(); let (users, securities, _accounts) = test_utils::setup_basic_environment(&tx).unwrap(); @@ -157,7 +243,26 @@ mod test_caching_cheques { (conn, security_emitter, payee, payee_account.id()) } - fn _create_100_secs_in_payee_account( + pub fn setup_security_emitter_mismatched_accounts() -> (Connection, Drawer, Payee, AccountId) { + let mut conn = setup_conn(); + let tx = conn.transaction().unwrap(); + let (users, securities, _accounts) = test_utils::setup_basic_environment(&tx).unwrap(); + let sec_1 = securities.get(0).unwrap(); + let sec_2 = securities.get(1).unwrap(); + let drawer = users.get(0).unwrap(); + let payee = users.get(1).unwrap(); + let security_emitter_account = + create_security_emitter_account(&tx, &sec_1.id(), &drawer.id()).unwrap(); + let payee_account = create_checking_account(&tx, &sec_2.id(), &payee.id()).unwrap(); + + tx.commit().unwrap(); + let security_emitter: Drawer = (drawer.id(), security_emitter_account.id(), sec_1.id()); + let payee: Payee = (payee.id(), sec_1.id()); + (conn, security_emitter, payee, payee_account.id()) + + } + + pub fn create_100_security_units_in_payee_account( tx: &mut Transaction, security_emitter: &Drawer, payee: &Payee, @@ -172,12 +277,12 @@ mod test_caching_cheques { (payee.0, payee_account, payee.1) } - fn setup_basic() -> (Connection, Drawer, Payee, AccountId) { + pub fn setup_basic() -> (Connection, Drawer, Payee, AccountId) { let (mut conn, security_emitter, payee, payee_account) = setup_security_emitter(); let mut tx = conn.transaction().unwrap(); - _create_100_secs_in_payee_account(&mut tx, &security_emitter, &payee, &payee_account); + create_100_security_units_in_payee_account(&mut tx, &security_emitter, &payee, &payee_account); let reg_account = create_checking_account(&tx, &security_emitter.2, &security_emitter.0).unwrap(); @@ -188,9 +293,25 @@ mod test_caching_cheques { return (conn, drawer, new_payee, reg_account.id()); } +} + +#[cfg(test)] +mod test_caching_cheques { + use rusqlite::Connection; + + use crate::db::accounts::{ + create_checking_account, create_security_emitter_account, get_account_by_id, + }; + use crate::db::cheques::{Drawer, Payee}; + use crate::db::external_id::{Id, UserId}; + use crate::db::test_utils; + use crate::migration; + + use super::*; + use super::test_utils::*; #[test] - fn test_happy_path_currency_emitter() -> CustomResult<()> { + fn test_happy_path_currency_emitter() -> CustomResult<()> { // done let (mut conn, security_emitter, payee, payee_account) = setup_security_emitter(); let mut tx = conn.transaction().unwrap(); @@ -207,7 +328,7 @@ mod test_caching_cheques { } #[test] - fn test_happy_path_regular_cheque() -> CustomResult<()> { + fn test_happy_path_regular_cheque() -> CustomResult<()> { // done let (mut conn, drawer, payee, payee_account) = setup_basic(); let mut tx = conn.transaction().unwrap(); @@ -255,7 +376,7 @@ mod test_caching_cheques { test_negative_cheque(&mut conn, drawer, payee, payee_account) } - fn test_double_spend( + fn _test_double_spend( conn: &mut Connection, drawer: Drawer, payee: Payee, @@ -281,16 +402,16 @@ mod test_caching_cheques { #[test] fn test_double_spend_security_emitter_cheque() -> CustomResult<()> { let (mut conn, security_emitter, payee, payee_account) = setup_security_emitter(); - test_double_spend(&mut conn, security_emitter, payee, payee_account) + _test_double_spend(&mut conn, security_emitter, payee, payee_account) } #[test] fn test_double_spend_regular_emitter_cheque() -> CustomResult<()> { let (mut conn, drawer, payee, payee_account) = setup_basic(); - test_double_spend(&mut conn, drawer, payee, payee_account) + _test_double_spend(&mut conn, drawer, payee, payee_account) } - fn test_invalid_drawer( + fn _test_invalid_drawer( conn: &mut Connection, drawer: Drawer, payee: Payee, @@ -311,21 +432,20 @@ mod test_caching_cheques { #[test] fn test_invalid_drawer_security_emitter() -> CustomResult<()> { let (mut conn, security_emitter, payee, payee_account) = setup_security_emitter(); - test_invalid_drawer(&mut conn, security_emitter, payee, payee_account) + _test_invalid_drawer(&mut conn, security_emitter, payee, payee_account) } #[test] fn test_invalid_drawer_regular_cheque() -> CustomResult<()> { let (mut conn, drawer, payee, payee_account) = setup_basic(); - test_invalid_drawer(&mut conn, drawer, payee, payee_account) + _test_invalid_drawer(&mut conn, drawer, payee, payee_account) } fn test_invalid_payee( - _conn: &mut Connection, - _security_emitter: Drawer, - _payee: Payee, + conn: &mut Connection, + security_emitter: Drawer, + payee: Payee, ) -> CustomResult<()> { - let (mut conn, security_emitter, payee, _payee_account) = setup_security_emitter(); let mut tx = conn.transaction().unwrap(); let wrong_payee_account = &security_emitter.1; @@ -349,4 +469,62 @@ mod test_caching_cheques { let (mut conn, drawer, payee, _payee_account) = setup_basic(); test_invalid_payee(&mut conn, drawer, payee) } + + #[test] + fn test_mismatch_securities() -> CustomResult<()>{ + let (mut conn, drawer, payee, payee_account) = setup_security_emitter_mismatched_accounts(); + let cheque = Cheque::new(drawer.clone(), payee.clone(), 1000, 99); + let mut tx = conn.transaction().unwrap(); + + match cash_cheque(&mut tx, &cheque, & payee_account) { + Err(Error::InvalidPayee(_)) => Ok(()), + _ => Err(Error::Testing("Expected invalid payee here.".to_string())) + } + } } + + +#[cfg(test)] +mod test_wire_transfers { + use crate::db::accounts::get_account_by_id; + use super::*; + use super::test_utils::*; + + #[test] + fn test_happy_path_currency_emitter() -> CustomResult<()> { + let (mut conn, security_emitter, payee, payee_account) = setup_security_emitter(); + let mut tx = conn.transaction().unwrap(); + + // let cheque = Cheque::new(security_emitter, payee, 1000, 99); + let wire_transfer = WireTransferRequest::new(security_emitter, &payee_account, 1000);; + + cash_wire_transfer_request(&mut tx, &wire_transfer)?; + tx.commit().unwrap(); + + let tx = conn.transaction().unwrap(); + let acc = get_account_by_id(&tx, &payee_account).unwrap(); + assert_eq!(acc.raw_amount(), 1000); + + Ok(()) + } + + #[test] + fn test_happy_path_regular_wire_transfer() -> CustomResult<()> { + let (mut conn, drawer, payee, payee_account) = setup_basic(); + let mut tx = conn.transaction().unwrap(); + + let wire_transfer = WireTransferRequest::new(drawer.clone(), &payee_account, 25); + + cash_wire_transfer_request(&mut tx, &wire_transfer)?; + + let payee_acc = get_account_by_id(&tx, &payee_account).unwrap(); + assert_eq!(payee_acc.raw_amount(), 25); + + let drawer_acc = get_account_by_id(&tx, &drawer.1).unwrap(); + assert_eq!(drawer_acc.raw_amount(), 75); + + Ok(()) + } + + +} \ No newline at end of file