open-standards/openpay/crates/op-ledger/src/balance.rs
Transaction Science 70ecd6caa0 Initial import — three open standards under one shelf
Transaction Science open standards monorepo. Each subdirectory is a
self-contained reference protocol with its own README and licence.

- openpay/    Apache-2.0  payment-acceptance stack (Rust, 26 crates)
- smart-byte/ CC-BY-4.0   content-addressed value carrier (spec)
- eoc/        CC-BY-4.0   energy-optimised compute (spec)

The wire format and the right to fork are public. The optional hosted
services are the offer; the protocols are not.
2026-05-23 07:58:09 -04:00

91 lines
2.8 KiB
Rust

//! Balance — what an account holds at a point in time.
//!
//! Three balance views per Modern Treasury convention:
//!
//! - **Posted** — sum of posted transactions only. The settled state.
//! - **Pending** — sum of posted AND pending transactions. The
//! "balance after everything in flight settles" projection.
//! - **Available** — context-dependent; for OpenPay's reference
//! model it equals `posted` (we don't model holds beyond pending).
//!
//! Balances are derived (never stored). They're computed by walking
//! the ledger's entries; the [`LedgerStore`](crate::LedgerStore)
//! does the iteration.
//!
//! ## Why a struct instead of `(i64, i64)`?
//!
//! Type-safety. `Balance::pending` and `Balance::posted` are clearly
//! labeled; you can't accidentally pass posted where pending is
//! expected. The struct also carries the currency so the caller
//! doesn't have to refetch the account to format the amount.
use op_core::{Currency, Money};
/// Balance for a single account at a point in time.
///
/// All three views carry the account's currency. They're always in
/// the SAME currency (one account → one currency, fixed at
/// creation).
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Balance {
/// Currency. Always matches the account's currency.
pub currency: Currency,
/// Sum of posted-transaction entries, signed by the account's
/// normal balance so it's natural-positive for a healthy account.
pub posted: Money,
/// Sum of posted-and-pending entries, signed the same way.
pub pending: Money,
}
impl Balance {
/// Construct a zero balance in the given currency.
#[must_use]
pub fn zero(currency: Currency) -> Self {
Self {
currency,
posted: Money::from_minor(0, currency),
pending: Money::from_minor(0, currency),
}
}
/// True if both views are zero.
#[must_use]
pub fn is_zero(&self) -> bool {
self.posted.is_zero() && self.pending.is_zero()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn zero_constructs_zero_money_for_each_view() {
let b = Balance::zero(Currency::USD);
assert!(b.is_zero());
assert_eq!(b.posted.minor_units, 0);
assert_eq!(b.pending.minor_units, 0);
assert_eq!(b.currency, Currency::USD);
}
#[test]
fn non_zero_pending_is_not_zero() {
let b = Balance {
currency: Currency::USD,
posted: Money::from_minor(0, Currency::USD),
pending: Money::from_minor(100, Currency::USD),
};
assert!(!b.is_zero());
}
#[test]
fn equality_requires_all_three_fields() {
let a = Balance::zero(Currency::USD);
let b = Balance::zero(Currency::USD);
assert_eq!(a, b);
let c = Balance::zero(Currency::EUR);
assert_ne!(a, c);
}
}