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::*;