Last active 1741791789

by claude.

MediciBankingSystem.java Raw
1/**
2
3This Java implementation showcases the core principles of double-entry accounting, using ducats as
4the currency in honor of the Medici banking dynasty. Here's what the code demonstrates:
5
61. **The Fundamental Principle**: Every transaction affects at least two accounts
7(the double-entry principle), and the sum of debits must always equal the sum of credits.
8
92. **Five Main Account Types**:
10 - Assets: Resources owned by the business
11 - Liabilities: Debts owed by the business
12 - Equity: Owner's interest in the business
13 - Revenue: Income earned by the business
14 - Expenses: Costs incurred by the business
15
163. **Account Balance Rules**:
17 - Assets and Expenses: Increased by debits, decreased by credits
18 - Liabilities, Equity, and Revenue: Increased by credits, decreased by debits
19
204. **Key Financial Reports**:
21 - Trial Balance: Verifies that total debits equal total credits
22 - Balance Sheet: Shows Assets = Liabilities + Equity
23 - Income Statement: Shows Revenue - Expenses = Net Income
24
25The example simulates transactions for the Medici Bank in the year 1397,
26including initial capitalization, loans with interest (a key banking activity),
27property acquisition, and operating expenses.
28
29*/
30import java.math.BigDecimal;
31import java.math.RoundingMode;
32import java.time.LocalDate;
33import java.util.*;
34
35/**
36 * A double-entry accounting system implementation inspired by the
37 * Florentine banking practices of the deMedici family.
38 */
39public class MediciBankingSystem {
40
41 public static void main(String[] args) {
42 // Create a new ledger for our banking operations
43 Ledger mediciLedger = new Ledger("Medici Family Bank");
44
45 // Define our chart of accounts
46 Account cash = mediciLedger.createAccount("Cash", AccountType.ASSET);
47 Account accounts_receivable = mediciLedger.createAccount("Accounts Receivable", AccountType.ASSET);
48 Account inventory = mediciLedger.createAccount("Inventory", AccountType.ASSET);
49 Account land = mediciLedger.createAccount("Land", AccountType.ASSET);
50
51 Account accounts_payable = mediciLedger.createAccount("Accounts Payable", AccountType.LIABILITY);
52 Account loans = mediciLedger.createAccount("Loans", AccountType.LIABILITY);
53
54 Account capital = mediciLedger.createAccount("Owner's Capital", AccountType.EQUITY);
55 Account retained_earnings = mediciLedger.createAccount("Retained Earnings", AccountType.EQUITY);
56
57 Account revenue = mediciLedger.createAccount("Revenue", AccountType.REVENUE);
58 Account interest_income = mediciLedger.createAccount("Interest Income", AccountType.REVENUE);
59
60 Account expenses = mediciLedger.createAccount("Expenses", AccountType.EXPENSE);
61 Account wages = mediciLedger.createAccount("Wages", AccountType.EXPENSE);
62
63 // Starting our banking operations with initial capital
64 System.out.println("=== STARTING THE MEDICI BANK ===");
65 mediciLedger.recordTransaction(
66 LocalDate.of(1397, 1, 1),
67 "Initial investment from Giovanni de' Medici",
68 new TransactionEntry(cash, new BigDecimal("10000.00")),
69 new TransactionEntry(capital, new BigDecimal("10000.00"))
70 );
71
72 // Recording a loan to a wool merchant
73 mediciLedger.recordTransaction(
74 LocalDate.of(1397, 2, 15),
75 "Loan to Wool Merchant",
76 new TransactionEntry(accounts_receivable, new BigDecimal("2000.00")),
77 new TransactionEntry(cash, new BigDecimal("2000.00"))
78 );
79
80 // Receiving partial payment with interest
81 mediciLedger.recordTransaction(
82 LocalDate.of(1397, 8, 10),
83 "Partial loan repayment from Wool Merchant with interest",
84 new TransactionEntry(cash, new BigDecimal("1200.00")),
85 new TransactionEntry(accounts_receivable, new BigDecimal("1000.00")),
86 new TransactionEntry(interest_income, new BigDecimal("200.00"))
87 );
88
89 // Purchasing land for a new banking house
90 mediciLedger.recordTransaction(
91 LocalDate.of(1397, 9, 5),
92 "Purchase of land for new Medici banking house",
93 new TransactionEntry(land, new BigDecimal("3000.00")),
94 new TransactionEntry(cash, new BigDecimal("3000.00"))
95 );
96
97 // Paying wages to bank employees
98 mediciLedger.recordTransaction(
99 LocalDate.of(1397, 12, 1),
100 "Quarterly wages for bank employees",
101 new TransactionEntry(wages, new BigDecimal("800.00")),
102 new TransactionEntry(cash, new BigDecimal("800.00"))
103 );
104
105 // Print the trial balance to verify our accounting is balanced
106 System.out.println("\n=== MEDICI BANK TRIAL BALANCE (Year 1397) ===");
107 mediciLedger.printTrialBalance();
108
109 // Print the balance sheet
110 System.out.println("\n=== MEDICI BANK BALANCE SHEET (Year 1397) ===");
111 mediciLedger.printBalanceSheet();
112
113 // Print the income statement
114 System.out.println("\n=== MEDICI BANK INCOME STATEMENT (Year 1397) ===");
115 mediciLedger.printIncomeStatement();
116 }
117}
118
119/**
120 * Represents a financial account in the double-entry system
121 */
122class Account {
123 private String name;
124 private AccountType type;
125 private BigDecimal balance;
126
127 public Account(String name, AccountType type) {
128 this.name = name;
129 this.type = type;
130 this.balance = BigDecimal.ZERO;
131 }
132
133 public String getName() {
134 return name;
135 }
136
137 public AccountType getType() {
138 return type;
139 }
140
141 public BigDecimal getBalance() {
142 return balance;
143 }
144
145 /**
146 * Debits increase ASSET and EXPENSE accounts
147 * Debits decrease LIABILITY, EQUITY, and REVENUE accounts
148 */
149 public void debit(BigDecimal amount) {
150 if (type == AccountType.ASSET || type == AccountType.EXPENSE) {
151 balance = balance.add(amount);
152 } else {
153 balance = balance.subtract(amount);
154 }
155 }
156
157 /**
158 * Credits decrease ASSET and EXPENSE accounts
159 * Credits increase LIABILITY, EQUITY, and REVENUE accounts
160 */
161 public void credit(BigDecimal amount) {
162 if (type == AccountType.ASSET || type == AccountType.EXPENSE) {
163 balance = balance.subtract(amount);
164 } else {
165 balance = balance.add(amount);
166 }
167 }
168
169 @Override
170 public String toString() {
171 return name + " (" + type + "): " + balance + " ducats";
172 }
173}
174
175/**
176 * The different types of accounts in double-entry accounting
177 */
178enum AccountType {
179 ASSET, // Resources owned by the business
180 LIABILITY, // Debts owed by the business
181 EQUITY, // Owner's interest in the business
182 REVENUE, // Income earned by the business
183 EXPENSE // Costs incurred by the business
184}
185
186/**
187 * Represents a single entry in a transaction
188 */
189class TransactionEntry {
190 private Account account;
191 private BigDecimal amount;
192
193 public TransactionEntry(Account account, BigDecimal amount) {
194 this.account = account;
195 this.amount = amount;
196 }
197
198 public Account getAccount() {
199 return account;
200 }
201
202 public BigDecimal getAmount() {
203 return amount;
204 }
205}
206
207/**
208 * Represents a complete financial transaction in the double-entry system
209 */
210class Transaction {
211 private String description;
212 private LocalDate date;
213 private List<TransactionEntry> debits;
214 private List<TransactionEntry> credits;
215
216 public Transaction(LocalDate date, String description) {
217 this.date = date;
218 this.description = description;
219 this.debits = new ArrayList<>();
220 this.credits = new ArrayList<>();
221 }
222
223 public void addDebit(TransactionEntry entry) {
224 debits.add(entry);
225 }
226
227 public void addCredit(TransactionEntry entry) {
228 credits.add(entry);
229 }
230
231 public boolean isBalanced() {
232 BigDecimal totalDebits = debits.stream()
233 .map(TransactionEntry::getAmount)
234 .reduce(BigDecimal.ZERO, BigDecimal::add);
235
236 BigDecimal totalCredits = credits.stream()
237 .map(TransactionEntry::getAmount)
238 .reduce(BigDecimal.ZERO, BigDecimal::add);
239
240 return totalDebits.compareTo(totalCredits) == 0;
241 }
242
243 public void post() {
244 // Apply all debits
245 for (TransactionEntry entry : debits) {
246 entry.getAccount().debit(entry.getAmount());
247 }
248
249 // Apply all credits
250 for (TransactionEntry entry : credits) {
251 entry.getAccount().credit(entry.getAmount());
252 }
253 }
254
255 @Override
256 public String toString() {
257 StringBuilder sb = new StringBuilder();
258 sb.append("Transaction: ").append(date).append(" - ").append(description).append("\n");
259
260 sb.append(" Debits:\n");
261 for (TransactionEntry entry : debits) {
262 sb.append(" ").append(entry.getAccount().getName())
263 .append(": ").append(entry.getAmount()).append(" ducats\n");
264 }
265
266 sb.append(" Credits:\n");
267 for (TransactionEntry entry : credits) {
268 sb.append(" ").append(entry.getAccount().getName())
269 .append(": ").append(entry.getAmount()).append(" ducats\n");
270 }
271
272 return sb.toString();
273 }
274}
275
276/**
277 * The main ledger that keeps track of all accounts and transactions
278 */
279class Ledger {
280 private String name;
281 private List<Account> accounts;
282 private List<Transaction> transactions;
283
284 public Ledger(String name) {
285 this.name = name;
286 this.accounts = new ArrayList<>();
287 this.transactions = new ArrayList<>();
288 }
289
290 public Account createAccount(String name, AccountType type) {
291 Account account = new Account(name, type);
292 accounts.add(account);
293 return account;
294 }
295
296 /**
297 * Records a transaction with any number of debits and credits,
298 * ensuring that debits = credits (the fundamental principle of double-entry)
299 */
300 public void recordTransaction(LocalDate date, String description,
301 TransactionEntry... entries) {
302 Transaction transaction = new Transaction(date, description);
303
304 // Separate entries into debits and credits based on account type
305 for (TransactionEntry entry : entries) {
306 Account account = entry.getAccount();
307 AccountType type = account.getType();
308
309 // For asset and expense accounts, positive amounts are debits
310 // For liability, equity, and revenue accounts, positive amounts are credits
311 if (type == AccountType.ASSET || type == AccountType.EXPENSE) {
312 if (entry.getAmount().compareTo(BigDecimal.ZERO) >= 0) {
313 transaction.addDebit(entry);
314 } else {
315 // Negative amount means we're crediting the account
316 transaction.addCredit(new TransactionEntry(
317 account, entry.getAmount().abs()));
318 }
319 } else {
320 if (entry.getAmount().compareTo(BigDecimal.ZERO) >= 0) {
321 transaction.addCredit(entry);
322 } else {
323 // Negative amount means we're debiting the account
324 transaction.addDebit(new TransactionEntry(
325 account, entry.getAmount().abs()));
326 }
327 }
328 }
329
330 // Verify that the transaction is balanced
331 if (!transaction.isBalanced()) {
332 throw new IllegalArgumentException("Transaction is not balanced: debits must equal credits");
333 }
334
335 // Post the transaction to update account balances
336 transaction.post();
337
338 // Record the transaction in the ledger
339 transactions.add(transaction);
340 System.out.println(transaction);
341 }
342
343 /**
344 * Prints a trial balance to verify that debits = credits across all accounts
345 */
346 public void printTrialBalance() {
347 BigDecimal totalDebits = BigDecimal.ZERO;
348 BigDecimal totalCredits = BigDecimal.ZERO;
349
350 System.out.printf("%-30s %-15s %-15s%n", "Account", "Debit (Ducats)", "Credit (Ducats)");
351 System.out.println("-".repeat(60));
352
353 for (Account account : accounts) {
354 BigDecimal balance = account.getBalance();
355
356 // For the trial balance, we show positive balances in their normal position
357 // Assets and Expenses with positive balances are shown as debits
358 // Liabilities, Equity, and Revenue with positive balances are shown as credits
359 if (account.getType() == AccountType.ASSET ||
360 account.getType() == AccountType.EXPENSE) {
361 if (balance.compareTo(BigDecimal.ZERO) > 0) {
362 System.out.printf("%-30s %-15s %-15s%n",
363 account.getName(), balance.setScale(2, RoundingMode.HALF_UP), "");
364 totalDebits = totalDebits.add(balance);
365 } else if (balance.compareTo(BigDecimal.ZERO) < 0) {
366 System.out.printf("%-30s %-15s %-15s%n",
367 account.getName(), "", balance.abs().setScale(2, RoundingMode.HALF_UP));
368 totalCredits = totalCredits.add(balance.abs());
369 }
370 } else {
371 if (balance.compareTo(BigDecimal.ZERO) > 0) {
372 System.out.printf("%-30s %-15s %-15s%n",
373 account.getName(), "", balance.setScale(2, RoundingMode.HALF_UP));
374 totalCredits = totalCredits.add(balance);
375 } else if (balance.compareTo(BigDecimal.ZERO) < 0) {
376 System.out.printf("%-30s %-15s %-15s%n",
377 account.getName(), balance.abs().setScale(2, RoundingMode.HALF_UP), "");
378 totalDebits = totalDebits.add(balance.abs());
379 }
380 }
381 }
382
383 System.out.println("-".repeat(60));
384 System.out.printf("%-30s %-15s %-15s%n",
385 "TOTAL", totalDebits.setScale(2, RoundingMode.HALF_UP),
386 totalCredits.setScale(2, RoundingMode.HALF_UP));
387
388 if (totalDebits.compareTo(totalCredits) == 0) {
389 System.out.println("\nThe books are balanced! ✓");
390 } else {
391 System.out.println("\nWARNING: The books are NOT balanced! ✗");
392 }
393 }
394
395 /**
396 * Prints a balance sheet (Assets = Liabilities + Equity)
397 */
398 public void printBalanceSheet() {
399 BigDecimal totalAssets = BigDecimal.ZERO;
400 BigDecimal totalLiabilities = BigDecimal.ZERO;
401 BigDecimal totalEquity = BigDecimal.ZERO;
402
403 // Print Assets
404 System.out.println("ASSETS");
405 System.out.println("-".repeat(40));
406 for (Account account : accounts) {
407 if (account.getType() == AccountType.ASSET && account.getBalance().compareTo(BigDecimal.ZERO) != 0) {
408 System.out.printf("%-30s %10s%n",
409 account.getName(), account.getBalance().setScale(2, RoundingMode.HALF_UP));
410 totalAssets = totalAssets.add(account.getBalance());
411 }
412 }
413 System.out.println("-".repeat(40));
414 System.out.printf("%-30s %10s%n", "TOTAL ASSETS", totalAssets.setScale(2, RoundingMode.HALF_UP));
415 System.out.println();
416
417 // Print Liabilities
418 System.out.println("LIABILITIES");
419 System.out.println("-".repeat(40));
420 for (Account account : accounts) {
421 if (account.getType() == AccountType.LIABILITY && account.getBalance().compareTo(BigDecimal.ZERO) != 0) {
422 System.out.printf("%-30s %10s%n",
423 account.getName(), account.getBalance().setScale(2, RoundingMode.HALF_UP));
424 totalLiabilities = totalLiabilities.add(account.getBalance());
425 }
426 }
427 System.out.println("-".repeat(40));
428 System.out.printf("%-30s %10s%n", "TOTAL LIABILITIES", totalLiabilities.setScale(2, RoundingMode.HALF_UP));
429 System.out.println();
430
431 // Print Equity
432 System.out.println("EQUITY");
433 System.out.println("-".repeat(40));
434 for (Account account : accounts) {
435 if (account.getType() == AccountType.EQUITY && account.getBalance().compareTo(BigDecimal.ZERO) != 0) {
436 System.out.printf("%-30s %10s%n",
437 account.getName(), account.getBalance().setScale(2, RoundingMode.HALF_UP));
438 totalEquity = totalEquity.add(account.getBalance());
439 }
440 }
441 System.out.println("-".repeat(40));
442 System.out.printf("%-30s %10s%n", "TOTAL EQUITY", totalEquity.setScale(2, RoundingMode.HALF_UP));
443 System.out.println();
444
445 // Verify the accounting equation: Assets = Liabilities + Equity
446 System.out.println("ACCOUNTING EQUATION");
447 System.out.println("-".repeat(40));
448 System.out.printf("%-30s %10s%n", "Total Assets", totalAssets.setScale(2, RoundingMode.HALF_UP));
449 System.out.printf("%-30s %10s%n", "Total Liabilities + Equity",
450 totalLiabilities.add(totalEquity).setScale(2, RoundingMode.HALF_UP));
451
452 if (totalAssets.compareTo(totalLiabilities.add(totalEquity)) == 0) {
453 System.out.println("\nThe accounting equation is balanced! ✓");
454 } else {
455 System.out.println("\nWARNING: The accounting equation is NOT balanced! ✗");
456 }
457 }
458
459 /**
460 * Prints an income statement (Revenue - Expenses = Net Income)
461 */
462 public void printIncomeStatement() {
463 BigDecimal totalRevenue = BigDecimal.ZERO;
464 BigDecimal totalExpenses = BigDecimal.ZERO;
465
466 // Print Revenue
467 System.out.println("REVENUE");
468 System.out.println("-".repeat(40));
469 for (Account account : accounts) {
470 if (account.getType() == AccountType.REVENUE && account.getBalance().compareTo(BigDecimal.ZERO) != 0) {
471 System.out.printf("%-30s %10s%n",
472 account.getName(), account.getBalance().setScale(2, RoundingMode.HALF_UP));
473 totalRevenue = totalRevenue.add(account.getBalance());
474 }
475 }
476 System.out.println("-".repeat(40));
477 System.out.printf("%-30s %10s%n", "TOTAL REVENUE", totalRevenue.setScale(2, RoundingMode.HALF_UP));
478 System.out.println();
479
480 // Print Expenses
481 System.out.println("EXPENSES");
482 System.out.println("-".repeat(40));
483 for (Account account : accounts) {
484 if (account.getType() == AccountType.EXPENSE && account.getBalance().compareTo(BigDecimal.ZERO) != 0) {
485 System.out.printf("%-30s %10s%n",
486 account.getName(), account.getBalance().setScale(2, RoundingMode.HALF_UP));
487 totalExpenses = totalExpenses.add(account.getBalance());
488 }
489 }
490 System.out.println("-".repeat(40));
491 System.out.printf("%-30s %10s%n", "TOTAL EXPENSES", totalExpenses.setScale(2, RoundingMode.HALF_UP));
492 System.out.println();
493
494 // Calculate Net Income
495 BigDecimal netIncome = totalRevenue.subtract(totalExpenses);
496 System.out.println("SUMMARY");
497 System.out.println("-".repeat(40));
498 System.out.printf("%-30s %10s%n", "Total Revenue", totalRevenue.setScale(2, RoundingMode.HALF_UP));
499 System.out.printf("%-30s %10s%n", "Total Expenses", totalExpenses.setScale(2, RoundingMode.HALF_UP));
500 System.out.println("-".repeat(40));
501 System.out.printf("%-30s %10s%n", "NET INCOME", netIncome.setScale(2, RoundingMode.HALF_UP));
502 }
503}
504
505