336 lines
9.7 KiB
Rust
336 lines
9.7 KiB
Rust
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<SecurityType> {
|
|
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<rusqlite::types::ToSqlOutput<'_>> {
|
|
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<Security> {
|
|
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<i64>,
|
|
}
|
|
|
|
impl InterestRate {
|
|
pub fn from_row(row: &Row) -> Result<InterestRate> {
|
|
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<Vec<InterestRate>> {
|
|
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<InterestRate> = 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<Security> {
|
|
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<Option<Security>> {
|
|
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<Vec<Security>> {
|
|
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<Security> = 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<User>) {
|
|
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(());
|
|
}
|
|
}
|