Started to implement accounts in the UI.

This commit is contained in:
didier amyot committed 2023-04-01 09:16:38 -04:00
1 parent ce92916942
commit 76e1e8cb53
7 files changed
+171 -8

No files matched your search

+85 -4
View File
@@ -6,6 +6,7 @@ use crate::db::_securities::Security;
use crate::db::external_id::SecurityId; use crate::db::external_id::SecurityId;
use crate::util::{CustomResult, Error}; use crate::util::{CustomResult, Error};
use crate::util::Error::FuckingError;
#[derive(PartialEq, Debug)] #[derive(PartialEq, Debug)]
pub enum AccountType { pub enum AccountType {
@@ -14,13 +15,28 @@ pub enum AccountType {
} }
impl AccountType { impl AccountType {
fn from_string(s: & String) -> Option<AccountType>{
if s == "security_emitter" {
Some(AccountType::SecurityEmitter)
} else if s == "checking"{
Some(AccountType::Checking)
}else{
None
}
}
pub fn from_row(row: &Row) -> Result<AccountType> { pub fn from_row(row: &Row) -> Result<AccountType> {
let s: String = row.get("acc_account_type")?; let s: String = row.get("acc_account_type")?;
if s == "security_emitter" { AccountType::from_string(& s).ok_or(rusqlite::Error::ExecuteReturnedResults)
Ok(AccountType::SecurityEmitter)
} else {
Ok(AccountType::Checking)
} }
}
impl ToString for AccountType{
fn to_string(&self) -> String {
match self {
AccountType::Checking => "checking",
AccountType::SecurityEmitter => "security_emitter",
}.to_string()
} }
} }
@@ -50,6 +66,19 @@ impl Account {
pub fn raw_amount(&self) -> i64 { pub fn raw_amount(&self) -> i64 {
self.amount self.amount
} }
pub fn amount(&self) -> i64 {
self.amount
}
pub fn timestamp(&self) -> i64 {
self.timestamp
}
pub fn security(&self) -> &Security {
&self.security
}
pub fn account_type(&self) -> &AccountType {
&self.account_type
}
} }
#[derive(Debug)] #[derive(Debug)]
@@ -123,6 +152,31 @@ pub fn list_accounts<'a>(conn: &Connection) -> CustomResult<Vec<Account>> {
return Ok(ret); return Ok(ret);
} }
pub fn list_user_accounts(conn: &Connection, user_id: & UserId) -> 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,
sec.name AS sec_name
FROM accounts acc
JOIN securities sec ON acc.security = sec.id
JOIN account_user_permissions ON account_user_permissions.account_id = acc.id
WHERE account_user_permissions.user_id = ?1
ORDER BY acc.id ASC";
let mut stmt = conn.prepare(q).map_err(Error::SqlError)?;
let ret: Vec<Account> = stmt
.query_map(params![user_id], &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> { pub fn get_account_by_id(tx: &Connection, id: &AccountId) -> CustomResult<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,
@@ -270,6 +324,7 @@ pub fn add_to_account(
#[cfg(test)] #[cfg(test)]
mod test_accounts { mod test_accounts {
use rusqlite::{Connection, Transaction}; use rusqlite::{Connection, Transaction};
use serde_json::to_string;
use crate::db::_test_utils::setup_users_and_securities; use crate::db::_test_utils::setup_users_and_securities;
use crate::db::users::User; use crate::db::users::User;
@@ -302,6 +357,18 @@ mod test_accounts {
return Ok(ret); return Ok(ret);
} }
fn assert_test_account_type_to_string_and_from_row_works(account_type: AccountType){
let serialized = account_type.to_string();
assert_eq!(AccountType::from_string(&serialized).unwrap(), account_type);
}
#[test]
fn test_account_type_to_string_and_from_row(){
assert_test_account_type_to_string_and_from_row_works(AccountType::Checking);
assert_test_account_type_to_string_and_from_row_works(AccountType::SecurityEmitter);
}
#[test] #[test]
fn test_simplest_checking_account() -> CustomResult<()> { fn test_simplest_checking_account() -> CustomResult<()> {
let (mut conn, users, secs) = setup(); let (mut conn, users, secs) = setup();
@@ -422,4 +489,18 @@ mod test_accounts {
assert_eq!(get_account_history(&tx2, &acc.id())?.len(), 2); assert_eq!(get_account_history(&tx2, &acc.id())?.len(), 2);
Ok(()) Ok(())
} }
#[test]
fn test_list_user_accounts() -> CustomResult<()> {
let (mut conn, users, secs) = setup();
{
let tx = conn.transaction().unwrap();
create_all_accounts(& tx, &users, &secs)?;
tx.commit().map_err(Error::SqlError)?;
}
let res = list_user_accounts(& conn, & users[0].id())?;
assert_eq!(res.len(), 2);
Ok(())
}
} }
+1
View File
@@ -29,6 +29,7 @@ pub mod accounts {
pub use super::_accounts::{ pub use super::_accounts::{
add_to_account, create_checking_account, create_security_emitter_account, add_to_account, create_checking_account, create_security_emitter_account,
get_account_by_id, is_owner, list_accounts, Account, AccountChangeReceipt, get_account_by_id, is_owner, list_accounts, Account, AccountChangeReceipt,
list_user_accounts
}; };
} }
+52
View File
@@ -0,0 +1,52 @@
use rusqlite::Connection;
use crate::db::accounts::{Account, list_user_accounts};
use crate::db::external_id::{Id, UserId};
use crate::util::{CustomResult, Error};
use serde::Deserialize;
use serde::Serialize;
pub struct AccountService<'a> {
conn: &'a Connection,
}
#[derive(Deserialize, Serialize, Debug)]
pub struct AccountDTO {
pub id: String,
pub amount: i64,
pub account_type: String,
pub last_update: i64,
pub security_symbol: String,
}
impl AccountDTO {
fn from_entity(account: & Account) -> AccountDTO {
AccountDTO{
id: account.id().to_string(),
amount: account.amount(),
account_type: account.account_type().to_string(),
last_update: account.timestamp(),
security_symbol: account.security().symbol()
}
}
}
impl AccountService<'_> {
pub fn new(conn: & Connection) -> AccountService{
AccountService{conn}
}
pub fn get_user_accounts(& self, user_id: String) -> CustomResult<Vec<AccountDTO>>{
let p = UserId::from_string(&user_id);
if p.is_none() {
let msg = format!("{} is an invalid user id.", &user_id);
return Err(Error::UserInputError(msg));
}
let ret = list_user_accounts(&self.conn, &p.unwrap())?;
Ok(ret.iter().map(AccountDTO::from_entity).collect())
}
}
+2
View File
@@ -1,10 +1,12 @@
pub use crate::services::_authentication::SessionService; pub use crate::services::_authentication::SessionService;
pub use crate::services::_securities::SecurityService; pub use crate::services::_securities::SecurityService;
pub use crate::services::_users::UserService; pub use crate::services::_users::UserService;
pub use crate::services::_accounts::AccountService;
mod _authentication; mod _authentication;
mod _securities; mod _securities;
mod _transactions; mod _transactions;
mod _users; mod _users;
mod _accounts;
pub use crate::db::ledger_internal::LedgerInternal; pub use crate::db::ledger_internal::LedgerInternal;
+11 -1
View File
@@ -1,5 +1,6 @@
use std::io; use std::fmt::{Display, Formatter, Pointer};
use std::path::PathBuf; use std::path::PathBuf;
use std::{fmt, io};
use rusqlite::{Connection, OpenFlags}; use rusqlite::{Connection, OpenFlags};
@@ -16,6 +17,15 @@ pub enum Error {
Testing(String), Testing(String),
} }
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Error::SqlError(e) => write!(f, "SqlError: {:?}", e),
_ => write!(f, "Unknown error.. fuck mf"),
}
}
}
pub type CustomResult<T, E = Error> = Result<T, E>; pub type CustomResult<T, E = Error> = Result<T, E>;
#[derive(Clone)] #[derive(Clone)]
+9 -1
View File
@@ -1,4 +1,4 @@
use crate::services::{SecurityService, SessionService, UserService}; use crate::services::{SecurityService, SessionService, UserService, AccountService};
use actix_web::{error, Result}; use actix_web::{error, Result};
use r2d2::PooledConnection; use r2d2::PooledConnection;
@@ -55,4 +55,12 @@ impl ServicesFactory {
) -> Result<SecurityService> { ) -> Result<SecurityService> {
Ok(SecurityService::new(conn)) Ok(SecurityService::new(conn))
} }
pub fn get_account_service<'a>(
&'a self,
conn: &'a PooledConnection<SqliteConnectionManager>,
)-> Result<AccountService> {
Ok(AccountService::new(conn))
}
} }
+11 -2
View File
@@ -5,6 +5,7 @@ use actix_web_lab::respond::Html;
use log::info; use log::info;
use minijinja::context; use minijinja::context;
use rusqlite::params;
use serde::Deserialize; use serde::Deserialize;
use uuid::Uuid; use uuid::Uuid;
@@ -19,8 +20,16 @@ pub async fn actions(tmpl_env: MiniJinjaRenderer) -> actix_web::Result<impl Resp
} }
#[actix_web::get("/accounts")] #[actix_web::get("/accounts")]
pub async fn accounts(tmpl_env: MiniJinjaRenderer) -> actix_web::Result<impl Responder> { pub async fn accounts(tmpl_env: MiniJinjaRenderer,
tmpl_env.render("accounts.html", ()) req: HttpRequest,
service_factory: web::Data<ServicesFactory>) -> actix_web::Result<impl Responder> {
let conn = service_factory.get_connection()?;
let auth_service = service_factory.get_authentication_service()?;
let user = auth_service.get_login(&req);
let account_service = service_factory.get_account_service(& conn)?;
let accounts = account_service.get_user_accounts(user.unwrap())
.map_err(actix_web::error::ErrorInternalServerError)?;
tmpl_env.render("accounts.html", context!{accounts => accounts})
} }
#[actix_web::get("/overview")] #[actix_web::get("/overview")]