mod types; use std::collections::HashMap; use std::sync::Arc; use chrono::Utc; use types::*; #[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"); } }