Files
escarmouche/src/db/_accounts.rs
T
2022-12-20 10:18:17 -05:00

223 lines
7.2 KiB
Rust

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;
#[derive(PartialEq, Debug)]
pub enum AccountType {
Checking,
SecurityEmitter,
}
impl AccountType {
pub fn from_row(row: &Row) -> Result<AccountType> {
let s: String = row.get("acc_account_type")?;
if s == "security_emitter" {
Ok(AccountType::SecurityEmitter)
} else {
Ok(AccountType::Checking)
}
}
}
pub struct Account {
id: u32,
amount: u32,
timestamp: i32,
security: Security,
account_type: AccountType,
}
impl Account {
pub fn id(&self) -> AccountId {
AccountId::from_id(self.id)
}
pub fn from_row(row: &Row) -> Result<Account> {
return Ok(Account {
id: row.get("acc_id")?,
amount: row.get("acc_amount")?,
timestamp: row.get("acc_timestamp")?,
security: Security::from_row(row)?,
account_type: AccountType::from_row(row)?,
});
}
pub fn raw_amount(&self) -> u32 {
self.amount
}
}
pub fn list_accounts<'a>(conn: &Connection) -> Result<Vec<Account>> {
let q = "SELECT acc.id AS acc_id,
acc.amount AS acc_amount,
UNIXEPOCH(acc.timestamp) AS acc_timestamp,
account_type AS acc_account_type,
sec.id AS sec_id,
sec.symbol AS sec_symbol,
sec.security_type AS sec_security_type
FROM accounts acc
JOIN securities sec ON acc.security = sec.id
ORDER BY acc.id ASC";
let mut stmt = conn.prepare(q)?;
let ret: Vec<Account> = stmt
.query_map([], &Account::from_row)?
.filter(Result::is_ok)
.map(Result::unwrap)
.collect();
return Ok(ret);
}
pub fn get_account_by_id(tx: &Transaction, id: &AccountId) -> Result<Account> {
let q = "SELECT acc.id AS acc_id,
acc.amount AS acc_amount,
UNIXEPOCH(acc.timestamp) AS acc_timestamp,
account_type AS acc_account_type,
sec.id AS sec_id,
sec.symbol AS sec_symbol,
sec.security_type AS sec_security_type
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);
}
fn create_account(tx: &Transaction, sec: &SecurityId, user: &UserId, account_type: AccountType) -> Result<Account> {
let q = "INSERT INTO accounts(security, owner, amount, timestamp, account_type)
VALUES (?1, ?2, 0, CURRENT_TIMESTAMP, ?3)";
let account_type = match account_type {
AccountType::Checking => "checking",
AccountType::SecurityEmitter => "security_emitter",
};
tx.execute(q, (sec, user, account_type))?;
let id = AccountId::from_id(tx.last_insert_rowid() as u32);
return get_account_by_id(tx, &id);
}
pub fn create_checking_account(tx: &Transaction, sec: &SecurityId, user: &UserId) -> Result<Account> {
create_account(tx, sec, user, AccountType::Checking)
}
pub fn create_security_emitter_account(tx: &Transaction, sec: &SecurityId, user: &UserId) -> Result<Account> {
create_account(tx, sec, user, AccountType::SecurityEmitter)
}
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)]
mod test_accounts {
use rusqlite::Connection;
use crate::db::_test_utils::{setup_users_and_securities};
use crate::db::_securities;
use crate::db::_users;
use crate::migration;
use super::*;
fn setup() -> (Connection, Vec<User>, Vec<Security>) {
let mut conn = Connection::open_in_memory().unwrap();
migration::apply_migrations(&mut conn).unwrap();
let tx = conn.transaction().unwrap();
let (users, securities) = setup_users_and_securities(&tx).unwrap();
tx.commit().unwrap();
return (conn, users, securities);
}
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 user in users.iter() {
ret.push(create_checking_account(&tx, &sec.id(), &user.id())?);
}
}
return Ok(ret);
}
#[test]
fn test_simplest_checking_account() -> Result<()> {
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 acc = create_checking_account(&tx, &sec.id(), &user.id())?;
assert_eq!(acc.account_type, AccountType::Checking);
assert_eq!(0, acc.raw_amount());
assert!(is_owner(& tx, & acc.id(), &user.id())?);
assert!(!is_owner(& tx, & acc.id(), &not_owner.id())?);
Ok(())
}
#[test]
fn test_simplest_security_emitter_account() -> Result<()> {
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 acc = create_security_emitter_account(&tx, &sec.id(), &user.id())?;
assert_eq!(acc.account_type, AccountType::SecurityEmitter);
assert_eq!(0, acc.raw_amount());
assert!(is_owner(& tx, & acc.id(), &user.id())?);
assert!(!is_owner(& tx, & acc.id(), &not_owner.id())?);
Ok(())
}
#[test]
fn test_happy_path() -> Result<()> {
let (mut conn, users, secs) = setup();
let tx = conn.transaction()?;
create_all_accounts(&tx, &users, &secs)?;
tx.commit()?;
let accs = list_accounts(&conn)?;
assert_eq!(4, accs.len());
assert_eq!(1, accs.get(0).unwrap().id);
return Ok(());
}
#[test]
fn test_happy_path_no_commit() -> Result<()> {
let (mut conn, users, secs) = setup();
create_all_accounts(&conn.transaction()?, &users, &secs)?;
let accs = list_accounts(&conn)?;
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_checking_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(())
}
}