use rusqlite::{params, Connection, OptionalExtension, Result, Row, ToSql, Transaction}; use serde::Serialize; use crate::db::_external_id::{Id, SecurityId}; use crate::util::{CustomResult, Error}; #[derive(Debug, Clone, Serialize, PartialEq)] pub enum SecurityType { Currency, Bond, } impl ToString for SecurityType { fn to_string(&self) -> String { match self { SecurityType::Currency => "currency".to_string(), SecurityType::Bond => "bond".to_string(), } } } impl SecurityType { fn from_string_unsafe(s: String) -> SecurityType { if s == "currency".to_string() { return SecurityType::Currency; } if s == "bond".to_string() { return SecurityType::Bond; } panic!("Impossible to convert string {} to SecurityType.", s); } pub fn from_string(s: String) -> Option { if s == "currency".to_string() { Some(SecurityType::Currency) } else if s == "bond".to_string() { Some(SecurityType::Bond) } else { None } } } impl ToSql for SecurityType { #[inline] fn to_sql(&self) -> Result> { Ok(rusqlite::types::ToSqlOutput::from(match self { SecurityType::Currency => "currency", SecurityType::Bond => "bond", })) } } #[derive(Debug, Serialize, PartialEq)] pub struct Security { id: i64, security_type: SecurityType, symbol: String, name: String, } impl Security { pub fn id(&self) -> SecurityId { SecurityId::from_id(self.id) } pub fn name(&self) -> String { self.name.clone() } pub fn symbol(&self) -> String { self.symbol.clone() } pub fn security_type(&self) -> SecurityType { self.security_type.clone() } } impl Security { pub fn from_row(row: &Row) -> Result { Ok(Security { id: row.get("sec_id")?, security_type: SecurityType::from_string_unsafe(row.get("sec_security_type")?), symbol: row.get("sec_symbol")?, name: row.get("sec_name")?, }) } } impl Clone for Security { fn clone(&self) -> Self { Security { id: self.id, security_type: self.security_type.clone(), symbol: self.symbol.clone(), name: self.name.clone(), } } } pub struct NewSecurity { pub security_type: SecurityType, pub symbol: String, pub name: String, } struct InterestRate { rate: f64, start_date: i64, end_date: Option, } impl InterestRate { pub fn from_row(row: &Row) -> Result { Ok(InterestRate { rate: row.get("rate")?, start_date: row.get("start_date")?, end_date: row.get("end_date")?, }) } } fn set_initial_interest_rate(tx: &Connection, sec_id: &SecurityId) -> CustomResult<()> { let q = " INSERT INTO interest_rates( security_id, security_rate_id, start_date, end_date, rate ) SELECT id, 0, CURRENT_TIMESTAMP, NULL, 1.0 FROM securities WHERE id = ?1 AND securities.security_type = ?2 "; tx.execute(q, params![sec_id, SecurityType::Currency]).map_err(Error::SqlError)?; Ok(()) } fn set_interest_rate(tx: &Connection, sec_id: &SecurityId, rate: f64) -> CustomResult<()> { let q = " INSERT INTO interest_rates( security_id, security_rate_id, start_date, end_date, rate ) SELECT ?1 AS security_id, MAX(interest_rates.security_rate_id) + 1 AS security_rate_id, CURRENT_TIMESTAMP AS start_date, NULL AS end_date, ?2 AS rate FROM interest_rates JOIN securities ON interest_rates.security_id = securities.id WHERE interest_rates.security_id = ?1 AND securities.security_type = ?3 "; tx.execute(q, params![sec_id, rate, SecurityType::Currency]) .map_err(Error::SqlError)?; Ok(()) } fn get_rates(tx: &Connection, sec_id: &SecurityId) -> CustomResult> { let q = " SELECT rate, UNIXEPOCH(start_date) AS start_date, UNIXEPOCH(end_date) AS end_date FROM interest_rates WHERE security_id = ?1 ORDER BY security_rate_id ASC"; let mut stmt = tx.prepare(q).map_err(Error::SqlError)?; let ret: Vec = stmt .query_map(params![sec_id], InterestRate::from_row) .map_err(Error::SqlError)? .filter(Result::is_ok) .map(Result::unwrap) .collect(); return Ok(ret); } pub fn create_security(tx: &Connection, sec: &NewSecurity) -> CustomResult { let q = "INSERT INTO securities(name, symbol, security_type) VALUES(?1, ?2, ?3)"; let params = (&sec.name, &sec.symbol, &sec.security_type); tx.execute(q, params).map_err(Error::SqlError)?; let new_sec_id = &SecurityId::from_id(tx.last_insert_rowid()); if sec.security_type == SecurityType::Currency { set_initial_interest_rate(tx, new_sec_id)?; } let ret = get_security_by_id(tx, new_sec_id)?; Ok(ret.unwrap()) } pub fn get_security_by_id(tx: &Connection, id: &SecurityId) -> CustomResult> { let q = "SELECT id AS sec_id, security_type AS sec_security_type, symbol AS sec_symbol, name AS sec_name FROM securities WHERE id = ?1"; tx.query_row(q, params![id], Security::from_row) .optional() .map_err(Error::SqlError) } pub fn list_securities(conn: &Connection) -> CustomResult> { let q = "SELECT id AS sec_id, security_type AS sec_security_type, symbol AS sec_symbol, name AS sec_name FROM securities ORDER BY id ASC"; let mut stmt = conn.prepare(q).map_err(Error::SqlError)?; let ret: Vec = stmt .query_map([], Security::from_row) .map_err(Error::SqlError)? .filter(Result::is_ok) .map(Result::unwrap) .collect(); return Ok(ret); } #[cfg(test)] mod test_securities { use rusqlite::{params, Connection}; use crate::db::_test_utils::*; use crate::db::users::User; use crate::migration; use super::*; fn setup() -> (Connection, Vec) { let mut conn = Connection::open_in_memory().unwrap(); migration::apply_migrations(&mut conn).unwrap(); let tx = conn.transaction().unwrap(); let users = setup_users(&tx).unwrap(); tx.commit().unwrap(); return (conn, users); } #[test] fn test_security_types() -> Result<(), rusqlite::Error> { let (conn, _users) = setup(); let q = "CREATE TABLE test_security_types( name TEXT, FOREIGN KEY (name) REFERENCES security_types (name) )"; conn.execute(q, ())?; let security_types = vec![SecurityType::Currency, SecurityType::Bond]; for sec in security_types.iter() { let q = "INSERT INTO test_security_types(name) VALUES (?1)"; conn.execute(q, params![sec])?; } return Ok(()); } #[test] fn test_create_security_currency() -> CustomResult<()> { let (mut conn, _users) = setup(); let new_sec = mock_currency_1(); let sec = create_security(&conn, &new_sec)?; assert_eq!(sec.symbol, new_sec.symbol); assert_eq!(sec.security_type, SecurityType::Currency); let interest_rate = get_rates(&conn, &sec.id())?; assert_eq!(interest_rate.len(), 1); Ok(()) } #[test] fn test_create_security_bond() -> CustomResult<()> { let (mut conn, _users) = setup(); let new_sec = mock_currency_2(); let sec = create_security(&conn, &new_sec)?; assert_eq!(sec.symbol, new_sec.symbol); assert_eq!(sec.security_type, SecurityType::Bond); let interest_rate = get_rates(&conn, &sec.id())?; assert_eq!(interest_rate.len(), 0); Ok(()) } #[test] fn test_set_interest_rate() -> CustomResult<()> { let (mut conn, _users) = setup(); let new_sec = mock_currency_1(); let sec = &create_security(&conn, &new_sec)?.id(); set_interest_rate(&conn, sec, 90.00)?; let interest_rate = get_rates(&conn, sec)?; assert_eq!(interest_rate.len(), 2); Ok(()) } #[test] fn test_list_security() -> CustomResult<()> { let (mut conn, _users) = setup(); assert_eq!(0, list_securities(&conn)?.len()); let sec = mock_currency_1(); let tx = conn.transaction().unwrap(); create_security(&tx, &sec)?; tx.commit().unwrap(); let ret = list_securities(&conn)?; let expt = Security { id: 1, security_type: SecurityType::Currency, symbol: "P".to_string(), name: "piastre".to_string(), }; assert_eq!(1, ret.len()); assert_eq!(&expt, ret.get(0).unwrap()); let sec = mock_currency_2(); let expt = Security { id: 2, security_type: SecurityType::Bond, symbol: "B".to_string(), name: "bong".to_string(), }; let tx = conn.transaction().unwrap(); create_security(&tx, &sec)?; tx.commit().unwrap(); let ret = list_securities(&conn)?; assert_eq!(2, ret.len()); assert_eq!(&expt, ret.get(1).unwrap()); return Ok(()); } }