I need to fucking work.

This commit is contained in:
didier amyot committed 2023-01-25 12:50:16 -05:00
1 parent e1e5d5dbf3
commit f99513a8c8
7 files changed
+230 -53

No files matched your search

+127 -30
View File
@@ -1,4 +1,5 @@
use rusqlite::{Connection, OptionalExtension, params, Result, Row, Transaction};
use fraction::{Fraction};
use crate::db::_external_id::{AccountId, Id, UserId};
use crate::db::_securities::Security;
@@ -51,7 +52,56 @@ impl Account {
}
}
pub fn list_accounts<'a>(conn: &Connection) -> Result<Vec<Account>> {
#[derive(Debug)]
struct AccountSnapshot {
amount: i64,
version: i64,
timestamp: i64,
}
impl AccountSnapshot {
pub fn from_row(row: &Row) -> Result<AccountSnapshot> {
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<AccountSnapshot>,
}
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<AccountSnapshot>) -> CustomResult<AccountHistory> {
Ok(AccountHistory {
id: account.id,
security: account.security,
account_type: account.account_type,
snapshots,
})
}
}
pub fn list_accounts<'a>(conn: &Connection) -> CustomResult<Vec<Account>> {
let q = "SELECT acc.id AS acc_id,
acc.amount AS acc_amount,
UNIXEPOCH(acc.timestamp) AS acc_timestamp,
@@ -63,16 +113,17 @@ pub fn list_accounts<'a>(conn: &Connection) -> Result<Vec<Account>> {
FROM accounts acc
JOIN securities sec ON acc.security = sec.id
ORDER BY acc.id ASC";
let mut stmt = conn.prepare(q)?;
let mut stmt = conn.prepare(q).map_err(Error::SqlError)?;
let ret: Vec<Account> = stmt
.query_map([], &Account::from_row)?
.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) -> Result<Account> {
pub fn get_account_by_id(tx: &Connection, id: &AccountId) -> CustomResult<Account> {
let q = "SELECT acc.id AS acc_id,
acc.amount AS acc_amount,
UNIXEPOCH(acc.timestamp) AS acc_timestamp,
@@ -84,43 +135,64 @@ pub fn get_account_by_id(tx: &Connection, id: &AccountId) -> Result<Account> {
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);
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) -> Result<Account> {
let q = "INSERT INTO accounts(security, amount, timestamp, account_type)
VALUES (?1, 0, CURRENT_TIMESTAMP, ?2)";
fn create_account(tx: &Connection, sec: &SecurityId, user: &UserId, account_type: AccountType) -> CustomResult<Account> {
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))?;
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])?;
tx.execute(q, params![user, id]).map_err(Error::SqlError)?;
update_account_history(tx, &id)?;
return get_account_by_id(tx, &id);
}
pub fn create_checking_account(tx: &Connection, sec: &SecurityId, user: &UserId) -> Result<Account> {
fn get_account_history(tx: &Connection, id: &AccountId) -> CustomResult<AccountHistory> {
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<AccountSnapshot> = 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<Account> {
create_account(tx, sec, user, AccountType::Checking)
}
pub fn create_security_emitter_account(tx: &Connection, sec: &SecurityId, user: &UserId) -> Result<Account> {
pub fn create_security_emitter_account(tx: &Connection, sec: &SecurityId, user: &UserId) -> CustomResult<Account> {
create_account(tx, sec, user, AccountType::SecurityEmitter)
}
pub fn is_owner(tx: &Connection, account_id: &AccountId, user_id: &UserId) -> Result<bool> {
pub fn is_owner(tx: &Connection, account_id: &AccountId, user_id: &UserId) -> CustomResult<bool> {
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::<usize, i64>(0);
let option = tx.query_row(q, params, ite).optional()?;
let option = tx.query_row(q, params, ite).optional().map_err(Error::SqlError)?;
Ok(option.is_some())
}
pub struct Receipt {
pub account_id: AccountId,
pub version: i64,
pub timestamp: i64,
pub amount: i64,
}
@@ -129,12 +201,27 @@ impl Receipt {
pub fn from_row(row: &Row) -> Result<Receipt> {
Ok(Receipt {
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<Receipt> {
@@ -143,12 +230,15 @@ pub fn add_to_account(tx: &Connection,
WHEN 'security_emitter'
THEN amount
ELSE amount + ?1 END,
version = version + 1,
timestamp = CURRENT_TIMESTAMP
WHERE id = ?2
RETURNING id, amount, UNIXEPOCH(CURRENT_TIMESTAMP) as timestamp
RETURNING id, version, amount, UNIXEPOCH(CURRENT_TIMESTAMP) as timestamp
";
let params = params![amount, account_id];
tx.query_row(q, params, Receipt::from_row).map_err(Error::SqlError)
let ret = tx.query_row(q, params, Receipt::from_row).map_err(Error::SqlError);
update_account_history(tx, account_id)?;
return ret;
}
#[cfg(test)]
@@ -171,7 +261,7 @@ mod test_accounts {
return (conn, users, securities);
}
fn create_all_accounts(tx: &Transaction, users: &Vec<User>, secs: &Vec<Security>) -> Result<Vec<Account>> {
fn create_all_accounts(tx: &Transaction, users: &Vec<User>, secs: &Vec<Security>) -> CustomResult<Vec<Account>> {
let mut ret: Vec<Account> = Vec::new();
for sec in secs.iter() {
for user in users.iter() {
@@ -182,12 +272,12 @@ mod test_accounts {
}
#[test]
fn test_simplest_checking_account() -> Result<()> {
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()?;
let tx = conn.transaction().unwrap();
let acc = create_checking_account(&tx, &sec.id(), &user.id())?;
assert_eq!(acc.account_type, AccountType::Checking);
@@ -195,16 +285,18 @@ mod test_accounts {
assert!(is_owner(&tx, &acc.id(), &user.id())?);
assert!(!is_owner(&tx, &acc.id(), &not_owner.id())?);
assert_eq!(get_account_history(&tx, &acc.id())?.len(), 1);
Ok(())
}
#[test]
fn test_simplest_security_emitter_account() -> Result<()> {
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()?;
let tx = conn.transaction().unwrap();
let acc = create_security_emitter_account(&tx, &sec.id(), &user.id())?;
assert_eq!(acc.account_type, AccountType::SecurityEmitter);
@@ -216,30 +308,30 @@ mod test_accounts {
}
#[test]
fn test_happy_path() -> Result<()> {
fn test_happy_path() -> CustomResult<()> {
let (mut conn, users, secs) = setup();
let tx = conn.transaction()?;
let tx = conn.transaction().unwrap();
create_all_accounts(&tx, &users, &secs)?;
tx.commit()?;
let accs = list_accounts(&conn)?;
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() -> Result<()> {
fn test_happy_path_no_commit() -> CustomResult<()> {
let (mut conn, users, secs) = setup();
create_all_accounts(&conn.transaction()?, &users, &secs)?;
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() -> Result<()> {
fn test_is_owner() -> CustomResult<()> {
let (mut conn, users, secs) = setup();
let tx = &conn.transaction()?;
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();
@@ -269,6 +361,8 @@ mod test_accounts {
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(())
}
@@ -279,10 +373,12 @@ mod test_accounts {
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 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();
@@ -290,6 +386,7 @@ mod test_accounts {
assert_eq!(acc.timestamp, receipt.timestamp);
assert_eq!(acc.amount, 0);
assert_eq!(get_account_history(&tx2, &acc.id())?.len(), 2);
Ok(())
}
}
+3 -2
View File
@@ -6,6 +6,7 @@ use crate::db::_external_id::UserId;
use crate::db::_securities::{NewSecurity, Security, SecurityType};
use crate::db::_users::User;
use crate::db::accounts::Account;
use crate::util::CustomResult;
pub fn mock_currency_1() -> NewSecurity {
return NewSecurity {
@@ -41,8 +42,8 @@ pub fn setup_users_and_securities(tx: &Transaction) -> Result<(Vec<User>, Vec<Se
Ok((users, securities))
}
pub fn setup_basic_environment(tx: &Transaction) -> Result<(Vec<User>, Vec<Security>, Vec<Account>)> {
let (users, securities) = setup_users_and_securities(tx)?;
pub fn setup_basic_environment(tx: &Transaction) -> CustomResult<(Vec<User>, Vec<Security>, Vec<Account>)> {
let (users, securities) = setup_users_and_securities(tx).unwrap();
let acc1 = create_checking_account(tx,
&securities.get(0).unwrap().id(),
+1
View File
@@ -20,6 +20,7 @@ pub mod users {
pub mod accounts {
pub use super::_accounts::{Account,
Receipt,
create_checking_account,
create_security_emitter_account,
is_owner,
+18 -10
View File
@@ -8,35 +8,40 @@ use crate::util::*;
fn _move_securities(tx: &Savepoint,
cheque: &Cheque,
account_id: &AccountId) -> CustomResult<()> {
account_id: &AccountId) -> CustomResult<(accounts::Receipt, accounts::Receipt)> {
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)?;
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(())
Ok((drawer_receipt, payee_receipt))
}
fn _add_cheque_to_cashed_cheques_log(tx: &Connection,
cheque: &Cheque,
payee_account_id: &AccountId) -> Result<(), Error> {
drawer_receipt: &accounts::Receipt,
payee_receipt: &accounts::Receipt) -> Result<(), Error> {
let q = "
INSERT INTO cashed_cheques(
drawer_user_id,
drawer_account_id,
drawer_account_version,
payee_user_id,
payee_account_id,
payee_account_version,
cheque_number,
amount,
underlying_security
)
VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7)
VALUES(?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9)
";
let params = params![cheque.drawer_user_id,
cheque.drawer_account_id,
drawer_receipt.version,
cheque.payee_user_id,
payee_account_id,
payee_receipt.account_id,
payee_receipt.version,
cheque.cheque_number,
cheque.amount,
cheque.security_id];
@@ -55,16 +60,19 @@ fn cash_cheque_unsafe(savepoint: &Savepoint,
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)? {
if !accounts::is_owner(&savepoint, &cheque.drawer_account_id, &cheque.drawer_user_id)? {
return Err(Error::InvalidDrawer(format!("The drawer is the security owner: {:?}", cheque)));
}
if !accounts::is_owner(&savepoint, account_id, &cheque.payee_user_id).map_err(Error::SqlError)? {
if !accounts::is_owner(&savepoint, 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)));
}
_move_securities(&savepoint, cheque, account_id)?;
_add_cheque_to_cashed_cheques_log(&savepoint, cheque, account_id)?;
let (drawer_receipt, payee_receipt) = _move_securities(&savepoint, cheque, account_id)?;
_add_cheque_to_cashed_cheques_log(&savepoint,
cheque,
&drawer_receipt,
&payee_receipt)?;
Ok(())
}