going somewhere.
This commit is contained in:
1 parent
64e9be8b5a
commit
9ccaeaf410
5 files changed
+101
-36
No files matched your search
@@ -65,5 +65,6 @@ CREATE TABLE IF NOT EXISTS cashed_cheques
|
|||||||
|
|
||||||
FOREIGN KEY (underlying_security) REFERENCES securities (id),
|
FOREIGN KEY (underlying_security) REFERENCES securities (id),
|
||||||
FOREIGN KEY (drawer_user_id) REFERENCES users (id),
|
FOREIGN KEY (drawer_user_id) REFERENCES users (id),
|
||||||
FOREIGN KEY (drawer_account_id) REFERENCES accounts (id)
|
FOREIGN KEY (drawer_account_id) REFERENCES accounts (id),
|
||||||
|
CONSTRAINT double_spending UNIQUE (drawer_user_id, cheque_number) -- no double spending
|
||||||
);
|
);
|
||||||
+7
-2
@@ -26,6 +26,10 @@ impl Account {
|
|||||||
security: Security::from_row(row)?,
|
security: Security::from_row(row)?,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn raw_amount(&self) -> u32 {
|
||||||
|
self.amount
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn list_accounts<'a>(conn: &Connection) -> Result<Vec<Account>> {
|
pub fn list_accounts<'a>(conn: &Connection) -> Result<Vec<Account>> {
|
||||||
@@ -48,7 +52,7 @@ pub fn list_accounts<'a>(conn: &Connection) -> Result<Vec<Account>> {
|
|||||||
return Ok(ret);
|
return Ok(ret);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_account_by_id(tx: &Transaction, id: u32) -> Result<Account> {
|
pub fn get_account_by_id(tx: &Transaction, id: &AccountId) -> Result<Account> {
|
||||||
let q = "SELECT acc.id AS acc_id,
|
let q = "SELECT acc.id AS acc_id,
|
||||||
acc.amount AS acc_amount,
|
acc.amount AS acc_amount,
|
||||||
UNIXEPOCH(acc.timestamp) AS acc_timestamp,
|
UNIXEPOCH(acc.timestamp) AS acc_timestamp,
|
||||||
@@ -66,7 +70,8 @@ pub fn create_account(tx: &Transaction, sec: &SecurityId, user: &UserId) -> Resu
|
|||||||
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, user))?;
|
tx.execute(q, (sec, user))?;
|
||||||
return get_account_by_id(tx, tx.last_insert_rowid() as u32);
|
let id = AccountId::from_id(tx.last_insert_rowid() as u32);
|
||||||
|
return get_account_by_id(tx, &id);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_owner(tx: &Transaction, account_id: &AccountId, user_id: &UserId) -> Result<bool> {
|
pub fn is_owner(tx: &Transaction, account_id: &AccountId, user_id: &UserId) -> Result<bool> {
|
||||||
|
|||||||
+7
-10
@@ -1,4 +1,5 @@
|
|||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
use regex::{Captures, Regex};
|
use regex::{Captures, Regex};
|
||||||
|
|
||||||
@@ -17,16 +18,12 @@ pub trait Id {
|
|||||||
fn get_id(&self) -> u32;
|
fn get_id(&self) -> u32;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for SafeId {
|
||||||
impl ToString for SafeId {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
fn to_string(&self) -> String {
|
write!(f, "{}-{}", hex::encode(self.encrypted_id), hex::encode(self.signature))
|
||||||
format!("{}-{}",
|
|
||||||
hex::encode(self.encrypted_id),
|
|
||||||
hex::encode(self.signature))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl Id for SafeId {
|
impl Id for SafeId {
|
||||||
type Output = Self;
|
type Output = Self;
|
||||||
|
|
||||||
@@ -64,9 +61,9 @@ impl Id for SafeId {
|
|||||||
macro_rules! impl_ids {
|
macro_rules! impl_ids {
|
||||||
($T:ident, $prefix:literal) => {
|
($T:ident, $prefix:literal) => {
|
||||||
|
|
||||||
impl ToString for $T {
|
impl fmt::Display for $T {
|
||||||
fn to_string(&self) -> String {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
format!("{}-{}", $prefix, self.0.to_string())
|
write!(f, "{}-{}", $prefix, self.0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use rusqlite::{params, Params, Transaction};
|
use rusqlite::{ffi, params, Params, Transaction};
|
||||||
|
|
||||||
use crate::db::accounts::{Account, get_account_by_id};
|
use crate::db::accounts::{Account, get_account_by_id};
|
||||||
use crate::db::cheques::SecurityEmitterCheque;
|
use crate::db::cheques::SecurityEmitterCheque;
|
||||||
@@ -29,21 +29,29 @@ fn _add_cheque_to_cashed_cheques_log(tx: &TransactionHelper,
|
|||||||
)
|
)
|
||||||
VALUES(?1, ?2, ?3, ?4)
|
VALUES(?1, ?2, ?3, ?4)
|
||||||
";
|
";
|
||||||
tx.execute(q, params![cheque.drawer_user_id,
|
let ret = tx.execute(q, params![cheque.drawer_user_id,
|
||||||
cheque.cheque_number,
|
cheque.cheque_number,
|
||||||
cheque.amount,
|
cheque.amount,
|
||||||
cheque.security_id])?;
|
cheque.security_id]);
|
||||||
Ok(())
|
match ret {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(Error::SqlError(rusqlite::Error::SqliteFailure(ffi::Error { code: _, extended_code: 2067 }, _))) =>
|
||||||
|
Err(Error::DoubleSpendingError(format!("Cheque number {} from user {} has already been cached.", cheque.cheque_number, cheque.drawer_user_id))),
|
||||||
|
Err(a) => Err(a)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cash_cheque(tx: &Transaction,
|
fn cash_cheque(tx: &Transaction,
|
||||||
cheque: &SecurityEmitterCheque,
|
cheque: &SecurityEmitterCheque,
|
||||||
account_id: &AccountId) -> CustomResult<()> {
|
account_id: &AccountId) -> CustomResult<()> {
|
||||||
// move money to new account
|
|
||||||
if !securities::is_owner(tx, &cheque.security_id, &cheque.drawer_user_id).map_err(Error::SqlError)? {
|
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)));
|
return Err(Error::InvalidDrawer(format!("The drawer is the security owner: {:?}", cheque)));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// todo:
|
||||||
|
// 1. payee is owner of the account.
|
||||||
|
|
||||||
|
|
||||||
let myx = TransactionHelper::new(tx);
|
let myx = TransactionHelper::new(tx);
|
||||||
_add_securities_to_account(&myx, cheque, account_id)?;
|
_add_securities_to_account(&myx, cheque, account_id)?;
|
||||||
@@ -54,51 +62,103 @@ fn cash_cheque(tx: &Transaction,
|
|||||||
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod test {
|
mod test_caching_cheques {
|
||||||
use rusqlite::{Connection, params};
|
use rusqlite::{Connection, params};
|
||||||
use crate::db::accounts::create_account;
|
use crate::db::accounts::create_account;
|
||||||
|
|
||||||
use crate::db::test_utils;
|
use crate::db::{accounts, test_utils};
|
||||||
|
use crate::db::external_id::{Id, SecurityId, UserId};
|
||||||
|
use crate::db::users::User;
|
||||||
use crate::migration;
|
use crate::migration;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn setup() -> Connection {
|
fn setup_conn() -> Connection {
|
||||||
let mut conn = Connection::open_in_memory().unwrap();
|
let mut conn = Connection::open_in_memory().unwrap();
|
||||||
migration::apply_migrations(&mut conn).unwrap();
|
migration::apply_migrations(&mut conn).unwrap();
|
||||||
return conn;
|
return conn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn setup_basic() -> (Connection, UserId, SecurityId, UserId, AccountId) {
|
||||||
#[test]
|
let mut conn = setup_conn();
|
||||||
fn test_basic() -> Result<(), Error> {
|
|
||||||
let mut conn = setup();
|
|
||||||
let tx = conn.transaction().unwrap();
|
let tx = conn.transaction().unwrap();
|
||||||
let (users, securities, accounts) = test_utils::setup_basic_environment(&tx).unwrap();
|
let (users, securities, accounts) = test_utils::setup_basic_environment(&tx).unwrap();
|
||||||
let sec = securities.get(0).unwrap();
|
let sec = securities.get(0).unwrap();
|
||||||
let drawer = users.get(0).unwrap();
|
let drawer = users.get(0).unwrap();
|
||||||
let payee = users.get(1).unwrap();
|
let payee = users.get(1).unwrap();
|
||||||
let amount: u32 = 1000;
|
|
||||||
let payee_account = create_account(&tx, &sec.id(), &payee.id()).unwrap();
|
let payee_account = create_account(&tx, &sec.id(), &payee.id()).unwrap();
|
||||||
|
|
||||||
tx.commit().unwrap();
|
tx.commit().unwrap();
|
||||||
|
(conn, drawer.id(), sec.id(), payee.id(), payee_account.id())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_basic_happy_path() -> CustomResult<()> {
|
||||||
|
let (mut conn, drawer, sec, payee, payee_account) = setup_basic();
|
||||||
let tx = conn.transaction().unwrap();
|
let tx = conn.transaction().unwrap();
|
||||||
|
|
||||||
|
|
||||||
// let tx = conn.transaction()?;
|
|
||||||
// SecurityEmitterCheque::new(securities.get())
|
|
||||||
let cheque = SecurityEmitterCheque::new(
|
let cheque = SecurityEmitterCheque::new(
|
||||||
drawer.id(),
|
drawer,
|
||||||
sec.id(),
|
sec,
|
||||||
payee.id(),
|
payee,
|
||||||
amount,
|
1000,
|
||||||
99,
|
99,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
cash_cheque(&tx, &cheque, &payee_account)?;
|
||||||
cash_cheque(&tx, &cheque, &payee_account.id())?;
|
|
||||||
|
|
||||||
tx.commit().unwrap();
|
tx.commit().unwrap();
|
||||||
|
|
||||||
|
let tx = conn.transaction().unwrap();
|
||||||
|
let acc = get_account_by_id(&tx, &payee_account).unwrap();
|
||||||
|
assert_eq!(acc.raw_amount(), 1000);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_double_spend() -> CustomResult<()> {
|
||||||
|
let (mut conn, drawer, sec, payee, payee_account) = setup_basic();
|
||||||
|
let amount: u32 = 1000;
|
||||||
|
let tx = conn.transaction().unwrap();
|
||||||
|
|
||||||
|
let cheque = SecurityEmitterCheque::new(
|
||||||
|
drawer,
|
||||||
|
sec,
|
||||||
|
payee,
|
||||||
|
1000,
|
||||||
|
99,
|
||||||
|
);
|
||||||
|
|
||||||
|
cash_cheque(&tx, &cheque, &payee_account)?;
|
||||||
|
tx.commit().unwrap();
|
||||||
|
let tx = conn.transaction().unwrap();
|
||||||
|
|
||||||
|
let ret = cash_cheque(&tx, &cheque, &payee_account);
|
||||||
|
match ret {
|
||||||
|
Err(Error::DoubleSpendingError(_)) => Ok(()),
|
||||||
|
_ => Err(Error::Testing("Expected double spending error here.".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_invalid_drawer() -> CustomResult<()> {
|
||||||
|
let (mut conn, drawer, sec, payee, payee_account) = setup_basic();
|
||||||
|
let amount: u32 = 1000;
|
||||||
|
let tx = conn.transaction().unwrap();
|
||||||
|
let not_currency_owner = UserId::from_id(drawer.get_id() + 72623);
|
||||||
|
let cheque = SecurityEmitterCheque::new(
|
||||||
|
not_currency_owner,
|
||||||
|
sec,
|
||||||
|
payee,
|
||||||
|
1000,
|
||||||
|
99,
|
||||||
|
);
|
||||||
|
|
||||||
|
let ret = cash_cheque(&tx, &cheque, &payee_account);
|
||||||
|
match ret {
|
||||||
|
Err(Error::InvalidDrawer(_)) => Ok(()),
|
||||||
|
_ => Err(Error::Testing("Expected invalid drawer here.".to_string()))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -4,6 +4,8 @@ use rusqlite::{Params, Transaction};
|
|||||||
pub enum Error {
|
pub enum Error {
|
||||||
SqlError(rusqlite::Error),
|
SqlError(rusqlite::Error),
|
||||||
InvalidDrawer(String),
|
InvalidDrawer(String),
|
||||||
|
DoubleSpendingError(String),
|
||||||
|
Testing(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type CustomResult<T, E = Error> = Result<T, E>;
|
pub type CustomResult<T, E = Error> = Result<T, E>;
|
||||||
|
|||||||
Reference in new issue
Block a user