From 64e9be8b5a4bb04f5ccdc5e215f4415831d82238 Mon Sep 17 00:00:00 2001 From: didier amyot Date: Sun, 18 Dec 2022 10:39:33 -0500 Subject: [PATCH] going somewhere. --- migrations/create_tables_001.sql | 2 +- src/db/README.md | 2 + src/db/_accounts.rs | 53 ++++++++++++++---- src/db/_cheques.rs | 29 +++++----- src/db/_external_id.rs | 9 ++- src/db/_securities.rs | 27 +++++---- src/db/_test_utils.rs | 15 +++-- src/db/_users.rs | 18 ++++-- src/db/mod.rs | 6 +- src/main.rs | 1 + src/services/_transactions.rs | 94 +++++++++++++++++++++++--------- src/util.rs | 23 ++++++++ 12 files changed, 203 insertions(+), 76 deletions(-) create mode 100644 src/util.rs diff --git a/migrations/create_tables_001.sql b/migrations/create_tables_001.sql index 24298b7..129055e 100644 --- a/migrations/create_tables_001.sql +++ b/migrations/create_tables_001.sql @@ -56,7 +56,7 @@ CREATE TABLE IF NOT EXISTS cashed_cheques ( id INTEGER PRIMARY KEY AUTOINCREMENT, drawer_user_id INTEGER NOT NULL, - drawer_account_id INTEGER NOT NULL, + drawer_account_id INTEGER, cheque_number INTEGER NOT NULL, amount INTEGER NOT NULL, underlying_security INTEGER NOT NULL, diff --git a/src/db/README.md b/src/db/README.md index 2a08896..7ff2787 100644 --- a/src/db/README.md +++ b/src/db/README.md @@ -6,6 +6,8 @@ erDiagram users ||--o{ securities : owner security_types ||--o{ securities: s securities ||--o{ accounts: ps + securities ||--o{ security_permissions: i + users ||--o{ security_permissions: i users ||--o{ cheques: ps securities ||--o{ cheques: ps ``` \ No newline at end of file diff --git a/src/db/_accounts.rs b/src/db/_accounts.rs index a385238..c40d044 100644 --- a/src/db/_accounts.rs +++ b/src/db/_accounts.rs @@ -1,16 +1,23 @@ -use rusqlite::{Connection, params, Result, Row, Transaction}; +use rusqlite::{Connection, OptionalExtension, params, Result, Row, Transaction}; +use sha2::digest::typenum::op; +use crate::db::_external_id::{Id, AccountId, UserId}; use crate::db::_securities::Security; use crate::db::_users::User; +use crate::db::external_id::SecurityId; pub struct Account { - pub id: i64, - pub amount: i64, - pub timestamp: i64, - pub security: Security, + id: u32, + amount: u32, + timestamp: i32, + security: Security, } 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")?, @@ -55,13 +62,21 @@ pub fn get_account_by_id(tx: &Transaction, id: u32) -> Result { return tx.query_row(q, params![id], Account::from_row); } -pub fn create_account(tx: &Transaction, sec: &Security, user: &User) -> Result { +pub fn create_account(tx: &Transaction, sec: &SecurityId, user: &UserId) -> Result { let q = "INSERT INTO accounts(security, owner, amount, timestamp) VALUES (?1, ?2, 0, CURRENT_TIMESTAMP)"; - tx.execute(q, (sec.id, user.id))?; + tx.execute(q, (sec, user))?; return get_account_by_id(tx, tx.last_insert_rowid() as u32); } +pub fn is_owner(tx: &Transaction, account_id: &AccountId, user_id: &UserId) -> Result { + let q = "SELECT id FROM accounts WHERE id = ?1 AND owner = ?2"; + let params = params![account_id, user_id]; + let ite = |row: &Row| row.get::(0); + let option = tx.query_row(q, params, ite).optional()?; + Ok(option.is_some()) +} + #[cfg(test)] mod test { use rusqlite::Connection; @@ -84,13 +99,14 @@ mod test { return (conn, users, securities); } - fn create_all_accounts(tx: &Transaction, users: &Vec, secs: &Vec) -> Result<()> { + fn create_all_accounts(tx: &Transaction, users: &Vec, secs: &Vec) -> Result> { + let mut ret: Vec = Vec::new(); for sec in secs.iter() { for user in users.iter() { - create_account(&tx, sec, user)?; + ret.push(create_account(&tx, &sec.id(), &user.id())?); } } - return Ok(()); + return Ok(ret); } #[test] @@ -114,5 +130,22 @@ mod test { assert_eq!(0, accs.len()); return Ok(()); } + + #[test] + fn test_is_owner() -> Result<()> { + let (mut conn, users, secs) = setup(); + let tx = &conn.transaction()?; + 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_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(()) + } } diff --git a/src/db/_cheques.rs b/src/db/_cheques.rs index a946d19..f15bf71 100644 --- a/src/db/_cheques.rs +++ b/src/db/_cheques.rs @@ -1,36 +1,33 @@ use rusqlite::{Result, Transaction}; -use crate::db::_external_id::{AccountId, SecurityId}; +use crate::db::_external_id::{AccountId, SecurityId, UserId}; +#[derive(Debug)] pub struct SecurityEmitterCheque { - pub drawer_user_id: AccountId, + pub drawer_user_id: UserId, pub security_id: SecurityId, - pub payee_user_id: AccountId, + pub payee_user_id: UserId, pub cheque_number: u32, pub amount: u32, } impl SecurityEmitterCheque { - fn new_safe(tx: &Transaction, - drawer_user_id: AccountId, - security_id: SecurityId, - payee_user_id: AccountId, - amount: u32, - id_generator: fn() -> u32) -> Result { - Ok(SecurityEmitterCheque { + pub fn new(drawer_user_id: UserId, + security_id: SecurityId, + payee_user_id: UserId, + amount: u32, + cheque_number: u32) -> SecurityEmitterCheque { + SecurityEmitterCheque { drawer_user_id, security_id, payee_user_id, - cheque_number: id_generator(), + cheque_number, amount, - }) - } - - pub fn assert_valid(&self, tx: &Transaction) -> Result<()> { - Ok(()) + } } } +#[cfg(test)] pub mod test { use super::*; diff --git a/src/db/_external_id.rs b/src/db/_external_id.rs index 5241eaa..6421fce 100644 --- a/src/db/_external_id.rs +++ b/src/db/_external_id.rs @@ -70,6 +70,13 @@ macro_rules! impl_ids { } } + impl rusqlite::ToSql for $T { + #[inline] + fn to_sql(&self) -> rusqlite::Result> { + Ok(rusqlite::types::ToSqlOutput::from(self.0.get_id())) + } + } + impl Id for $T { type Output = $T; @@ -162,7 +169,7 @@ impl ExternalId { } } - +#[cfg(test)] pub mod test { use std::any::Any; diff --git a/src/db/_securities.rs b/src/db/_securities.rs index 9007847..3062e8e 100644 --- a/src/db/_securities.rs +++ b/src/db/_securities.rs @@ -1,5 +1,6 @@ use rusqlite::{Connection, OptionalExtension, params, Result, Row, ToSql, Transaction}; use serde::Serialize; +use crate::db::_external_id::{Id, SecurityId, UserId}; use crate::db::users::User; @@ -33,11 +34,17 @@ impl ToSql for SecurityType { #[derive(Debug, Serialize, PartialEq)] pub struct Security { - pub id: i32, + id: u32, security_type: SecurityType, symbol: String, } +impl Security { + pub fn id(&self) -> SecurityId { + SecurityId::from_id(self.id) + } +} + impl Security { pub fn from_row(row: &Row) -> Result { Ok(Security { @@ -56,7 +63,7 @@ pub struct NewSecurity { pub fn create_security(tx: &Transaction, sec: &NewSecurity, - owner: &User) -> Result { + owner: &UserId) -> Result { let q = "INSERT INTO securities(name, symbol, security_type) VALUES(?1, ?2, ?3)"; @@ -67,18 +74,18 @@ pub fn create_security(tx: &Transaction, let q = "INSERT INTO security_permissions(security_id, user_id) VALUES(?1, ?2)"; - tx.execute(q, params![new_sec_id, owner.id])?; + tx.execute(q, params![new_sec_id, owner])?; return get_security_by_id(tx, new_sec_id); } -pub fn is_owner(tx: &Transaction, sec: &Security, user: &User) -> Result { +pub fn is_owner(tx: &Transaction, sec_id: &SecurityId, user_id: &UserId) -> Result { let q = "SELECT id FROM security_permissions WHERE security_id = ?1 AND user_id = ?2 LIMIT 1"; let ret: Option = tx.query_row(q, - params![sec.id, user.id], + params![sec_id, user_id], |r| r.get(0)).optional()?; return Ok(ret.is_some()); } @@ -145,10 +152,10 @@ mod test { let not_owner = users.get(1).unwrap(); let sec = mock_currency_1(); let tx = conn.transaction()?; - let sec = create_security(&tx, &sec, &owner)?; + let sec = create_security(&tx, &sec, &owner.id())?; - assert!(is_owner(&tx, &sec, &owner)?); - assert!(!is_owner(&tx, &sec, ¬_owner)?); + assert!(is_owner(&tx, &sec.id(), &owner.id())?); + assert!(!is_owner(&tx, &sec.id(), ¬_owner.id())?); tx.commit() } @@ -161,7 +168,7 @@ mod test { let sec = mock_currency_1(); let tx = conn.transaction()?; - create_security(&tx, &sec, &user)?; + create_security(&tx, &sec, &user.id())?; tx.commit()?; let ret = list_securities(&conn)?; let expt = Security { @@ -180,7 +187,7 @@ mod test { symbol: "B".to_string(), }; let tx = conn.transaction()?; - create_security(&tx, &sec, &user)?; + create_security(&tx, &sec, &user.id())?; tx.commit()?; let ret = list_securities(&conn)?; assert_eq!(2, ret.len()); diff --git a/src/db/_test_utils.rs b/src/db/_test_utils.rs index cf9f084..e0327a4 100644 --- a/src/db/_test_utils.rs +++ b/src/db/_test_utils.rs @@ -2,6 +2,7 @@ use rusqlite::{Result, Transaction}; use crate::db::{_securities, _users}; use crate::db::_accounts::create_account; +use crate::db::_external_id::UserId; use crate::db::_securities::{NewSecurity, Security, SecurityType}; use crate::db::_users::User; use crate::db::accounts::Account; @@ -28,22 +29,26 @@ pub fn setup_users(tx: &Transaction) -> Result> { return Ok(vec![user_1, user_2]); } -pub fn setup_securities(tx: &Transaction, owner: &User) -> Result> { +pub fn setup_securities(tx: &Transaction, owner: &UserId) -> Result> { let sec_1 = _securities::create_security(tx, &mock_currency_1(), owner)?; let sec_2 = _securities::create_security(tx, &mock_currency_2(), owner)?; return Ok(vec![sec_1, sec_2]); } -pub fn setup_users_and_securities(tx: &Transaction) -> Result<(Vec, Vec)>{ +pub fn setup_users_and_securities(tx: &Transaction) -> Result<(Vec, Vec)> { let users = setup_users(tx)?; - let securities = setup_securities(tx, users.get(0).unwrap())?; + let securities = setup_securities(tx, &users.get(0).unwrap().id())?; Ok((users, securities)) } pub fn setup_basic_environment(tx: &Transaction) -> Result<(Vec, Vec, Vec)> { let (users, securities) = setup_users_and_securities(tx)?; - let acc1 = create_account(tx, securities.get(0).unwrap(), users.get(0).unwrap())?; - let acc2 = create_account(tx, securities.get(1).unwrap(), users.get(1).unwrap())?; + let acc1 = create_account(tx, + &securities.get(0).unwrap().id(), + &users.get(0).unwrap().id())?; + let acc2 = create_account(tx, + &securities.get(1).unwrap().id(), + &users.get(1).unwrap().id())?; Ok((users, securities, vec![acc1, acc2])) } \ No newline at end of file diff --git a/src/db/_users.rs b/src/db/_users.rs index fd9bd9a..ea7cada 100644 --- a/src/db/_users.rs +++ b/src/db/_users.rs @@ -1,21 +1,28 @@ use rusqlite::{Connection, params, Result, Transaction, Row}; use serde::{Serialize}; +use crate::db::_external_id::{UserId, Id}; #[derive(Debug, Serialize)] pub struct User { - pub id: i32, + id: u32, name: String, public_key: String, } -pub fn create_user(tx: & Transaction, name: &String, public_key: &String) -> Result{ +impl User { + pub fn id(&self) -> UserId { + UserId::from_id(self.id) + } +} + +pub fn create_user(tx: &Transaction, name: &String, public_key: &String) -> Result { tx.execute("INSERT INTO users (name, public_key) VALUES (?1, ?2)", (name, public_key))?; return get_user_by_id(tx, tx.last_insert_rowid() as i32); } -fn _user_from_row(row: &Row) -> Result{ +fn _user_from_row(row: &Row) -> Result { Ok(User { id: row.get(0)?, name: row.get(1)?, @@ -23,15 +30,14 @@ fn _user_from_row(row: &Row) -> Result{ }) } -fn get_user_by_id(tx: & Transaction, id: i32) -> Result{ +fn get_user_by_id(tx: &Transaction, id: i32) -> Result { let q = "SELECT id, name, public_key FROM users WHERE id = ?1"; tx.query_row(q, params![id], _user_from_row) } -pub fn list_users(conn: & Connection) -> Result>{ - +pub fn list_users(conn: &Connection) -> Result> { let mut stmt = conn.prepare("SELECT id, name, public_key FROM users")?; let ret: Vec = stmt.query_map([], _user_from_row)? .filter(Result::is_ok) diff --git a/src/db/mod.rs b/src/db/mod.rs index 3bfa073..7b92974 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -5,8 +5,12 @@ mod _cheques; mod _external_id; mod _test_utils; +pub mod external_id { + pub use super::_external_id::*; +} + pub mod securities { - pub use super::_securities::{get_security_by_id, list_securities, Security}; + pub use super::_securities::{get_security_by_id, list_securities, Security, is_owner}; } pub mod users { diff --git a/src/main.rs b/src/main.rs index df9045b..8e42610 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,6 +13,7 @@ use crate::db::users::{create_user, list_users}; mod migration; mod db; mod services; +mod util; #[derive(Debug, Serialize)] struct Output { diff --git a/src/services/_transactions.rs b/src/services/_transactions.rs index 510e054..35ada41 100644 --- a/src/services/_transactions.rs +++ b/src/services/_transactions.rs @@ -1,37 +1,62 @@ -use rusqlite::{params, Result, Transaction}; +use rusqlite::{params, Params, Transaction}; -use crate::db::accounts::get_account_by_id; +use crate::db::accounts::{Account, get_account_by_id}; use crate::db::cheques::SecurityEmitterCheque; +use crate::db::external_id::AccountId; +use crate::db::securities; use crate::db::securities::get_security_by_id; +use crate::util::*; -fn _add_securities_to_account(tx: &Transaction, - cheque: &SecurityEmitterCheque) -> Result<()> { +fn _add_securities_to_account(tx: &TransactionHelper, + cheque: &SecurityEmitterCheque, + account_id: &AccountId) -> CustomResult<()> { + let q = "UPDATE accounts + SET amount = amount + ?1 + WHERE id = ?2"; + tx.execute(q, params![cheque.amount, account_id])?; + Ok(()) +} + +fn _add_cheque_to_cashed_cheques_log(tx: &TransactionHelper, + cheque: &SecurityEmitterCheque, + account_id: &AccountId) -> Result<(), Error> { let q = " - INSERT INTO accounts() VALUES() + INSERT INTO cashed_cheques( + drawer_user_id, + cheque_number, + amount, + underlying_security + ) + VALUES(?1, ?2, ?3, ?4) "; - tx.execute(q, params![cheque.amount])?; + tx.execute(q, params![cheque.drawer_user_id, + cheque.cheque_number, + cheque.amount, + cheque.security_id])?; Ok(()) } fn cash_cheque(tx: &Transaction, - cheque: &SecurityEmitterCheque) -> Result<()> { - cheque.assert_valid(tx)?; + cheque: &SecurityEmitterCheque, + account_id: &AccountId) -> CustomResult<()> { // move money to new account - _add_securities_to_account(tx, cheque)?; - // create transaction log - // create cheque log - Ok(()) -} + if !securities::is_owner(tx, &cheque.security_id, &cheque.drawer_user_id).map_err(Error::SqlError)? { + return Err(Error::InvalidDrawer(format!("The drawer is the security owner: {:?}", cheque))); + } -fn update_account(tx: &Transaction, account_id: u32) -> Result<()> { - let account = get_account_by_id(tx, account_id)?; - return Ok(()); + + let myx = TransactionHelper::new(tx); + _add_securities_to_account(&myx, cheque, account_id)?; + _add_cheque_to_cashed_cheques_log(&myx, cheque, account_id)?; + // create transaction log + Ok(()) } #[cfg(test)] mod test { - use rusqlite::{Connection, params, Result}; + use rusqlite::{Connection, params}; + use crate::db::accounts::create_account; use crate::db::test_utils; use crate::migration; @@ -44,19 +69,36 @@ mod test { return conn; } + #[test] - fn test_basic() -> Result<()> { + fn test_basic() -> Result<(), Error> { let mut conn = setup(); - let tx = conn.transaction()?; - let (users, securities, accounts) = test_utils::setup_basic_environment(&tx)?; - tx.commit()?; + let tx = conn.transaction().unwrap(); + let (users, securities, accounts) = test_utils::setup_basic_environment(&tx).unwrap(); + let sec = securities.get(0).unwrap(); + let drawer = users.get(0).unwrap(); + let payee = users.get(1).unwrap(); + let amount: u32 = 1000; + let payee_account = create_account(&tx, & sec.id(), & payee.id()).unwrap(); - let tx = conn.transaction()?; - // let cheque = SecurityEmitterCheque{ - // drawer_user_id: users.get_unchecked(0).id - // }; + tx.commit().unwrap(); + let tx = conn.transaction().unwrap(); + + // let tx = conn.transaction()?; + // SecurityEmitterCheque::new(securities.get()) + let cheque = SecurityEmitterCheque::new( + drawer.id(), + sec.id(), + payee.id(), + amount, + 99, + ); + + + cash_cheque(&tx, &cheque, &payee_account.id())?; + + tx.commit().unwrap(); Ok(()) } - } \ No newline at end of file diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..40e9e23 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,23 @@ +use rusqlite::{Params, Transaction}; + +#[derive(Debug)] +pub enum Error { + SqlError(rusqlite::Error), + InvalidDrawer(String), +} + +pub type CustomResult = Result; + +pub struct TransactionHelper<'a> { + tx: &'a Transaction<'a>, +} + +impl TransactionHelper<'_> { + pub fn new<'a>(tx: &'a Transaction) -> TransactionHelper<'a> { + TransactionHelper { tx } + } + + pub fn execute(&self, sql: &str, params: P) -> CustomResult { + self.tx.execute(sql, params).map_err(Error::SqlError) + } +}