sqlx_ledger/
lib.rs

1//! # sqlx-ledger
2//!
3//! This crate builds on the sqlx crate to provide a set of primitives for
4//! implementing an SQL-compatible double-entry accounting system. This system
5//! is engineered specifically for dealing with money and building financial
6//! products.
7//!
8//! ## Quick Start
9//!
10//! Add and execute the migrations from the migrations directory before usage.
11//! ```bash,ignore
12//! cp ./migrations/* <path/to/your/projects/migrations>
13//! # in your project
14//! cargo sqlx migrate
15//! ```
16//!
17//! Here is how to initialize a ledger create a primitive template and post a transaction.
18//! This is a toy example that brings all pieces together end-to-end.
19//! Not recommended for real use.
20//! ```rust
21//! use uuid::uuid;
22//! use rust_decimal::Decimal;
23//! use sqlx_ledger::{*, journal::*, account::*, tx_template::*};
24//!
25//! async fn init_ledger(journal_id: JournalId) -> SqlxLedger {
26//!     let pg_con =
27//!         std::env::var("PG_CON").unwrap_or(format!("postgres://user:password@localhost:5432/pg"));
28//!     let pool = sqlx::PgPool::connect(&pg_con).await.unwrap();
29//!     let ledger = SqlxLedger::new(&pool);
30//!
31//!     // Initialize the journal - all entities are constructed via builders
32//!     let new_journal = NewJournal::builder()
33//!         .id(journal_id)
34//!         .description("General ledger".to_string())
35//!         .name("Ledger")
36//!         .build()
37//!         .expect("Couldn't build NewJournal");
38//!
39//!     let _ = ledger.journals().create(new_journal).await;
40//!
41//!     // Initialize an income omnibus account
42//!     let main_account_id = uuid!("00000000-0000-0000-0000-000000000001");
43//!     let new_account = NewAccount::builder()
44//!         .id(main_account_id)
45//!         .name("Income")
46//!         .code("Income")
47//!         .build()
48//!         .unwrap();
49//!
50//!     let _ = ledger.accounts().create(new_account).await;
51//!
52//!     // Create the trivial 'income' template
53//!     //
54//!     // Here are the 'parameters' that the template will require as inputs.
55//!     let params = vec![
56//!         ParamDefinition::builder()
57//!             .name("sender_account_id")
58//!             .r#type(ParamDataType::UUID)
59//!             .build()
60//!             .unwrap(),
61//!         ParamDefinition::builder()
62//!             .name("units")
63//!             .r#type(ParamDataType::DECIMAL)
64//!             .build()
65//!             .unwrap()
66//!     ];
67//!
68//!     // The templates for the Entries that will be created as part of the transaction.
69//!     let entries = vec![
70//!         EntryInput::builder()
71//!             .entry_type("'INCOME_DR'")
72//!             // Reference the input parameters via CEL syntax
73//!             .account_id("params.sender_account_id")
74//!             .layer("SETTLED")
75//!             .direction("DEBIT")
76//!             .units("params.units")
77//!             .currency("'BTC'")
78//!             .build()
79//!             .unwrap(),
80//!         EntryInput::builder()
81//!             .entry_type("'INCOME_CR'")
82//!             .account_id(format!("uuid('{main_account_id}')"))
83//!             .layer("SETTLED")
84//!             .direction("CREDIT")
85//!             .units("params.units")
86//!             .currency("'BTC'")
87//!             .build()
88//!             .unwrap(),
89//!     ];
90//!     let tx_code = "GENERAL_INCOME";
91//!     let new_template = NewTxTemplate::builder()
92//!         .id(uuid::Uuid::new_v4())
93//!         .code(tx_code)
94//!         .params(params)
95//!         .tx_input(
96//!             // Template for the Transaction metadata.
97//!             TxInput::builder()
98//!                 .effective("date()")
99//!                 .journal_id(format!("uuid('{journal_id}')"))
100//!                 .build()
101//!                 .unwrap(),
102//!         )
103//!         .entries(entries)
104//!         .build()
105//!         .unwrap();
106//!
107//!     let _ = ledger.tx_templates().create(new_template).await;
108//!
109//!     ledger
110//! }
111//!
112//! tokio_test::block_on(async {
113//!     let journal_id = JournalId::from(uuid!("00000000-0000-0000-0000-000000000001"));
114//!     let ledger = init_ledger(journal_id).await;
115//!
116//!     // The account that is sending to the general income account
117//!     let sender_account_id = AccountId::new();
118//!     let sender_account = NewAccount::builder()
119//!         .id(sender_account_id)
120//!         .name(format!("Sender-{sender_account_id}"))
121//!         .code(format!("Sender-{sender_account_id}"))
122//!         .build()
123//!         .unwrap();
124//!     ledger.accounts().create(sender_account).await.unwrap();
125//!
126//!     // Prepare the input parameters that the template requires
127//!     let mut params = TxParams::new();
128//!     params.insert("sender_account_id", sender_account_id);
129//!     params.insert("units", Decimal::ONE);
130//!
131//!     // Create the transaction via the template
132//!     ledger
133//!         .post_transaction(TransactionId::new(), "GENERAL_INCOME", Some(params))
134//!         .await
135//!         .unwrap();
136//!
137//!     // Check the resulting balance
138//!     let account_balance = ledger
139//!         .balances()
140//!         .find(journal_id, sender_account_id, "BTC".parse().unwrap())
141//!         .await
142//!         .unwrap();
143//!
144//!     assert_eq!(account_balance.unwrap().settled(), Decimal::NEGATIVE_ONE);
145//! });
146//! ```
147
148#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
149#![cfg_attr(feature = "fail-on-warnings", deny(clippy::all))]
150
151pub mod account;
152pub mod balance;
153pub mod entry;
154pub mod event;
155pub mod journal;
156pub mod transaction;
157pub mod tx_template;
158
159mod error;
160mod ledger;
161mod macros;
162mod primitives;
163
164pub use error::*;
165pub use ledger::*;
166pub use primitives::*;