diff --git a/resources/migrations/sql/create_tables_001.sql b/resources/migrations/sql/create_tables_001.sql index 3ad3b52..bba12ff 100644 --- a/resources/migrations/sql/create_tables_001.sql +++ b/resources/migrations/sql/create_tables_001.sql @@ -2,7 +2,6 @@ PRAGMA journal_mode= WAL; CREATE TABLE IF NOT EXISTS ledger_internal ( - -- th id INTEGER PRIMARY KEY AUTOINCREMENT, creation_time DATETIME DEFAULT CURRENT_TIMESTAMP, cookie_secret TEXT NOT NULL @@ -38,6 +37,20 @@ CREATE TABLE IF NOT EXISTS securities FOREIGN KEY (security_type) REFERENCES security_types (name) ); +CREATE TABLE IF NOT EXISTS interest_rates +( + id INTEGER PRIMARY KEY AUTOINCREMENT, + security_id INTEGER NOT NULL, + security_rate_id INTEGER NOT NULL, + start_date DATETIME NOT NULL, + end_date DATETIME, + rate REAL NOT NULL, + FOREIGN KEY (security_id) REFERENCES securities(id), + CONSTRAINT unique_security_rate_id UNIQUE (security_id, security_rate_id), + CHECK (end_date IS NULL OR start_date <= end_date), + CHECK ( rate >= 0 ) +); + CREATE TABLE IF NOT EXISTS account_types ( name TEXT PRIMARY KEY @@ -60,7 +73,9 @@ CREATE TABLE IF NOT EXISTS accounts creation_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (security) REFERENCES securities (id), - FOREIGN KEY (account_type) REFERENCES account_types (name) + FOREIGN KEY (account_type) REFERENCES account_types (name), + CHECK (amount >= 0 OR (account_type = 'security_emitter')) + ); CREATE TABLE IF NOT EXISTS accounts_history diff --git a/src/db/_accounts.rs b/src/db/_accounts.rs index 7786d37..9269fdc 100644 --- a/src/db/_accounts.rs +++ b/src/db/_accounts.rs @@ -373,6 +373,13 @@ fn update_account_history(tx: &Connection, account_id: &AccountId) -> CustomResu Ok(()) } +/// Super low level functions that needs to be wrapped in something else. +/// +/// This method does NOT guarantee that all amount added into an account +/// comes from another account. +/// +/// This method does NOT guarantee that checking account cannot go in the +/// negative. pub fn add_to_account( tx: &Connection, account_id: &AccountId, @@ -411,6 +418,10 @@ pub fn get_number_of_accounts(tx: &Connection, tx.query_row(q, params, |row| row.get("count")).map_err(Error::SqlError) } +/// Market cap is computer by summing the security emitter accounts. +/// We use this technique because the sum of the security +/// emitter accounts MUST equals (more or less very tiny interest +/// amounts) the sum of the checking accounts. pub fn get_security_market_cap(tx: &Connection, sec: &SecurityId) -> CustomResult { let q = " diff --git a/src/db/_interests.rs b/src/db/_interests.rs index 8b13789..e948fad 100644 --- a/src/db/_interests.rs +++ b/src/db/_interests.rs @@ -1 +1,30 @@ +const _one_massive_fucking_query : &str = " +WITH RECURSIVE + effective_interest_rates + AS (SELECT ROW_NUMBER() OVER () AS id, + interest_rates.id AS shit_id, + max(13, start_date) AS start_date, + min(coalesce(end_date, 20), 20) AS end_date, + min(coalesce(end_date, 20), 20) - max(13, start_date) AS duration, + interest_rate + FROM interest_rates + WHERE ((start_date >= 13) AND NOT (start_date > 20)) + OR ((start_date < 13) AND (end_date > 13 OR end_date IS NULL)) + ORDER BY start_date ASC), + compound_value(compound_value_id, value, interest_rate, duration) + AS (SELECT 0, 100, 0, 0 -- the 100 is the initial value + UNION + SELECT compound_value.compound_value_id + 1, + compound_value.value * + pow(1 + effective_interest_rates.interest_rate, effective_interest_rates.duration), + effective_interest_rates.interest_rate, + effective_interest_rates.duration + FROM effective_interest_rates + JOIN compound_value ON compound_value.compound_value_id + 1 = effective_interest_rates.id) +SELECT value +FROM compound_value +ORDER BY compound_value_id DESC +LIMIT 1 +"; + diff --git a/src/db/_securities.rs b/src/db/_securities.rs index ec5cc3a..1e13012 100644 --- a/src/db/_securities.rs +++ b/src/db/_securities.rs @@ -1,4 +1,4 @@ -use rusqlite::{params, Connection, Result, Row, ToSql, Transaction, OptionalExtension}; +use rusqlite::{params, Connection, OptionalExtension, Result, Row, ToSql, Transaction}; use serde::Serialize; use crate::db::_external_id::{Id, SecurityId}; @@ -89,7 +89,7 @@ impl Security { impl Clone for Security { fn clone(&self) -> Self { - Security{ + Security { id: self.id, security_type: self.security_type.clone(), symbol: self.symbol.clone(), @@ -104,19 +104,97 @@ pub struct NewSecurity { 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:i64 = tx.last_insert_rowid(); + let new_sec_id = &SecurityId::from_id(tx.last_insert_rowid()); - let ret = get_security_by_id(tx, &SecurityId::from_id(new_sec_id))?; + 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> { +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, @@ -126,7 +204,6 @@ pub fn get_security_by_id(tx: &Connection, id: & SecurityId) -> CustomResult CustomResult> { @@ -184,14 +261,39 @@ mod test_securities { } #[test] - fn test_create_security() -> CustomResult<()> { + fn test_create_security_currency() -> CustomResult<()> { let (mut conn, _users) = setup(); let new_sec = mock_currency_1(); - let tx = conn.transaction().unwrap(); - let sec = create_security(&tx, &new_sec)?; + let sec = create_security(&conn, &new_sec)?; assert_eq!(sec.symbol, new_sec.symbol); + assert_eq!(sec.security_type, SecurityType::Currency); - tx.commit().unwrap(); + 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(()) } diff --git a/src/services/_transactions.rs b/src/services/_transactions.rs index 54509a2..f5a15f3 100644 --- a/src/services/_transactions.rs +++ b/src/services/_transactions.rs @@ -459,6 +459,7 @@ mod test_caching_cheques { let ret = cash_cheque(&mut tx, &cheque, &payee_account); match ret { Err(Error::InvalidAmount(_)) => Ok(()), + Err(x) => Err(Error::Testing(format!("{:?}", x))), _ => Err(Error::Testing( "Expected invalid amount error here.".to_string(), )),