From ccb6d208204fb286e469e87ad9c85a8c8b7be72a Mon Sep 17 00:00:00 2001 From: PROINT <> Date: Wed, 22 Jul 2026 17:03:47 +0700 Subject: Added JournalEntryRepository --- src/main.rs | 196 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 194 insertions(+), 2 deletions(-) (limited to 'src/main.rs') 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 = 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(""); + 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 = 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"); + } } -- cgit v1.3