summaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs196
1 files changed, 194 insertions, 2 deletions
diff --git a/src/main.rs b/src/main.rs
index bfc2330..831c0dd 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,7 +1,199 @@
mod types;
+use std::collections::HashMap;
+use std::sync::Arc;
+
+use chrono::Utc;
use types::*;
-fn main() {
- println!("Hello World!");
+#[tokio::main(flavor = "current_thread")]
+async fn main() {
+ let account_repo = Arc::new(InMemoryAccountRepository::default());
+
+ // Seed a few accounts.
+ let cash = account_repo
+ .create(NewAccount {
+ account_type: AccountType::Asset,
+ name: "Cash".into(),
+ description: String::new(),
+ parent: None,
+ })
+ .await
+ .unwrap();
+ let revenue = account_repo
+ .create(NewAccount {
+ account_type: AccountType::Revenue,
+ name: "Sales Revenue".into(),
+ description: String::new(),
+ parent: None,
+ })
+ .await
+ .unwrap();
+ let rent = account_repo
+ .create(NewAccount {
+ account_type: AccountType::Expense,
+ name: "Rent Expense".into(),
+ description: String::new(),
+ parent: None,
+ })
+ .await
+ .unwrap();
+
+ // Post entries through the repository — it validates and checks accounts exist.
+ let journal_repo = InMemoryJournalEntryRepository::new(account_repo.clone());
+
+ let sale = journal_repo
+ .create(NewJournalEntry {
+ date: Utc::now(),
+ description: "Cash sale".into(),
+ postings: vec![
+ NewPosting {
+ posting_type: PostingType::Debit,
+ account_id: cash.id(),
+ amount: 500_000,
+ },
+ NewPosting {
+ posting_type: PostingType::Credit,
+ account_id: revenue.id(),
+ amount: 500_000,
+ },
+ ],
+ })
+ .await
+ .unwrap();
+
+ let rent_payment = journal_repo
+ .create(NewJournalEntry {
+ date: Utc::now(),
+ description: "Pay office rent".into(),
+ postings: vec![
+ NewPosting {
+ posting_type: PostingType::Debit,
+ account_id: rent.id(),
+ amount: 200_000,
+ },
+ NewPosting {
+ posting_type: PostingType::Credit,
+ account_id: cash.id(),
+ amount: 200_000,
+ },
+ ],
+ })
+ .await
+ .unwrap();
+
+ let entries = vec![sale, rent_payment];
+ let accounts = vec![cash, revenue, rent];
+ print_journal(&entries, &accounts);
+ println!();
+ print_ledger(&entries, &accounts);
+}
+
+fn account_type_name(t: &AccountType) -> &'static str {
+ match t {
+ AccountType::Asset => "Asset",
+ AccountType::Liability => "Liability",
+ AccountType::Equity => "Equity",
+ AccountType::Revenue => "Revenue",
+ AccountType::Expense => "Expense",
+ }
+}
+
+/// Format a whole-won amount with thousands separators.
+fn group(n: u64) -> String {
+ let s = n.to_string();
+ let b = s.as_bytes();
+ let mut out = String::new();
+ for (i, c) in b.iter().enumerate() {
+ if i > 0 && (b.len() - i) % 3 == 0 {
+ out.push(',');
+ }
+ out.push(*c as char);
+ }
+ out
+}
+
+fn signed(balance: i128) -> String {
+ if balance < 0 {
+ format!("-{}", group((-balance) as u64))
+ } else {
+ group(balance as u64)
+ }
+}
+
+fn print_journal(entries: &[JournalEntry], accounts: &[Account]) {
+ let by_id: HashMap<AccountId, &Account> = accounts.iter().map(|a| (a.id(), a)).collect();
+
+ println!("JOURNAL");
+ println!("{}", "-".repeat(52));
+ for entry in entries {
+ println!("{} {}", entry.date.format("%Y-%m-%d"), entry.description);
+ for p in &entry.postings {
+ let name = by_id
+ .get(&p.account_id)
+ .map(|a| a.name.as_str())
+ .unwrap_or("<unknown>");
+ let (dr, cr) = match p.posting_type {
+ PostingType::Debit => (group(p.amount), String::new()),
+ PostingType::Credit => (String::new(), group(p.amount)),
+ };
+ println!(" {:<18} {:>12} {:>12}", name, dr, cr);
+ }
+ }
+}
+
+fn print_ledger(entries: &[JournalEntry], accounts: &[Account]) {
+ // Accumulate debit/credit totals per account.
+ let mut totals: HashMap<AccountId, (u64, u64)> = HashMap::new();
+ for entry in entries {
+ for p in &entry.postings {
+ let e = totals.entry(p.account_id).or_insert((0, 0));
+ match p.posting_type {
+ PostingType::Debit => e.0 += p.amount,
+ PostingType::Credit => e.1 += p.amount,
+ }
+ }
+ }
+
+ println!("LEDGER (KRW)");
+ println!("{}", "-".repeat(72));
+ println!(
+ "{:<18} {:<10} {:>12} {:>12} {:>14}",
+ "Account", "Type", "Debits", "Credits", "Balance"
+ );
+ println!("{}", "-".repeat(72));
+
+ let (mut total_dr, mut total_cr) = (0u64, 0u64);
+ for account in accounts {
+ let (dr, cr) = totals.get(&account.id()).copied().unwrap_or((0, 0));
+ total_dr += dr;
+ total_cr += cr;
+ // Balance shown on the account's normal side (debit-normal for Asset/Expense).
+ let balance: i128 = match &account.account_type {
+ AccountType::Asset | AccountType::Expense => dr as i128 - cr as i128,
+ _ => cr as i128 - dr as i128,
+ };
+ println!(
+ "{:<18} {:<10} {:>12} {:>12} {:>14}",
+ account.name,
+ account_type_name(&account.account_type),
+ group(dr),
+ group(cr),
+ signed(balance),
+ );
+ }
+
+ println!("{}", "-".repeat(72));
+ println!(
+ "{:<18} {:<10} {:>12} {:>12}",
+ "TOTAL",
+ "",
+ group(total_dr),
+ group(total_cr)
+ );
+ if total_dr == total_cr {
+ println!("\nBooks balance: debits == credits ({} KRW)", group(total_dr));
+ } else {
+ println!("\nWARNING: books do not balance");
+ }
}