From e108814108c4e584ee377fa90ab2f26be82d6bba Mon Sep 17 00:00:00 2001 From: Kevin Hoerr Date: Tue, 21 Apr 2026 15:11:24 -0400 Subject: ledger.rs: Add unit tests, fix no-events case --- src/app/ledger.rs | 121 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 93 insertions(+), 28 deletions(-) (limited to 'src') diff --git a/src/app/ledger.rs b/src/app/ledger.rs index d20a4fd..7829d0b 100644 --- a/src/app/ledger.rs +++ b/src/app/ledger.rs @@ -6,13 +6,21 @@ type DateTime = chrono::DateTime; /// An estimated ledger is the effective sum of all the equity change events /// derived from record for a particular fiduciary account and equity symbol. pub struct EstimatedLedger<'a> { + /// The reference to the fiduciary account that owns the events of the ledger pub fiduciary_account: &'a FiduciaryAccount, + /// The reference to the equity symbol that all events describe pub equity_symbol: &'a EquitySymbol, + /// The list of references to events that compose the ledger pub equity_change_events: Vec<&'a EquityChangeEvent>, + /// The quantity of shares derived from all events for the account and symbol pub account_symbol_quantity: BigDecimal, + /// The estimated price of all events in the ledger derived from the cost basis and quantity pub account_symbol_cost_basis_price: BigDecimal, + /// The basis of all purchases and sales not derived from quantity pub account_symbol_cost_basis_total: BigDecimal, + /// The estimated price of the equity from the latest event in the ledger pub account_symbol_last_effective_price: BigDecimal, + /// The event timestmap of the latest event in the ledger pub account_symbol_last_update_timestamp: DateTime, } @@ -43,17 +51,17 @@ impl EstimatedLedger<'_> { acsym_events.push(event); } - let cost_basis_price = (cost_basis_total.clone() / quantity.clone()).with_scale_round(2, ROUNDING_MODE); + let cost_basis_price = if quantity.is_zero() { BigDecimal::zero() } else { cost_basis_total.clone() / quantity.clone() }; EstimatedLedger { fiduciary_account, equity_symbol, equity_change_events: acsym_events, account_symbol_quantity: quantity, - account_symbol_cost_basis_price: cost_basis_price, + account_symbol_cost_basis_price: cost_basis_price.with_scale_round(2, ROUNDING_MODE), account_symbol_cost_basis_total: cost_basis_total.with_scale_round(2, ROUNDING_MODE), account_symbol_last_effective_price: last_event.map_or_else(|| BigDecimal::zero().with_scale(2), |ev| ev.estimated_price()), - account_symbol_last_update_timestamp: last_event.map_or_else(|| chrono::DateTime::::MIN_UTC, |ev| ev.equity_change_timestamp), + account_symbol_last_update_timestamp: last_event.map_or_else(|| DateTime::MIN_UTC, |ev| ev.equity_change_timestamp), } } @@ -61,61 +69,118 @@ impl EstimatedLedger<'_> { mod test { - use std::str::FromStr; - - use crate::app::store::records::{ChangeEventType, EquityType}; + use crate::app::store::records::*; use super::*; + /// simple generic to provide auto-incrementing value for the ID type that's needed + fn next + Copy>(id: &mut T) -> T { + let tmp = *id; + *id += T::from(true); + tmp + } + + fn dec(n: u32, scale: i64) -> BigDecimal { + BigDecimal::from_biguint(bigdecimal::num_bigint::BigUint::from(n), scale) + } + #[test] - fn compile_change_events() { + fn compile_change_events_small() { let account = FiduciaryAccount { fiduciary_account_id: 0, fiduciary_account_number: 12345678, fiduciary_account_name: "account".into(), fiduciary_account_description: "Brokerage Acct".into(), - fiduciary_account_created_timestamp: DateTime::from_str("2025-10-25 13:41:00.000Z").unwrap() + fiduciary_account_created_timestamp: DateTime::from_timestamp_secs(1761399660).unwrap(), }; let symbol = EquitySymbol { equity_symbol_id: 0, equity_symbol_name: "Business Company".into(), equity_symbol_managing_company: "Business Company".into(), equity_symbol_type: EquityType::STOCK, - equity_symbol_created_timestamp: DateTime::from_str("2026-01-02 09:43:10.000Z").unwrap(), + equity_symbol_created_timestamp: DateTime::from_timestamp_secs(1767346990).unwrap(), }; + let evid = &mut 0_usize; let events = vec![ EquityChangeEvent { - equity_change_event_id: 0, - fiduciary_account_id: 0, - equity_symbol_id: 0, - equity_change_quantity: BigDecimal::from_str("0.125").unwrap(), - equity_change_cost_basis: BigDecimal::from_str("82.80").unwrap(), + equity_change_event_id: next(evid), + fiduciary_account_id: account.fiduciary_account_id, + equity_symbol_id: symbol.equity_symbol_id, + equity_change_quantity: dec(125_u32, 3), + equity_change_cost_basis: dec(8280_u32, 2), equity_change_type: ChangeEventType::BUY, - equity_change_timestamp: DateTime::from_str("2026-01-02 09:43:10.000Z").unwrap(), + equity_change_timestamp: DateTime::from_timestamp_secs(1767346990).unwrap(), }, - EquityChangeEvent { - equity_change_event_id: 1, - fiduciary_account_id: 0, - equity_symbol_id: 1, - equity_change_quantity: BigDecimal::from_str("100.01").unwrap(), - equity_change_cost_basis: BigDecimal::from_str("5.45").unwrap(), + EquityChangeEvent { // different symbol, same account + equity_change_event_id: next(evid), + fiduciary_account_id: account.fiduciary_account_id, + equity_symbol_id: symbol.equity_symbol_id + 1, + equity_change_quantity: dec(10001_u32, 2), + equity_change_cost_basis: dec(545_u32, 2), equity_change_type: ChangeEventType::BUY, - equity_change_timestamp: DateTime::from_str("2026-01-02 10:43:10.000Z").unwrap(), + equity_change_timestamp: DateTime::from_timestamp_secs(1767350590).unwrap(), }, EquityChangeEvent { - equity_change_event_id: 2, - fiduciary_account_id: 0, - equity_symbol_id: 0, - equity_change_quantity: BigDecimal::from_str("0.015").unwrap(), - equity_change_cost_basis: BigDecimal::from_str("82.50").unwrap(), + equity_change_event_id: next(evid), + fiduciary_account_id: account.fiduciary_account_id, + equity_symbol_id: symbol.equity_symbol_id, + equity_change_quantity: dec(15_u32, 3), + equity_change_cost_basis: dec(998_u32, 2), equity_change_type: ChangeEventType::SELL, - equity_change_timestamp: DateTime::from_str("2026-01-02 11:43:10.000Z").unwrap(), + equity_change_timestamp: DateTime::from_timestamp_secs(1767354190).unwrap(), } ]; let event_refs = vec![&events[0], &events[1], &events[2]]; let ledger = EstimatedLedger::compile_change_events(&account, &symbol, event_refs); + // Check account, symbol, events references of ledger are what expected + assert_eq!(ledger.fiduciary_account.fiduciary_account_id, account.fiduciary_account_id); + assert_eq!(ledger.equity_symbol.equity_symbol_id, symbol.equity_symbol_id); + assert_eq!(ledger.equity_change_events.len(), 2); + assert_eq!(ledger.equity_change_events[0].equity_change_event_id, events[0].equity_change_event_id); + assert_eq!(ledger.equity_change_events[1].equity_change_event_id, events[2].equity_change_event_id); + + // Check computed values + assert_eq!(ledger.account_symbol_quantity.to_plain_string(), "0.110"); + assert_eq!(ledger.account_symbol_cost_basis_total.to_plain_string(), "72.82"); + assert_eq!(ledger.account_symbol_cost_basis_price.to_plain_string(), "662.00"); + // "665.33", repeating, of course + assert_eq!(ledger.account_symbol_last_effective_price, events[2].estimated_price()); + assert_eq!(ledger.account_symbol_last_update_timestamp, events[2].equity_change_timestamp); + } + + #[test] + fn compile_change_events_empty() { + let account = FiduciaryAccount { + fiduciary_account_id: 0, + fiduciary_account_number: 12345678, + fiduciary_account_name: "account".into(), + fiduciary_account_description: "Brokerage Acct".into(), + fiduciary_account_created_timestamp: DateTime::from_timestamp_secs(1761399660).unwrap(), + }; + let symbol = EquitySymbol { + equity_symbol_id: 0, + equity_symbol_name: "US Equity ETF".into(), + equity_symbol_managing_company: "Business Company".into(), + equity_symbol_type: EquityType::ETF, + equity_symbol_created_timestamp: DateTime::from_timestamp_secs(1767346990).unwrap(), + }; + let events: Vec<&EquityChangeEvent> = vec![]; + + let ledger = EstimatedLedger::compile_change_events(&account, &symbol, events); + + // Check account, symbol, events references of ledger are what expected + assert_eq!(ledger.fiduciary_account.fiduciary_account_id, account.fiduciary_account_id); + assert_eq!(ledger.equity_symbol.equity_symbol_id, symbol.equity_symbol_id); + assert!(ledger.equity_change_events.is_empty()); + + // Check computed values + assert_eq!(ledger.account_symbol_quantity.to_plain_string(), "0"); + assert_eq!(ledger.account_symbol_cost_basis_total.to_plain_string(), "0.00"); + assert_eq!(ledger.account_symbol_cost_basis_price.to_plain_string(), "0.00"); + assert_eq!(ledger.account_symbol_last_effective_price.to_plain_string(), "0.00"); + assert_eq!(ledger.account_symbol_last_update_timestamp, DateTime::MIN_UTC); } } \ No newline at end of file -- cgit v1.3