424 lines
13 KiB
Rust
424 lines
13 KiB
Rust
use fraction::Fraction;
|
|
use rusqlite::{params, Connection, OptionalExtension, Result, Row};
|
|
|
|
use crate::db::_external_id::{AccountId, Id, UserId};
|
|
use crate::db::_securities::Security;
|
|
|
|
use crate::db::external_id::SecurityId;
|
|
use crate::util::{CustomResult, Error};
|
|
|
|
#[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: i64,
|
|
amount: i64,
|
|
timestamp: i64,
|
|
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) -> i64 {
|
|
self.amount
|
|
}
|
|
}
|
|
|
|
#[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,
|
|
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).map_err(Error::SqlError)?;
|
|
let ret: Vec<Account> = stmt
|
|
.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) -> CustomResult<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)
|
|
.map_err(Error::SqlError);
|
|
}
|
|
|
|
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))
|
|
.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]).map_err(Error::SqlError)?;
|
|
|
|
update_account_history(tx, &id)?;
|
|
return get_account_by_id(tx, &id);
|
|
}
|
|
|
|
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,
|
|
) -> CustomResult<Account> {
|
|
create_account(tx, sec, user, AccountType::SecurityEmitter)
|
|
}
|
|
|
|
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()
|
|
.map_err(Error::SqlError)?;
|
|
Ok(option.is_some())
|
|
}
|
|
|
|
pub struct AccountChangeReceipt {
|
|
pub account_id: AccountId,
|
|
pub version: i64,
|
|
pub timestamp: i64,
|
|
pub amount: i64,
|
|
}
|
|
|
|
impl AccountChangeReceipt {
|
|
pub fn from_row(row: &Row) -> Result<AccountChangeReceipt> {
|
|
Ok(AccountChangeReceipt {
|
|
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<AccountChangeReceipt> {
|
|
let q = "
|
|
UPDATE accounts SET amount = CASE account_type
|
|
WHEN 'security_emitter'
|
|
THEN amount
|
|
ELSE amount + ?1 END,
|
|
version = version + 1,
|
|
timestamp = CURRENT_TIMESTAMP
|
|
WHERE id = ?2
|
|
RETURNING id, version, amount, UNIXEPOCH(CURRENT_TIMESTAMP) as timestamp
|
|
";
|
|
let params = params![amount, account_id];
|
|
let ret = tx
|
|
.query_row(q, params, AccountChangeReceipt::from_row)
|
|
.map_err(Error::SqlError);
|
|
update_account_history(tx, account_id)?;
|
|
return ret;
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod test_accounts {
|
|
use rusqlite::{Connection, Transaction};
|
|
|
|
use crate::db::_test_utils::setup_users_and_securities;
|
|
use crate::db::users::User;
|
|
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>,
|
|
) -> CustomResult<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() -> 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().unwrap();
|
|
|
|
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(), ¬_owner.id())?);
|
|
|
|
assert_eq!(get_account_history(&tx, &acc.id())?.len(), 1);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
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().unwrap();
|
|
|
|
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(), ¬_owner.id())?);
|
|
Ok(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_happy_path() -> CustomResult<()> {
|
|
let (mut conn, users, secs) = setup();
|
|
let tx = conn.transaction().unwrap();
|
|
create_all_accounts(&tx, &users, &secs)?;
|
|
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() -> CustomResult<()> {
|
|
let (mut conn, users, secs) = setup();
|
|
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() -> CustomResult<()> {
|
|
let (mut conn, users, secs) = setup();
|
|
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();
|
|
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(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_add_to_checking_account() -> CustomResult<()> {
|
|
let (mut conn, users, secs) = setup();
|
|
let tx = conn.transaction().unwrap();
|
|
let sec = secs.get(0).unwrap().id();
|
|
let user = users.get(0).unwrap().id();
|
|
let acc = create_checking_account(&tx, &sec, &user).unwrap();
|
|
let receipt = add_to_account(&tx, &acc.id(), 33)?;
|
|
assert_eq!(receipt.account_id, acc.id());
|
|
assert_eq!(receipt.amount, 33);
|
|
tx.commit().unwrap();
|
|
|
|
let tx2 = conn.transaction().unwrap();
|
|
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(())
|
|
}
|
|
|
|
#[test]
|
|
fn test_add_to_currency_emitter_account() -> CustomResult<()> {
|
|
let (mut conn, users, secs) = setup();
|
|
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)?;
|
|
|
|
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();
|
|
let acc = get_account_by_id(&tx2, &acc.id()).unwrap();
|
|
assert_eq!(acc.timestamp, receipt.timestamp);
|
|
assert_eq!(acc.amount, 0);
|
|
|
|
assert_eq!(get_account_history(&tx2, &acc.id())?.len(), 2);
|
|
Ok(())
|
|
}
|
|
}
|