Skip to main content

rustlavel_ledger/
lib.rs

1//! rustlavel-ledger: a book that balances.
2//!
3//! An application that sells credits has a number per customer that must be
4//! right — not approximately right, not right after a nightly job, right now
5//! and under load. This crate is that number and the rules around it.
6//!
7//! # Double entry
8//!
9//! Every movement of value is a [`Transfer`] between two [`Account`]s, and
10//! writes two [`Entry`] rows: a debit on one and a credit on the other, equal
11//! and opposite. Where does a top-up come *from*? A system account,
12//! `system:topup`. Where does a spent credit *go*? `system:consumption`. So
13//! the sum of every balance in the book is zero, always, and [`Ledger::audit`]
14//! can say whether it is. A single-entry design — "add 100 to the user" — has
15//! no such check, and its errors are invisible until a customer complains.
16//!
17//! # What is atomic, and how
18//!
19//! Every operation is one database transaction. The debit is
20//! `UPDATE … SET balance = balance - ? WHERE id = ? AND balance - held >= ?`,
21//! and the row count is the answer: zero means the funds were not there, and
22//! nothing else has happened yet. That guard is also the lock — the database
23//! serialises two transactions writing the same row — so two consumers racing
24//! for the last credit cannot both have it. Measured in this crate's tests.
25//!
26//! # Holds
27//!
28//! A job that will cost 50 credits and take a minute must not find the
29//! balance gone when it finishes. [`Ledger::hold`] reserves the amount up
30//! front (it leaves `balance` and raises `held`; `available` is the
31//! difference), and the job ends with [`Ledger::capture`] or
32//! [`Ledger::release`]. Both are a compare-and-set on the hold's status, so a
33//! hold cannot be captured twice. A hold has a lifetime; [`Ledger::sweep`]
34//! releases the ones nobody came back for.
35//!
36//! # Expiry
37//!
38//! Credits bought in March may be gone in May. Each top-up is a [`Lot`] with
39//! an expiry, consumption draws from the lot that expires first, and
40//! [`Ledger::sweep`] moves what is left in a dead lot to `system:expiry` — as
41//! a transfer, so it shows in the history like everything else.
42
43pub mod account;
44pub mod entry;
45pub mod hold;
46pub mod ledger;
47pub mod schema;
48
49pub use account::{Account, Balance, SYSTEM_CONSUMPTION, SYSTEM_EXPIRY, SYSTEM_TOPUP};
50pub use entry::{Entry, Transfer, TransferKind};
51pub use hold::{Hold, HoldStatus};
52pub use ledger::{Ledger, Sweep};
53pub use schema::{CreateLedgerTables, create_tables, drop_tables};
54
55pub use rustlavel_core::{Error, Result};