diff --git a/Cargo.lock b/Cargo.lock index 201605ee..fea77769 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -135,15 +135,30 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec 0.8.0", +] + [[package]] name = "bit-set" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "34ddef2995421ab6a5c779542c81ee77c115206f4ad9d5a8e05f4ff49716a3dd" dependencies = [ - "bit-vec", + "bit-vec 0.9.1", ] +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + [[package]] name = "bit-vec" version = "0.9.1" @@ -332,6 +347,7 @@ dependencies = [ "job", "obix", "opentelemetry", + "proptest", "rand 0.10.1", "regex", "rust_decimal", @@ -845,6 +861,16 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "es-entity" version = "0.10.35" @@ -907,6 +933,12 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "fastrand" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6" + [[package]] name = "find-msvc-tools" version = "0.1.9" @@ -1580,7 +1612,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "98a80a963123205c7157323c99611bc4abb65dcbd62ef46dc4bac74a3941bc75" dependencies = [ "ascii-canvas", - "bit-set", + "bit-set 0.9.1", "ena", "itertools 0.14.0", "lalrpop-util", @@ -1652,6 +1684,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" + [[package]] name = "litemap" version = "0.8.1" @@ -2139,6 +2177,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "proptest" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b45fcc2344c680f5025fe57779faef368840d0bd1f42f216291f0dc4ace4744" +dependencies = [ + "bit-set 0.8.0", + "bit-vec 0.8.0", + "bitflags", + "num-traits", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax", + "rusty-fork", + "tempfile", + "unarray", +] + [[package]] name = "prost" version = "0.14.3" @@ -2182,6 +2239,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + [[package]] name = "quote" version = "1.0.44" @@ -2279,6 +2342,15 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c8d0fd677905edcbeedbf2edb6494d676f0e98d54d5cf9bda0b061cb8fb8aba" +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.5", +] + [[package]] name = "rand_xoshiro" version = "0.6.0" @@ -2506,6 +2578,19 @@ dependencies = [ "syn 2.0.117", ] +[[package]] +name = "rustix" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + [[package]] name = "rustls" version = "0.23.36" @@ -2546,6 +2631,18 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "rusty-fork" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc6bf79ff24e648f6da1f8d1f011e9cac26491b619e6b9280f2b47f1774e6ee2" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + [[package]] name = "rusty-money" version = "0.5.0" @@ -3159,6 +3256,19 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "tempfile" +version = "3.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" +dependencies = [ + "fastrand", + "getrandom 0.4.1", + "once_cell", + "rustix", + "windows-sys 0.61.2", +] + [[package]] name = "term" version = "1.2.1" @@ -3545,6 +3655,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + [[package]] name = "unicode-bidi" version = "0.3.18" @@ -3638,6 +3754,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 8c7181e8..bb80b5a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ rust_decimal = "1.41" rusty-money = { version = "0.5", features = ["iso", "crypto"] } schemars = { version = "1.0", features = ["uuid1"] } rand = "0.10" +proptest = "1.5" [profile.release] lto = true diff --git a/cala-ledger/Cargo.toml b/cala-ledger/Cargo.toml index d5b7cde4..ab8bf322 100644 --- a/cala-ledger/Cargo.toml +++ b/cala-ledger/Cargo.toml @@ -50,5 +50,6 @@ anyhow = { workspace = true } rand = { workspace = true } tokio-test = "0.4" rust_decimal_macros = { workspace = true } +proptest = { workspace = true } [build-dependencies] diff --git a/cala-ledger/src/balance/account_balance.rs b/cala-ledger/src/balance/account_balance.rs index fd5971c0..79fb1005 100644 --- a/cala-ledger/src/balance/account_balance.rs +++ b/cala-ledger/src/balance/account_balance.rs @@ -166,3 +166,209 @@ impl BalanceRange { } } } + +#[cfg(test)] +mod proptests { + //! Property-based tests for `BalanceWithDirection` signed-balance math + //! and `AccountBalance::derive_diff`. + //! + //! Properties: + //! - Signed-balance direction: for a Credit account, `settled() == cr - dr`; + //! for a Debit account, `settled() == dr - cr`. Sign flips on direction. + //! Same for pending() and encumbrance(). + //! - `available(Settled)` always equals `settled()`. + //! - `available(Pending) == pending() + settled()`. + //! - `available(Encumbrance) == encumbrance() + pending() + settled()`. + //! - `derive_diff(zero_baseline) == self` on all four amount fields. + //! - `derive_diff(self) == zero` on all four amount fields. + + use chrono::TimeZone; + use proptest::prelude::*; + use rust_decimal::Decimal; + use uuid::Uuid; + + use cala_types::balance::{BalanceAmount, BalanceSnapshot}; + + use super::*; + + fn arb_amount_pair() -> impl Strategy { + let v = 0u64..=1_000_000_000_000u64; + (v.clone(), v).prop_map(|(d, c)| (Decimal::from(d), Decimal::from(c))) + } + + fn make_balance_amount(dr: Decimal, cr: Decimal) -> BalanceAmount { + BalanceAmount { + dr_balance: dr, + cr_balance: cr, + entry_id: EntryId::from(Uuid::nil()), + modified_at: chrono::Utc.timestamp_opt(0, 0).single().unwrap(), + } + } + + fn make_snapshot( + settled: (Decimal, Decimal), + pending: (Decimal, Decimal), + encumbrance: (Decimal, Decimal), + ) -> BalanceSnapshot { + let t = chrono::Utc.timestamp_opt(0, 0).single().unwrap(); + BalanceSnapshot { + journal_id: cala_types::primitives::JournalId::from(Uuid::nil()), + account_id: AccountId::from(Uuid::nil()), + currency: cala_types::primitives::Currency::USD, + version: 1, + created_at: t, + modified_at: t, + entry_id: EntryId::from(Uuid::nil()), + settled: make_balance_amount(settled.0, settled.1), + pending: make_balance_amount(pending.0, pending.1), + encumbrance: make_balance_amount(encumbrance.0, encumbrance.1), + } + } + + fn arb_direction() -> impl Strategy { + prop_oneof![Just(DebitOrCredit::Debit), Just(DebitOrCredit::Credit)] + } + + fn arb_layer() -> impl Strategy { + prop_oneof![ + Just(Layer::Settled), + Just(Layer::Pending), + Just(Layer::Encumbrance), + ] + } + + proptest! { + #![proptest_config(ProptestConfig { cases: 4096, ..ProptestConfig::default() })] + + /// `settled()` flips sign with the account direction. + /// Credit account: `cr - dr`; Debit account: `dr - cr`. + #[test] + fn settled_direction_dependent( + settled in arb_amount_pair(), + pending in arb_amount_pair(), + encumbrance in arb_amount_pair(), + ) { + let snap = make_snapshot(settled, pending, encumbrance); + + let credit_view = BalanceWithDirection::new(DebitOrCredit::Credit, &snap); + let debit_view = BalanceWithDirection::new(DebitOrCredit::Debit, &snap); + + prop_assert_eq!(credit_view.settled(), settled.1 - settled.0); + prop_assert_eq!(debit_view.settled(), settled.0 - settled.1); + + // Sign flip: credit_view.settled() + debit_view.settled() == 0 + prop_assert_eq!(credit_view.settled() + debit_view.settled(), Decimal::ZERO); + } + + /// `pending()` and `encumbrance()` follow the same direction-flip rule. + #[test] + fn pending_and_encumbrance_direction_dependent( + settled in arb_amount_pair(), + pending in arb_amount_pair(), + encumbrance in arb_amount_pair(), + dir in arb_direction(), + ) { + let snap = make_snapshot(settled, pending, encumbrance); + let view = BalanceWithDirection::new(dir, &snap); + + let expected_pending = match dir { + DebitOrCredit::Credit => pending.1 - pending.0, + DebitOrCredit::Debit => pending.0 - pending.1, + }; + let expected_enc = match dir { + DebitOrCredit::Credit => encumbrance.1 - encumbrance.0, + DebitOrCredit::Debit => encumbrance.0 - encumbrance.1, + }; + prop_assert_eq!(view.pending(), expected_pending); + prop_assert_eq!(view.encumbrance(), expected_enc); + } + + /// `available(Settled) == settled()`. + /// `available(Pending) == pending() + settled()`. + /// `available(Encumbrance) == encumbrance() + pending() + settled()`. + #[test] + fn available_layered_composition( + settled in arb_amount_pair(), + pending in arb_amount_pair(), + encumbrance in arb_amount_pair(), + dir in arb_direction(), + ) { + let snap = make_snapshot(settled, pending, encumbrance); + let view = BalanceWithDirection::new(dir, &snap); + + prop_assert_eq!(view.available(Layer::Settled), view.settled()); + prop_assert_eq!(view.available(Layer::Pending), view.pending() + view.settled()); + prop_assert_eq!( + view.available(Layer::Encumbrance), + view.encumbrance() + view.pending() + view.settled() + ); + } + + /// `AccountBalance::available(layer)` agrees with `BalanceWithDirection::available`. + #[test] + fn account_balance_available_matches_view( + settled in arb_amount_pair(), + pending in arb_amount_pair(), + encumbrance in arb_amount_pair(), + dir in arb_direction(), + layer in arb_layer(), + ) { + let snap = make_snapshot(settled, pending, encumbrance); + let ab = AccountBalance::new(dir, snap.clone()); + let view = BalanceWithDirection::new(dir, &snap); + prop_assert_eq!(ab.available(layer), view.available(layer)); + } + + /// `derive_diff(zero_baseline) == self` on the four amount fields. + #[test] + fn derive_diff_with_zero_is_identity( + settled in arb_amount_pair(), + pending in arb_amount_pair(), + encumbrance in arb_amount_pair(), + dir in arb_direction(), + ) { + let snap = make_snapshot(settled, pending, encumbrance); + let zero = make_snapshot( + (Decimal::ZERO, Decimal::ZERO), + (Decimal::ZERO, Decimal::ZERO), + (Decimal::ZERO, Decimal::ZERO), + ); + + let result = AccountBalance::new(dir, snap.clone()) + .derive_diff(&AccountBalance::new(dir, zero)); + + prop_assert_eq!(result.details.settled.dr_balance, snap.settled.dr_balance); + prop_assert_eq!(result.details.settled.cr_balance, snap.settled.cr_balance); + prop_assert_eq!(result.details.pending.dr_balance, snap.pending.dr_balance); + prop_assert_eq!(result.details.pending.cr_balance, snap.pending.cr_balance); + prop_assert_eq!( + result.details.encumbrance.dr_balance, + snap.encumbrance.dr_balance + ); + prop_assert_eq!( + result.details.encumbrance.cr_balance, + snap.encumbrance.cr_balance + ); + } + + /// `derive_diff(self)` produces zero balances on all four amount fields. + #[test] + fn derive_diff_with_self_is_zero( + settled in arb_amount_pair(), + pending in arb_amount_pair(), + encumbrance in arb_amount_pair(), + dir in arb_direction(), + ) { + let snap = make_snapshot(settled, pending, encumbrance); + let result = AccountBalance::new(dir, snap.clone()) + .derive_diff(&AccountBalance::new(dir, snap)); + + prop_assert_eq!(result.details.settled.dr_balance, Decimal::ZERO); + prop_assert_eq!(result.details.settled.cr_balance, Decimal::ZERO); + prop_assert_eq!(result.details.pending.dr_balance, Decimal::ZERO); + prop_assert_eq!(result.details.pending.cr_balance, Decimal::ZERO); + prop_assert_eq!(result.details.encumbrance.dr_balance, Decimal::ZERO); + prop_assert_eq!(result.details.encumbrance.cr_balance, Decimal::ZERO); + } + } +} diff --git a/cala-ledger/src/balance/snapshot.rs b/cala-ledger/src/balance/snapshot.rs index bfed0e94..4eca072f 100644 --- a/cala-ledger/src/balance/snapshot.rs +++ b/cala-ledger/src/balance/snapshot.rs @@ -102,3 +102,244 @@ impl Snapshots { snapshot } } + +#[cfg(test)] +mod proptests { + //! Property-based tests for `Snapshots::new_snapshot` and `update_snapshot`. + //! + //! Properties: + //! - Layer/direction dispatch: an entry only mutates the (layer, direction) + //! cell it targets. The other 5 of 6 cells are untouched. + //! - Version monotonicity: each `update_snapshot` increments version by 1. + //! - Sum-of-units: applying N entries to one (layer, direction) cell makes + //! that cell equal the sum of their units. + //! - Order independence: applying any permutation of the same multiset of + //! entries yields the same balance amounts (debits == debits, credits == + //! credits regardless of order). Foundation of the double-entry contract. + //! - `BalanceSnapshot::available` rollup composition: settled is exactly + //! `available(Settled)`; pending rollup adds pending+settled; encumbrance + //! rollup adds all three. + + use proptest::prelude::*; + use rust_decimal::Decimal; + use uuid::Uuid; + + use cala_types::{ + balance::BalanceSnapshot, + entry::EntryValues, + primitives::{Currency, DebitOrCredit, JournalId, Layer, TransactionId}, + }; + + use super::*; + + fn arb_layer() -> impl Strategy { + prop_oneof![Just(Layer::Settled), Just(Layer::Pending), Just(Layer::Encumbrance)] + } + + fn arb_direction() -> impl Strategy { + prop_oneof![Just(DebitOrCredit::Debit), Just(DebitOrCredit::Credit)] + } + + /// Units in cents-ish range; positive only. Decimal::from_i64 covers up to + /// ~9e18 which is well within rust_decimal's representable range when + /// summed across a small number of entries. + fn arb_units() -> impl Strategy { + (0u64..=1_000_000_000_000u64).prop_map(Decimal::from) + } + + fn entry(layer: Layer, direction: DebitOrCredit, units: Decimal) -> EntryValues { + EntryValues { + id: EntryId::from(Uuid::nil()), + version: 1, + transaction_id: TransactionId::from(Uuid::nil()), + journal_id: JournalId::from(Uuid::nil()), + account_id: AccountId::from(Uuid::nil()), + entry_type: "TEST".to_string(), + sequence: 1, + layer, + units, + currency: Currency::USD, + direction, + description: None, + metadata: None, + } + } + + fn time(seconds: i64) -> chrono::DateTime { + use chrono::TimeZone; + chrono::Utc.timestamp_opt(1_700_000_000 + seconds, 0).unwrap() + } + + /// All six (layer, direction) cells in a snapshot, returned as `(layer, direction, value)` + /// for compact comparison. + fn cells(s: &BalanceSnapshot) -> Vec<(Layer, DebitOrCredit, Decimal)> { + vec![ + (Layer::Settled, DebitOrCredit::Debit, s.settled.dr_balance), + (Layer::Settled, DebitOrCredit::Credit, s.settled.cr_balance), + (Layer::Pending, DebitOrCredit::Debit, s.pending.dr_balance), + (Layer::Pending, DebitOrCredit::Credit, s.pending.cr_balance), + (Layer::Encumbrance, DebitOrCredit::Debit, s.encumbrance.dr_balance), + (Layer::Encumbrance, DebitOrCredit::Credit, s.encumbrance.cr_balance), + ] + } + + proptest! { + #![proptest_config(ProptestConfig { cases: 4096, ..ProptestConfig::default() })] + + /// `new_snapshot` produces a snapshot with all-zero balance cells except + /// the one cell (layer, direction) that the entry targets. + #[test] + fn new_snapshot_only_touches_target_cell( + layer in arb_layer(), + direction in arb_direction(), + units in arb_units(), + ) { + let entry = entry(layer, direction, units); + let snap = Snapshots::new_snapshot(time(0), entry.account_id, &entry); + + for (l, d, v) in cells(&snap) { + if l == layer && d == direction { + prop_assert_eq!(v, units, "target cell should equal entry.units"); + } else { + prop_assert_eq!(v, Decimal::ZERO, "non-target cell ({:?}, {:?}) should be zero", l, d); + } + } + } + + /// `update_snapshot` increments version by exactly 1 per call. + #[test] + fn version_increments_by_one( + n in 1usize..30, + layer in arb_layer(), + direction in arb_direction(), + units in arb_units(), + ) { + let initial_entry = entry(layer, direction, units); + let mut snap = Snapshots::new_snapshot(time(0), initial_entry.account_id, &initial_entry); + prop_assert_eq!(snap.version, 1); + + for i in 1..n { + snap = Snapshots::update_snapshot(time(i as i64), snap, &initial_entry); + prop_assert_eq!(snap.version, (i + 1) as u32); + } + } + + /// Sum-of-units: applying N debit entries to a single (layer) yields + /// dr_balance == sum(units) on that layer. Cross-layer cells are zero. + #[test] + fn sum_of_units_lands_on_target_cell( + layer in arb_layer(), + direction in arb_direction(), + unit_list in proptest::collection::vec(arb_units(), 1..15), + ) { + let account = AccountId::from(Uuid::nil()); + let mut snap: Option = None; + for (i, &u) in unit_list.iter().enumerate() { + let mut e = entry(layer, direction, u); + e.account_id = account; + snap = Some(match snap { + None => Snapshots::new_snapshot(time(i as i64), account, &e), + Some(s) => Snapshots::update_snapshot(time(i as i64), s, &e), + }); + } + let snap = snap.unwrap(); + let expected_total: Decimal = unit_list.iter().sum(); + + for (l, d, v) in cells(&snap) { + if l == layer && d == direction { + prop_assert_eq!(v, expected_total); + } else { + prop_assert_eq!(v, Decimal::ZERO); + } + } + } + + /// Order independence: applying entries in any permutation yields the + /// same final balance amounts. This is the additive-commutativity + /// property that the entire double-entry contract rests on at the + /// per-account level. + #[test] + fn balance_amounts_are_order_independent( + entries_spec in proptest::collection::vec( + (arb_layer(), arb_direction(), arb_units()), + 1..10, + ), + permutation_seed in any::(), + ) { + let account = AccountId::from(Uuid::nil()); + + // Original order + let mut snap_a: Option = None; + for (i, &(l, d, u)) in entries_spec.iter().enumerate() { + let mut e = entry(l, d, u); + e.account_id = account; + snap_a = Some(match snap_a { + None => Snapshots::new_snapshot(time(i as i64), account, &e), + Some(s) => Snapshots::update_snapshot(time(i as i64), s, &e), + }); + } + + // Shuffled order via deterministic seeded RNG + use rand::seq::SliceRandom; + use rand::SeedableRng; + let mut shuffled = entries_spec.clone(); + let mut rng = rand::rngs::StdRng::seed_from_u64(permutation_seed); + shuffled.shuffle(&mut rng); + + let mut snap_b: Option = None; + for (i, &(l, d, u)) in shuffled.iter().enumerate() { + let mut e = entry(l, d, u); + e.account_id = account; + snap_b = Some(match snap_b { + None => Snapshots::new_snapshot(time(i as i64), account, &e), + Some(s) => Snapshots::update_snapshot(time(i as i64), s, &e), + }); + } + + prop_assert_eq!(cells(&snap_a.unwrap()), cells(&snap_b.unwrap())); + } + + /// `BalanceSnapshot::available` rollup composition. + /// - available(Settled) == settled + /// - available(Pending).dr - available(Settled).dr == pending.dr + /// - available(Encumbrance).dr - available(Pending).dr == encumbrance.dr + /// Same for cr_balance. + #[test] + fn available_rollup_composition( + entries_spec in proptest::collection::vec( + (arb_layer(), arb_direction(), arb_units()), + 1..10, + ), + ) { + let account = AccountId::from(Uuid::nil()); + let mut snap: Option = None; + for (i, &(l, d, u)) in entries_spec.iter().enumerate() { + let mut e = entry(l, d, u); + e.account_id = account; + snap = Some(match snap { + None => Snapshots::new_snapshot(time(i as i64), account, &e), + Some(s) => Snapshots::update_snapshot(time(i as i64), s, &e), + }); + } + let s = snap.unwrap(); + + let av_settled = s.available(Layer::Settled); + prop_assert_eq!(av_settled.dr_balance, s.settled.dr_balance); + prop_assert_eq!(av_settled.cr_balance, s.settled.cr_balance); + + let av_pending = s.available(Layer::Pending); + prop_assert_eq!(av_pending.dr_balance, s.settled.dr_balance + s.pending.dr_balance); + prop_assert_eq!(av_pending.cr_balance, s.settled.cr_balance + s.pending.cr_balance); + + let av_enc = s.available(Layer::Encumbrance); + prop_assert_eq!( + av_enc.dr_balance, + s.settled.dr_balance + s.pending.dr_balance + s.encumbrance.dr_balance + ); + prop_assert_eq!( + av_enc.cr_balance, + s.settled.cr_balance + s.pending.cr_balance + s.encumbrance.cr_balance + ); + } + } +}