going somewhere.

This commit is contained in:
didier amyot committed 2022-12-18 10:39:33 -05:00
1 parent 494cdedfc6
commit 64e9be8b5a
12 files changed
+203 -76

No files matched your search

+1 -1
View File
@@ -56,7 +56,7 @@ CREATE TABLE IF NOT EXISTS cashed_cheques
( (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
drawer_user_id INTEGER NOT NULL, drawer_user_id INTEGER NOT NULL,
drawer_account_id INTEGER NOT NULL, drawer_account_id INTEGER,
cheque_number INTEGER NOT NULL, cheque_number INTEGER NOT NULL,
amount INTEGER NOT NULL, amount INTEGER NOT NULL,
underlying_security INTEGER NOT NULL, underlying_security INTEGER NOT NULL,
+2
View File
@@ -6,6 +6,8 @@ erDiagram
users ||--o{ securities : owner users ||--o{ securities : owner
security_types ||--o{ securities: s security_types ||--o{ securities: s
securities ||--o{ accounts: ps securities ||--o{ accounts: ps
securities ||--o{ security_permissions: i
users ||--o{ security_permissions: i
users ||--o{ cheques: ps users ||--o{ cheques: ps
securities ||--o{ cheques: ps securities ||--o{ cheques: ps
``` ```
+43 -10
View File
@@ -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::_securities::Security;
use crate::db::_users::User; use crate::db::_users::User;
use crate::db::external_id::SecurityId;
pub struct Account { pub struct Account {
pub id: i64, id: u32,
pub amount: i64, amount: u32,
pub timestamp: i64, timestamp: i32,
pub security: Security, security: Security,
} }
impl Account { impl Account {
pub fn id(&self) -> AccountId {
AccountId::from_id(self.id)
}
pub fn from_row(row: &Row) -> Result<Account> { pub fn from_row(row: &Row) -> Result<Account> {
return Ok(Account { return Ok(Account {
id: row.get("acc_id")?, id: row.get("acc_id")?,
@@ -55,13 +62,21 @@ pub fn get_account_by_id(tx: &Transaction, id: u32) -> Result<Account> {
return tx.query_row(q, params![id], Account::from_row); return tx.query_row(q, params![id], Account::from_row);
} }
pub fn create_account(tx: &Transaction, sec: &Security, user: &User) -> Result<Account> { pub fn create_account(tx: &Transaction, sec: &SecurityId, user: &UserId) -> Result<Account> {
let q = "INSERT INTO accounts(security, owner, amount, timestamp) let q = "INSERT INTO accounts(security, owner, amount, timestamp)
VALUES (?1, ?2, 0, CURRENT_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); 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<bool> {
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::<usize, u32>(0);
let option = tx.query_row(q, params, ite).optional()?;
Ok(option.is_some())
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use rusqlite::Connection; use rusqlite::Connection;
@@ -84,13 +99,14 @@ mod test {
return (conn, users, securities); return (conn, users, securities);
} }
fn create_all_accounts(tx: &Transaction, users: &Vec<User>, secs: &Vec<Security>) -> Result<()> { fn create_all_accounts(tx: &Transaction, users: &Vec<User>, secs: &Vec<Security>) -> Result<Vec<Account>> {
let mut ret: Vec<Account> = Vec::new();
for sec in secs.iter() { for sec in secs.iter() {
for user in users.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] #[test]
@@ -114,5 +130,22 @@ mod test {
assert_eq!(0, accs.len()); assert_eq!(0, accs.len());
return Ok(()); 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(())
}
} }
+13 -16
View File
@@ -1,36 +1,33 @@
use rusqlite::{Result, Transaction}; 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 struct SecurityEmitterCheque {
pub drawer_user_id: AccountId, pub drawer_user_id: UserId,
pub security_id: SecurityId, pub security_id: SecurityId,
pub payee_user_id: AccountId, pub payee_user_id: UserId,
pub cheque_number: u32, pub cheque_number: u32,
pub amount: u32, pub amount: u32,
} }
impl SecurityEmitterCheque { impl SecurityEmitterCheque {
fn new_safe(tx: &Transaction, pub fn new(drawer_user_id: UserId,
drawer_user_id: AccountId, security_id: SecurityId,
security_id: SecurityId, payee_user_id: UserId,
payee_user_id: AccountId, amount: u32,
amount: u32, cheque_number: u32) -> SecurityEmitterCheque {
id_generator: fn() -> u32) -> Result<SecurityEmitterCheque> { SecurityEmitterCheque {
Ok(SecurityEmitterCheque {
drawer_user_id, drawer_user_id,
security_id, security_id,
payee_user_id, payee_user_id,
cheque_number: id_generator(), cheque_number,
amount, amount,
}) }
}
pub fn assert_valid(&self, tx: &Transaction) -> Result<()> {
Ok(())
} }
} }
#[cfg(test)]
pub mod test { pub mod test {
use super::*; use super::*;
+8 -1
View File
@@ -70,6 +70,13 @@ macro_rules! impl_ids {
} }
} }
impl rusqlite::ToSql for $T {
#[inline]
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
Ok(rusqlite::types::ToSqlOutput::from(self.0.get_id()))
}
}
impl Id for $T { impl Id for $T {
type Output = $T; type Output = $T;
@@ -162,7 +169,7 @@ impl ExternalId {
} }
} }
#[cfg(test)]
pub mod test { pub mod test {
use std::any::Any; use std::any::Any;
+17 -10
View File
@@ -1,5 +1,6 @@
use rusqlite::{Connection, OptionalExtension, params, Result, Row, ToSql, Transaction}; use rusqlite::{Connection, OptionalExtension, params, Result, Row, ToSql, Transaction};
use serde::Serialize; use serde::Serialize;
use crate::db::_external_id::{Id, SecurityId, UserId};
use crate::db::users::User; use crate::db::users::User;
@@ -33,11 +34,17 @@ impl ToSql for SecurityType {
#[derive(Debug, Serialize, PartialEq)] #[derive(Debug, Serialize, PartialEq)]
pub struct Security { pub struct Security {
pub id: i32, id: u32,
security_type: SecurityType, security_type: SecurityType,
symbol: String, symbol: String,
} }
impl Security {
pub fn id(&self) -> SecurityId {
SecurityId::from_id(self.id)
}
}
impl Security { impl Security {
pub fn from_row(row: &Row) -> Result<Security> { pub fn from_row(row: &Row) -> Result<Security> {
Ok(Security { Ok(Security {
@@ -56,7 +63,7 @@ pub struct NewSecurity {
pub fn create_security(tx: &Transaction, pub fn create_security(tx: &Transaction,
sec: &NewSecurity, sec: &NewSecurity,
owner: &User) -> Result<Security> { owner: &UserId) -> Result<Security> {
let q = "INSERT INTO let q = "INSERT INTO
securities(name, symbol, security_type) securities(name, symbol, security_type)
VALUES(?1, ?2, ?3)"; VALUES(?1, ?2, ?3)";
@@ -67,18 +74,18 @@ pub fn create_security(tx: &Transaction,
let q = "INSERT INTO let q = "INSERT INTO
security_permissions(security_id, user_id) security_permissions(security_id, user_id)
VALUES(?1, ?2)"; 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); return get_security_by_id(tx, new_sec_id);
} }
pub fn is_owner(tx: &Transaction, sec: &Security, user: &User) -> Result<bool> { pub fn is_owner(tx: &Transaction, sec_id: &SecurityId, user_id: &UserId) -> Result<bool> {
let q = "SELECT id let q = "SELECT id
FROM security_permissions FROM security_permissions
WHERE security_id = ?1 AND user_id = ?2 WHERE security_id = ?1 AND user_id = ?2
LIMIT 1"; LIMIT 1";
let ret: Option<u32> = tx.query_row(q, let ret: Option<u32> = tx.query_row(q,
params![sec.id, user.id], params![sec_id, user_id],
|r| r.get(0)).optional()?; |r| r.get(0)).optional()?;
return Ok(ret.is_some()); return Ok(ret.is_some());
} }
@@ -145,10 +152,10 @@ mod test {
let not_owner = users.get(1).unwrap(); let not_owner = users.get(1).unwrap();
let sec = mock_currency_1(); let sec = mock_currency_1();
let tx = conn.transaction()?; 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.id(), &owner.id())?);
assert!(!is_owner(&tx, &sec, &not_owner)?); assert!(!is_owner(&tx, &sec.id(), &not_owner.id())?);
tx.commit() tx.commit()
} }
@@ -161,7 +168,7 @@ mod test {
let sec = mock_currency_1(); let sec = mock_currency_1();
let tx = conn.transaction()?; let tx = conn.transaction()?;
create_security(&tx, &sec, &user)?; create_security(&tx, &sec, &user.id())?;
tx.commit()?; tx.commit()?;
let ret = list_securities(&conn)?; let ret = list_securities(&conn)?;
let expt = Security { let expt = Security {
@@ -180,7 +187,7 @@ mod test {
symbol: "B".to_string(), symbol: "B".to_string(),
}; };
let tx = conn.transaction()?; let tx = conn.transaction()?;
create_security(&tx, &sec, &user)?; create_security(&tx, &sec, &user.id())?;
tx.commit()?; tx.commit()?;
let ret = list_securities(&conn)?; let ret = list_securities(&conn)?;
assert_eq!(2, ret.len()); assert_eq!(2, ret.len());
+10 -5
View File
@@ -2,6 +2,7 @@ use rusqlite::{Result, Transaction};
use crate::db::{_securities, _users}; use crate::db::{_securities, _users};
use crate::db::_accounts::create_account; use crate::db::_accounts::create_account;
use crate::db::_external_id::UserId;
use crate::db::_securities::{NewSecurity, Security, SecurityType}; use crate::db::_securities::{NewSecurity, Security, SecurityType};
use crate::db::_users::User; use crate::db::_users::User;
use crate::db::accounts::Account; use crate::db::accounts::Account;
@@ -28,22 +29,26 @@ pub fn setup_users(tx: &Transaction) -> Result<Vec<User>> {
return Ok(vec![user_1, user_2]); return Ok(vec![user_1, user_2]);
} }
pub fn setup_securities(tx: &Transaction, owner: &User) -> Result<Vec<Security>> { pub fn setup_securities(tx: &Transaction, owner: &UserId) -> Result<Vec<Security>> {
let sec_1 = _securities::create_security(tx, &mock_currency_1(), owner)?; let sec_1 = _securities::create_security(tx, &mock_currency_1(), owner)?;
let sec_2 = _securities::create_security(tx, &mock_currency_2(), owner)?; let sec_2 = _securities::create_security(tx, &mock_currency_2(), owner)?;
return Ok(vec![sec_1, sec_2]); return Ok(vec![sec_1, sec_2]);
} }
pub fn setup_users_and_securities(tx: &Transaction) -> Result<(Vec<User>, Vec<Security>)>{ pub fn setup_users_and_securities(tx: &Transaction) -> Result<(Vec<User>, Vec<Security>)> {
let users = setup_users(tx)?; 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)) Ok((users, securities))
} }
pub fn setup_basic_environment(tx: &Transaction) -> Result<(Vec<User>, Vec<Security>, Vec<Account>)> { pub fn setup_basic_environment(tx: &Transaction) -> Result<(Vec<User>, Vec<Security>, Vec<Account>)> {
let (users, securities) = setup_users_and_securities(tx)?; let (users, securities) = setup_users_and_securities(tx)?;
let acc1 = create_account(tx, securities.get(0).unwrap(), users.get(0).unwrap())?; let acc1 = create_account(tx,
let acc2 = create_account(tx, securities.get(1).unwrap(), users.get(1).unwrap())?; &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])) Ok((users, securities, vec![acc1, acc2]))
} }
+12 -6
View File
@@ -1,21 +1,28 @@
use rusqlite::{Connection, params, Result, Transaction, Row}; use rusqlite::{Connection, params, Result, Transaction, Row};
use serde::{Serialize}; use serde::{Serialize};
use crate::db::_external_id::{UserId, Id};
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct User { pub struct User {
pub id: i32, id: u32,
name: String, name: String,
public_key: String, public_key: String,
} }
pub fn create_user(tx: & Transaction, name: &String, public_key: &String) -> Result<User>{ impl User {
pub fn id(&self) -> UserId {
UserId::from_id(self.id)
}
}
pub fn create_user(tx: &Transaction, name: &String, public_key: &String) -> Result<User> {
tx.execute("INSERT INTO users (name, public_key) VALUES (?1, ?2)", tx.execute("INSERT INTO users (name, public_key) VALUES (?1, ?2)",
(name, public_key))?; (name, public_key))?;
return get_user_by_id(tx, tx.last_insert_rowid() as i32); return get_user_by_id(tx, tx.last_insert_rowid() as i32);
} }
fn _user_from_row(row: &Row) -> Result<User>{ fn _user_from_row(row: &Row) -> Result<User> {
Ok(User { Ok(User {
id: row.get(0)?, id: row.get(0)?,
name: row.get(1)?, name: row.get(1)?,
@@ -23,15 +30,14 @@ fn _user_from_row(row: &Row) -> Result<User>{
}) })
} }
fn get_user_by_id(tx: & Transaction, id: i32) -> Result<User>{ fn get_user_by_id(tx: &Transaction, id: i32) -> Result<User> {
let q = "SELECT id, name, public_key let q = "SELECT id, name, public_key
FROM users FROM users
WHERE id = ?1"; WHERE id = ?1";
tx.query_row(q, params![id], _user_from_row) tx.query_row(q, params![id], _user_from_row)
} }
pub fn list_users(conn: & Connection) -> Result<Vec<User>>{ pub fn list_users(conn: &Connection) -> Result<Vec<User>> {
let mut stmt = conn.prepare("SELECT id, name, public_key FROM users")?; let mut stmt = conn.prepare("SELECT id, name, public_key FROM users")?;
let ret: Vec<User> = stmt.query_map([], _user_from_row)? let ret: Vec<User> = stmt.query_map([], _user_from_row)?
.filter(Result::is_ok) .filter(Result::is_ok)
+5 -1
View File
@@ -5,8 +5,12 @@ mod _cheques;
mod _external_id; mod _external_id;
mod _test_utils; mod _test_utils;
pub mod external_id {
pub use super::_external_id::*;
}
pub mod securities { 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 { pub mod users {
+1
View File
@@ -13,6 +13,7 @@ use crate::db::users::{create_user, list_users};
mod migration; mod migration;
mod db; mod db;
mod services; mod services;
mod util;
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
struct Output<T> { struct Output<T> {
+68 -26
View File
@@ -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::cheques::SecurityEmitterCheque;
use crate::db::external_id::AccountId;
use crate::db::securities;
use crate::db::securities::get_security_by_id; use crate::db::securities::get_security_by_id;
use crate::util::*;
fn _add_securities_to_account(tx: &Transaction, fn _add_securities_to_account(tx: &TransactionHelper,
cheque: &SecurityEmitterCheque) -> Result<()> { 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 = " 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(()) Ok(())
} }
fn cash_cheque(tx: &Transaction, fn cash_cheque(tx: &Transaction,
cheque: &SecurityEmitterCheque) -> Result<()> { cheque: &SecurityEmitterCheque,
cheque.assert_valid(tx)?; account_id: &AccountId) -> CustomResult<()> {
// move money to new account // move money to new account
_add_securities_to_account(tx, cheque)?; if !securities::is_owner(tx, &cheque.security_id, &cheque.drawer_user_id).map_err(Error::SqlError)? {
// create transaction log return Err(Error::InvalidDrawer(format!("The drawer is the security owner: {:?}", cheque)));
// create cheque log }
Ok(())
}
fn update_account(tx: &Transaction, account_id: u32) -> Result<()> {
let account = get_account_by_id(tx, account_id)?; let myx = TransactionHelper::new(tx);
return Ok(()); _add_securities_to_account(&myx, cheque, account_id)?;
_add_cheque_to_cashed_cheques_log(&myx, cheque, account_id)?;
// create transaction log
Ok(())
} }
#[cfg(test)] #[cfg(test)]
mod 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::db::test_utils;
use crate::migration; use crate::migration;
@@ -44,19 +69,36 @@ mod test {
return conn; return conn;
} }
#[test] #[test]
fn test_basic() -> Result<()> { fn test_basic() -> Result<(), Error> {
let mut conn = setup(); let mut conn = setup();
let tx = conn.transaction()?; let tx = conn.transaction().unwrap();
let (users, securities, accounts) = test_utils::setup_basic_environment(&tx)?; let (users, securities, accounts) = test_utils::setup_basic_environment(&tx).unwrap();
tx.commit()?; 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()?; tx.commit().unwrap();
// let cheque = SecurityEmitterCheque{ let tx = conn.transaction().unwrap();
// drawer_user_id: users.get_unchecked(0).id
// };
// 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(()) Ok(())
} }
} }
+23
View File
@@ -0,0 +1,23 @@
use rusqlite::{Params, Transaction};
#[derive(Debug)]
pub enum Error {
SqlError(rusqlite::Error),
InvalidDrawer(String),
}
pub type CustomResult<T, E = Error> = Result<T, E>;
pub struct TransactionHelper<'a> {
tx: &'a Transaction<'a>,
}
impl TransactionHelper<'_> {
pub fn new<'a>(tx: &'a Transaction) -> TransactionHelper<'a> {
TransactionHelper { tx }
}
pub fn execute<P: Params>(&self, sql: &str, params: P) -> CustomResult<usize> {
self.tx.execute(sql, params).map_err(Error::SqlError)
}
}