#![allow(dead_code)]
use doubleentry::{
AccountKind, Amount, BalanceLimit, Date, Entry, Error, Journal, LedgerId, MemoSalt, Rejection,
currency::EUR, date,
};
pub const LEDGER: &str = "example.com/acme";
pub fn ledger() -> LedgerId {
LedgerId::new(LEDGER).unwrap()
}
pub fn journal() -> Journal {
Journal::new(ledger())
.with_salt_source(|| MemoSalt([7; 16]))
.as_actor("setup")
}
pub fn month_start(month: u8) -> Date {
Date::from_calendar_date(2026, time::Month::try_from(month).unwrap(), 1).unwrap()
}
pub fn month_end(month: u8) -> Date {
let month = time::Month::try_from(month).unwrap();
Date::from_calendar_date(2026, month, month.length(2026)).unwrap()
}
pub fn books() -> Journal {
let mut books = journal();
books.declare_currency(EUR, 2, date!("2026-01-01")).unwrap();
books
.open("Assets:Cash", AccountKind::Asset, date!("2026-01-01"))
.unwrap();
books
.open("Income:Sales", AccountKind::Income, date!("2026-01-01"))
.unwrap();
books
.register(
doubleentry::AccountSpec::new(
"Liabilities:Wallet",
AccountKind::Liability,
date!("2026-01-01"),
)
.limit(BalanceLimit::NoDebitBalance),
)
.unwrap();
books
.open("Expenses:Rent", AccountKind::Expense, date!("2026-01-01"))
.unwrap();
books
.open("Equity:Retained", AccountKind::Equity, date!("2026-01-01"))
.unwrap();
for month in 1..=12 {
books
.define_period(
format!("2026-{month:02}"),
month_start(month),
month_end(month),
)
.unwrap();
}
books
}
pub fn eur(text: &str) -> Amount {
text.parse().unwrap()
}
pub fn sale(key: &str, on: Date, amount: &str) -> Entry {
Entry::new(key, on)
.debit("Assets:Cash", eur(amount), EUR)
.credit("Income:Sales", eur(amount), EUR)
}
#[track_caller]
pub fn rejected<T: core::fmt::Debug>(result: Result<T, Error>) -> Rejection {
match result {
Err(Error::Rejected(rejection)) => rejection,
other => panic!("expected a rejection, got {other:?}"),
}
}
#[track_caller]
pub fn code<T: core::fmt::Debug>(result: Result<T, Error>) -> u16 {
rejected(result).code()
}
pub struct Claims;
impl Claims {
pub fn of(journal: &Journal) -> doubleentry::store::StoreProjection {
let mut replay = doubleentry::store::Replayer::<2>::new(journal.id());
for leaf in journal.leaves() {
replay.feed(leaf).unwrap();
}
replay.projection()
}
}
pub fn tamper_cell(
claims: &mut doubleentry::store::StoreProjection,
part: doubleentry::store::ProjectionPart,
row: usize,
cell: usize,
value: doubleentry::store::Cell,
) -> doubleentry::store::ProjectionRow {
let rows = claims.rows_mut(part);
let before = rows[row].clone();
let mut cells = before.cells().to_vec();
cells[cell] = value;
rows[row] = doubleentry::store::ProjectionRow::new(cells);
before
}
pub fn audit_leaves(
journal: &Journal,
leaves: &[Vec<u8>],
claims: &doubleentry::store::StoreProjection,
) -> Result<doubleentry::AuditReport, doubleentry::error::AuditFailure> {
let side: Vec<_> = journal
.leaves()
.map(|leaf| (leaf.bytes.to_vec(), leaf.memo.cloned(), leaf.fingerprint))
.collect();
let mut replay = doubleentry::store::Replayer::<2>::new(journal.id());
for (position, bytes) in leaves.iter().enumerate() {
let (memo, fingerprint) = side
.get(position)
.filter(|(original, _, _)| original == bytes)
.map_or((None, None), |(_, memo, fingerprint)| {
(memo.clone(), *fingerprint)
});
replay.feed(doubleentry::log::ReplayLeaf {
position: position as u64,
bytes,
memo: memo.as_ref(),
fingerprint,
})?;
}
replay.finish(claims)
}
pub fn leaf_bytes(journal: &Journal) -> Vec<Vec<u8>> {
journal.leaves().map(|leaf| leaf.bytes.to_vec()).collect()
}