1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
//! An immutable, tamper-evident double-entry bookkeeping engine.
//!
//! A **ledger** is one entity's books, kept as one append-only
//! [RFC 6962](https://www.rfc-editor.org/rfc/rfc6962) SHA-256 Merkle log of
//! typed records: a genesis record, account events, currency declarations,
//! policy events, period definitions, entries, clearings and seals. Every record has exactly one
//! canonical byte form, so a position in the log, a leaf hash and a proof mean
//! the same thing to every implementation.
//!
//! Every write is decided by **one pure function**, the rule core: it takes
//! the draft and a read-only view of the ledger at the position the record
//! will be appended, and returns either the record to write or one
//! [`Rejection`] with a stable code. No façade and no store implements a
//! rule; [`Journal::audit`] and [`Ledger::audit`] replay the whole log
//! through the same core.
//!
//! # Where things are
//!
//! - **The crate root** — what bookkeeping code names: the two façades with
//! the same methods, [`Journal`] (sync, in memory) and [`Ledger`] (async,
//! over a store); the one [`Error`] and its [`Rejection`]; the values
//! (exact [`Amount`]s, [`Currency`] codes, validated names such as
//! [`AccountPath`] and [`Label`], the derived [`EntryId`]); the [`Entry`]
//! draft; [`BalanceQuery`]; the [`TreeHead`] proofs verify against.
//! - [`currency`](mod@currency) — the currency constants (`EUR`, `JPY`, …).
//! - [`report`] — what the reads return: [`TrialBalance`](report::TrialBalance),
//! [`Statement`](report::Statement), [`Rollup`](report::Rollup),
//! [`OpenItem`](report::OpenItem)s and balance assertions.
//! - [`export`] — the GoBD export view: verifiable flat rows.
//! - [`error`] — the payloads [`Error`]'s variants carry.
//! - [`log`] — proofs and the RFC 6962 hashing an independent verifier
//! re-implements; [`record`] — the eight record kinds and their canonical
//! bytes.
//! - [`prelude`] — the everyday vocabulary in one import.
//! - [`store`] — where a ledger is kept: the [`Store`](store::Store)
//! contract, [`MemoryStore`](store::MemoryStore), the SQL store, and the
//! rule core's protocol for implementers of another store.
//! - [`transparency`] — C2SP checkpoints, witness requests and replies,
//! policies, the auditor's verifier, proof bundles and tiles.
//! - `testing` (feature `testing`) — the conformance suite every store
//! passes.
//!
//! The user guide covers the same ground:
//! [Start](https://hupe1980.github.io/doubleentry/docs/start/),
//! [Concepts](https://hupe1980.github.io/doubleentry/docs/concepts/),
//! [Reporting](https://hupe1980.github.io/doubleentry/docs/reporting/),
//! [Operating](https://hupe1980.github.io/doubleentry/docs/operating/) and
//! [Assurance](https://hupe1980.github.io/doubleentry/docs/assurance/).
//!
//! # Example
//!
//! ```
//! use doubleentry::{AccountKind, Entry, Journal, LedgerId, Rejection, amount, date, currency::EUR};
//!
//! let mut books = Journal::new(LedgerId::new("example.com/acme")?).as_actor("setup");
//! books.declare_currency(EUR, 2, date!("2026-01-01"))?;
//! books.open("Assets:Cash", AccountKind::Asset, date!("2026-01-01"))?;
//! books.open("Income:Sales", AccountKind::Income, date!("2026-01-01"))?;
//! books.define_period("2026-03", date!("2026-03-01"), date!("2026-03-31"))?;
//!
//! let invoice = Entry::new("invoice-2026-0001", date!("2026-03-15"))
//! .debit("Assets:Cash", amount!("1190.00"), EUR)
//! .credit("Income:Sales", amount!("1190.00"), EUR);
//! let recorded = books.record(invoice.clone())?;
//! assert!(!recorded.replayed);
//! assert!(books.record(invoice)?.replayed); // a retry changes nothing
//!
//! // An unbalanced draft is refused with a stable code.
//! let refused = books.record(
//! Entry::new("invoice-2026-0002", date!("2026-03-16"))
//! .debit("Assets:Cash", amount!("10.00"), EUR)
//! .credit("Income:Sales", amount!("9.00"), EUR),
//! );
//! assert_eq!(refused.err().as_ref().and_then(|e| e.rejection()).map(Rejection::code), Some(106));
//!
//! books.seal_period("2026-03")?;
//! books.audit()?;
//! # Ok::<(), doubleentry::Error>(())
//! ```
//!
//! # A guide
//!
//! 1. **Set up the books** — [`declare_currency`](Journal::declare_currency)
//! (with its exponent), [`define_months`](Journal::define_months) (or
//! [`define_period`](Journal::define_period)), [`open`](Journal::open)
//! accounts by path, and optionally [`set_limit`](Journal::set_limit).
//! Every setup call is idempotent — repeating it with the same definition
//! writes nothing, a different one is refused — so a service runs it on
//! every start.
//! 2. **Draft and record** — [`Entry`] builders are infallible; a malformed
//! draft is refused by [`record`](Journal::record) with a [`Rejection`]
//! and a stable code. The idempotency key is the entry's identity: a
//! retry of the same draft returns the original [`Recorded`] with
//! `replayed == true`. [`record_batch`](Journal::record_batch) records
//! many drafts atomically, in order, in one transaction.
//! 3. **Read** — [`balance`](Journal::balance) and the reports take a
//! [`BalanceQuery`] (a log prefix, a date window by booking or value
//! date, a period, dimension filters); both façades answer them alike.
//! 4. **Close and prove** — [`seal_period`](Journal::seal_period) commits
//! every balance through the period's end; periods are sealed in
//! calendar order, and [`seal_through`](Journal::seal_through) seals every
//! open period up to one in a single call;
//! [`prove_entry`](Journal::prove_entry),
//! [`prove_consistency`](Journal::prove_consistency) and
//! [`prove_balance`](Journal::prove_balance) give proofs that verify
//! against a [`TreeHead`] alone; [`transparency`] signs heads and lets
//! independent witnesses cosign them.
//! 5. **Keep it durable** — the same calls on a [`Ledger`] over a
//! [`store`] (`MemoryStore`, SQLite, PostgreSQL), each `.await`ed; the
//! store runs the rule core inside its write transaction.
//! [`audit`](Ledger::audit) replays the log and compares.
//!
//! # Errors
//!
//! Every operation of either façade returns [`Result`], whose error is the
//! one [`Error`]. Match on its variant to decide what to do:
//!
//! | Error | [`is_transient`](Error::is_transient) | [`may_have_applied`](Error::may_have_applied) | What the caller does |
//! |---|---|---|---|
//! | [`Rejected(r)`](Error::Rejected) | false | false | show `r` and its code; fix the input; never retry unchanged |
//! | [`BatchRejected { index, rejection }`](Error::BatchRejected) | false | false | nothing of the batch was written; fix draft `index` (or drop it) and record the batch again |
//! | [`Value(e)`](Error::Value), [`Amount(e)`](Error::Amount) | false | false | fix the value or the arithmetic at the call site (never from a ledger operation) |
//! | [`Query(e)`](Error::Query) | false | false | fix the question: the entry, period, window or position asked for |
//! | [`Transparency(e)`](Error::Transparency) | false | false | the artefact is refused: a forged, stale or foreign checkpoint, bundle or reply; never retry it unchanged |
//! | [`Integrity(e)`](Error::Integrity) | false | false | stop writing; investigate position `e.position()`; restore from a backup and [`audit`](Journal::audit) |
//! | [`Backend(b)`](Error::Backend), transient, definite | true | false | retry later with the **same** draft |
//! | [`Backend(b)`](Error::Backend), outcome unknown | either | true | retry with the **same** draft: it applies once, or comes back with `replayed == true` |
//! | [`Backend(b)`](Error::Backend), permanent, definite | false | false | do not retry automatically: an operator problem (configuration, permissions) |
//! | [`Config(e)`](Error::Config) | false | false | the store holds another ledger, scale or schema: fix the URL, schema name, ledger id or `P` — the data is fine |
//!
//! `{:?}` on an [`Error`] prints its `Display` line, stable code included, so
//! `fn main() -> doubleentry::Result<()>` reports `rejected: [427] …`.
//!
//! ```
//! use doubleentry::prelude::*;
//!
//! # fn record(books: &mut Journal, entry: Entry) -> Result<()> {
//! match books.record(entry) {
//! Ok(recorded) if recorded.replayed => {} // a retry: nothing written
//! Ok(_) => {}
//! Err(Error::Rejected(rejection)) => eprintln!("refused: {rejection}"),
//! Err(error) if error.is_transient() => return Err(error), // retry later, same draft
//! Err(error) => return Err(error),
//! }
//! # Ok(())
//! # }
//! ```
//!
//! # Feature flags
//!
//! All features are off by default.
//!
// Assertions and fixture setup are allowed to panic; library code is not.
/// Hooks for the `fuzz/` package (cargo-fuzz sets `cfg(fuzzing)`): parsers
/// that are not public API, reached only from the fuzz targets. Not built
/// otherwise, so invisible to docs, `cargo public-api` and semver.
pub use crate;
pub use Rejection;
pub use ;
pub use Account;
pub use Journal;
pub use Ledger;
pub use TreeHead;
pub use ;
pub use BalanceQuery;
pub use ;
/// The currency codes as constants — `use doubleentry::currency::JPY;`. The
/// [`prelude`] brings the five most common; any other ISO 4217 code is
/// [`Currency::new`] or the [`currency!`] literal.
/// The log's record kinds and their canonical byte form — what an exporter
/// or an independent verifier decodes; bookkeeping code rarely names them.
///
/// Every leaf is `kind ‖ canonical(record)`: `Record::decode` reads one
/// strictly, `Record::to_leaf_bytes` writes it.
/// The calendar date every booking date, value date and effective date is
/// (re-exported from `time`, so [`date!`] needs no dependency of yours).
pub use Date;
/// A [`Date`] from a `YYYY-MM-DD` literal, checked at compile time.
///
/// ```
/// use doubleentry::{Date, date};
///
/// const MARCH_15: Date = date!("2026-03-15");
/// assert_eq!(MARCH_15.to_string(), "2026-03-15");
/// assert_eq!(date!("2024-02-29").ordinal(), 60); // a leap day
/// ```
///
/// A day the month does not have:
///
/// ```compile_fail
/// let _ = doubleentry::date!("2026-02-30");
/// ```
///
/// Anything but four, two and two digits:
///
/// ```compile_fail
/// let _ = doubleentry::date!("2026-3-1");
/// ```
/// The date a `YYYY-MM-DD` text names; `None` for any other text or a day
/// that does not exist. The engine of [`date!`].
pub const
/// The value of a run of ASCII digits; `None` for any other byte.
const
/// Every code block of the README, compiled and run as a doctest.
;