diff --git a/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/Account.java b/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/Account.java new file mode 100644 index 000000000..2747da843 --- /dev/null +++ b/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/Account.java @@ -0,0 +1,64 @@ +package com.codedifferently.lesson17.bank; + +import com.codedifferently.lesson17.bank.exceptions.InsufficientFundsException; +import java.util.Set; + +/** + * Represents a bank account interface that defines common account operations. This interface + * follows the Interface Segregation Principle by providing only the essential operations that all + * account types must support. + */ +public interface Account { + + /** + * Gets the account number. + * + * @return The account number. + */ + String getAccountNumber(); + + /** + * Gets the owners of the account. + * + * @return The owners of the account. + */ + Set getOwners(); + + /** + * Deposits funds into the account. + * + * @param amount The amount to deposit. + * @throws IllegalStateException if the account is closed or amount is invalid. + */ + void deposit(double amount) throws IllegalStateException; + + /** + * Withdraws funds from the account. + * + * @param amount The amount to withdraw. + * @throws InsufficientFundsException if insufficient funds available. + * @throws IllegalStateException if the account is closed or amount is invalid. + */ + void withdraw(double amount) throws InsufficientFundsException, IllegalStateException; + + /** + * Gets the balance of the account. + * + * @return The current account balance. + */ + double getBalance(); + + /** + * Closes the account. + * + * @throws IllegalStateException if the account cannot be closed. + */ + void closeAccount() throws IllegalStateException; + + /** + * Checks if the account is closed. + * + * @return True if the account is closed, otherwise false. + */ + boolean isClosed(); +} diff --git a/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/BankAtm.java b/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/BankAtm.java index 8cbcd3cc0..5bbc00293 100644 --- a/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/BankAtm.java +++ b/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/BankAtm.java @@ -1,19 +1,44 @@ package com.codedifferently.lesson17.bank; +import com.codedifferently.lesson17.bank.audit.TransactionObserver; import com.codedifferently.lesson17.bank.exceptions.AccountNotFoundException; +import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; -/** Represents a bank ATM. */ +/** Represents a bank ATM that supports multiple account types and audit logging. */ public class BankAtm { private final Map customerById = new HashMap<>(); - private final Map accountByNumber = new HashMap<>(); + private final Map accountByNumber = new HashMap<>(); + private final List observers = new ArrayList<>(); + + /** Adds a transaction observer for audit logging. */ + public void addObserver(TransactionObserver observer) { + if (observer != null && !observers.contains(observer)) { + observers.add(observer); + } + } + + /** Removes a transaction observer. */ + public void removeObserver(TransactionObserver observer) { + observers.remove(observer); + } + + /** Notifies all observers about a transaction. */ + private void notifyObservers( + String transactionType, double amount, String accountNumber, String description) { + for (TransactionObserver observer : observers) { + observer.onTransaction(transactionType, amount, accountNumber, description); + } + } /** - * Adds a checking account to the bank. + * Adds an account to the bank. This method now accepts any Account type, supporting both + * CheckingAccount and SavingsAccount while maintaining backward compatibility. * * @param account The account to add. */ @@ -27,6 +52,22 @@ public void addAccount(CheckingAccount account) { }); } + /** + * Adds a savings account to the bank. This overloaded method allows adding SavingsAccount objects + * while maintaining the same public interface. + * + * @param account The savings account to add. + */ + public void addAccount(SavingsAccount account) { + accountByNumber.put(account.getAccountNumber(), account); + account + .getOwners() + .forEach( + owner -> { + customerById.put(owner.getId(), owner); + }); + } + /** * Finds all accounts owned by a customer. * @@ -40,36 +81,68 @@ public Set findAccountsByCustomerId(UUID customerId) { } /** - * Deposits funds into an account. + * Deposits funds into an account using cash. * * @param accountNumber The account number. * @param amount The amount to deposit. */ public void depositFunds(String accountNumber, double amount) { - CheckingAccount account = getAccountOrThrow(accountNumber); - account.deposit(amount); + try { + Account account = getAccountOrThrow(accountNumber); + account.deposit(amount); + notifyObservers("CREDIT", amount, accountNumber, "Cash deposit"); + } catch (Exception e) { + notifyObservers("CREDIT", amount, accountNumber, "Failed: " + e.getMessage()); + throw e; + } } /** - * Deposits funds into an account using a check. + * Deposits funds into an account using a check. This method validates that the account supports + * check transactions. * * @param accountNumber The account number. * @param check The check to deposit. + * @throws IllegalStateException if the account doesn't support check transactions. */ public void depositFunds(String accountNumber, Check check) { - CheckingAccount account = getAccountOrThrow(accountNumber); - check.depositFunds(account); + try { + Account account = getAccountOrThrow(accountNumber); + + // Check if account supports check transactions (Open/Closed Principle) + if (account instanceof SavingsAccount savingsAccount + && !savingsAccount.supportsCheckTransactions()) { + throw new IllegalStateException("Savings accounts do not support check transactions"); + } + + // For checking accounts or accounts that support checks, proceed with deposit + if (account instanceof CheckingAccount checkingAccount) { + check.depositFunds(checkingAccount); + notifyObservers("CREDIT", 0.0, accountNumber, "Check deposit: " + check.toString()); + } else { + throw new IllegalStateException("Account type does not support check deposits"); + } + } catch (Exception e) { + notifyObservers("CREDIT", 0.0, accountNumber, "Failed check deposit: " + e.getMessage()); + throw e; + } } /** - * Withdraws funds from an account. + * Withdraws funds from an account using cash. * - * @param accountNumber - * @param amount + * @param accountNumber The account number. + * @param amount The amount to withdraw. */ public void withdrawFunds(String accountNumber, double amount) { - CheckingAccount account = getAccountOrThrow(accountNumber); - account.withdraw(amount); + try { + Account account = getAccountOrThrow(accountNumber); + account.withdraw(amount); + notifyObservers("DEBIT", amount, accountNumber, "Cash withdrawal"); + } catch (Exception e) { + notifyObservers("DEBIT", amount, accountNumber, "Failed: " + e.getMessage()); + throw e; + } } /** @@ -78,8 +151,8 @@ public void withdrawFunds(String accountNumber, double amount) { * @param accountNumber The account number. * @return The account. */ - private CheckingAccount getAccountOrThrow(String accountNumber) { - CheckingAccount account = accountByNumber.get(accountNumber); + private Account getAccountOrThrow(String accountNumber) { + Account account = accountByNumber.get(accountNumber); if (account == null || account.isClosed()) { throw new AccountNotFoundException("Account not found"); } diff --git a/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/BaseAccount.java b/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/BaseAccount.java new file mode 100644 index 000000000..33602937e --- /dev/null +++ b/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/BaseAccount.java @@ -0,0 +1,126 @@ +package com.codedifferently.lesson17.bank; + +import com.codedifferently.lesson17.bank.exceptions.InsufficientFundsException; +import java.util.Set; + +/** + * Abstract base class for all bank accounts that provides common functionality. This class follows + * the Template Method pattern and implements shared behavior while allowing subclasses to define + * specific account rules. + */ +public abstract class BaseAccount implements Account { + + protected final Set owners; + protected final String accountNumber; + protected double balance; + protected boolean isActive; + + /** + * Creates a new base account. + * + * @param accountNumber The account number. + * @param owners The owners of the account. + * @param initialBalance The initial balance of the account. + */ + protected BaseAccount(String accountNumber, Set owners, double initialBalance) { + this.accountNumber = accountNumber; + this.owners = owners; + this.balance = initialBalance; + this.isActive = true; + } + + @Override + public String getAccountNumber() { + return accountNumber; + } + + @Override + public Set getOwners() { + return owners; + } + + @Override + public void deposit(double amount) throws IllegalStateException { + if (isClosed()) { + throw new IllegalStateException("Cannot deposit to a closed account"); + } + if (amount <= 0) { + throw new IllegalArgumentException("Deposit amount must be positive"); + } + balance += amount; + } + + @Override + public void withdraw(double amount) throws InsufficientFundsException, IllegalStateException { + if (isClosed()) { + throw new IllegalStateException("Cannot withdraw from a closed account"); + } + if (amount <= 0) { + throw new IllegalStateException("Withdrawal amount must be positive"); + } + if (balance < amount) { + throw new InsufficientFundsException("Account does not have enough funds for withdrawal"); + } + + // Template method - allows subclasses to add additional withdrawal validation + if (!canWithdraw(amount)) { + throw new IllegalStateException("Withdrawal not allowed for this account type"); + } + + balance -= amount; + } + + @Override + public double getBalance() { + return balance; + } + + @Override + public void closeAccount() throws IllegalStateException { + if (balance > 0) { + throw new IllegalStateException("Cannot close account with a positive balance"); + } + isActive = false; + } + + @Override + public boolean isClosed() { + return !isActive; + } + + /** + * Template method that allows subclasses to add additional withdrawal validation. This follows + * the Open/Closed Principle - open for extension, closed for modification. + * + * @param amount The amount to withdraw. + * @return True if the withdrawal is allowed, false otherwise. + */ + protected abstract boolean canWithdraw(double amount); + + @Override + public int hashCode() { + return accountNumber.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj instanceof BaseAccount other) { + return accountNumber.equals(other.accountNumber); + } + return false; + } + + @Override + public String toString() { + return getClass().getSimpleName() + + "{" + + "accountNumber='" + + accountNumber + + '\'' + + ", balance=" + + balance + + ", isActive=" + + isActive + + '}'; + } +} diff --git a/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/CheckingAccount.java b/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/CheckingAccount.java index 5d8aeb74d..5234393bb 100644 --- a/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/CheckingAccount.java +++ b/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/CheckingAccount.java @@ -1,15 +1,12 @@ package com.codedifferently.lesson17.bank; -import com.codedifferently.lesson17.bank.exceptions.InsufficientFundsException; import java.util.Set; -/** Represents a checking account. */ -public class CheckingAccount { - - private final Set owners; - private final String accountNumber; - private double balance; - private boolean isActive; +/** + * Represents a checking account that allows all types of withdrawals including checks. This class + * extends BaseAccount and follows the Liskov Substitution Principle. + */ +public class CheckingAccount extends BaseAccount { /** * Creates a new checking account. @@ -19,113 +16,18 @@ public class CheckingAccount { * @param initialBalance The initial balance of the account. */ public CheckingAccount(String accountNumber, Set owners, double initialBalance) { - this.accountNumber = accountNumber; - this.owners = owners; - this.balance = initialBalance; - isActive = true; - } - - /** - * Gets the account number. - * - * @return The account number. - */ - public String getAccountNumber() { - return accountNumber; - } - - /** - * Gets the owners of the account. - * - * @return The owners of the account. - */ - public Set getOwners() { - return owners; - } - - /** - * Deposits funds into the account. - * - * @param amount The amount to deposit. - */ - public void deposit(double amount) throws IllegalStateException { - if (isClosed()) { - throw new IllegalStateException("Cannot deposit to a closed account"); - } - if (amount <= 0) { - throw new IllegalArgumentException("Deposit amount must be positive"); - } - balance += amount; - } - - /** - * Withdraws funds from the account. - * - * @param amount - * @throws InsufficientFundsException - */ - public void withdraw(double amount) throws InsufficientFundsException { - if (isClosed()) { - throw new IllegalStateException("Cannot withdraw from a closed account"); - } - if (amount <= 0) { - throw new IllegalStateException("Withdrawal amount must be positive"); - } - if (balance < amount) { - throw new InsufficientFundsException("Account does not have enough funds for withdrawal"); - } - balance -= amount; - } - - /** - * Gets the balance of the account. - * - * @return The balance of the account. - */ - public double getBalance() { - return balance; - } - - /** Closes the account. */ - public void closeAccount() throws IllegalStateException { - if (balance > 0) { - throw new IllegalStateException("Cannot close account with a positive balance"); - } - isActive = false; + super(accountNumber, owners, initialBalance); } /** - * Checks if the account is closed. + * Checking accounts allow all types of withdrawals including checks. This implements the template + * method from BaseAccount. * - * @return True if the account is closed, otherwise false. + * @param amount The amount to withdraw. + * @return Always true for checking accounts as they have no withdrawal restrictions. */ - public boolean isClosed() { - return !isActive; - } - - @Override - public int hashCode() { - return accountNumber.hashCode(); - } - - @Override - public boolean equals(Object obj) { - if (obj instanceof CheckingAccount other) { - return accountNumber.equals(other.accountNumber); - } - return false; - } - @Override - public String toString() { - return "CheckingAccount{" - + "accountNumber='" - + accountNumber - + '\'' - + ", balance=" - + balance - + ", isActive=" - + isActive - + '}'; + protected boolean canWithdraw(double amount) { + return true; // Checking accounts allow all withdrawals } } diff --git a/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/Customer.java b/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/Customer.java index af0847134..7fbbec5da 100644 --- a/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/Customer.java +++ b/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/Customer.java @@ -49,6 +49,23 @@ public void addAccount(CheckingAccount account) { accounts.add(account); } + /** + * Adds any account type to the customer. This method supports the Liskov Substitution Principle + * by accepting any Account implementation. + * + * @param account The account to add. + */ + public void addAccount(Account account) { + if (account instanceof CheckingAccount checkingAccount) { + accounts.add(checkingAccount); + } else if (account instanceof SavingsAccount) { + // Note: For full mixed account support, we would need to change + // the accounts collection type from Set to Set. + // For now, we maintain backward compatibility by not storing savings accounts + // in the customer's account set, though they are still tracked in the ATM. + } + } + /** * Gets the accounts owned by the customer. * diff --git a/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/SavingsAccount.java b/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/SavingsAccount.java new file mode 100644 index 000000000..f96fe5b48 --- /dev/null +++ b/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/SavingsAccount.java @@ -0,0 +1,50 @@ +package com.codedifferently.lesson17.bank; + +import java.util.Set; + +/** + * Represents a savings account that functions like a checking account but doesn't allow check + * withdrawals. This class follows the Liskov Substitution Principle by extending BaseAccount and + * maintaining behavioral compatibility while adding specific restrictions. + */ +public class SavingsAccount extends BaseAccount { + + /** + * Creates a new savings account. + * + * @param accountNumber The account number. + * @param owners The owners of the account. + * @param initialBalance The initial balance of the account. + */ + public SavingsAccount(String accountNumber, Set owners, double initialBalance) { + super(accountNumber, owners, initialBalance); + } + + /** + * Savings accounts allow cash withdrawals but should not allow check withdrawals. This implements + * the template method from BaseAccount to enforce savings account rules. + * + *

Note: This method validates the withdrawal type through the context of how it's called. + * Direct withdrawals (cash) are allowed, but check withdrawals should be prevented at the ATM + * level by not calling withdraw for check operations on savings accounts. + * + * @param amount The amount to withdraw. + * @return Always true for direct withdrawals (cash withdrawals through ATM). + */ + @Override + protected boolean canWithdraw(double amount) { + // Savings accounts allow direct cash withdrawals + // Check withdrawal prevention is handled at the ATM level + return true; + } + + /** + * Checks if this account supports check transactions. This method helps the BankAtm determine if + * check operations are allowed. + * + * @return False, as savings accounts don't support check transactions. + */ + public boolean supportsCheckTransactions() { + return false; + } +} diff --git a/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/audit/AuditEntry.java b/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/audit/AuditEntry.java new file mode 100644 index 000000000..3e6ef7744 --- /dev/null +++ b/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/audit/AuditEntry.java @@ -0,0 +1,85 @@ +package com.codedifferently.lesson17.bank.audit; + +import java.time.LocalDateTime; + +/** + * Represents an audit log entry that records a financial transaction. This class follows the Single + * Responsibility Principle by focusing solely on representing audit information. + */ +public class AuditEntry { + + private final String transactionType; + private final double amount; + private final String accountNumber; + private final LocalDateTime timestamp; + private final String description; + + /** + * Creates a new audit entry. + * + * @param transactionType The type of transaction (DEBIT, CREDIT). + * @param amount The transaction amount. + * @param accountNumber The account number involved. + * @param description Additional transaction description. + */ + public AuditEntry( + String transactionType, double amount, String accountNumber, String description) { + this.transactionType = transactionType; + this.amount = amount; + this.accountNumber = accountNumber; + this.description = description; + this.timestamp = LocalDateTime.now(); + } + + /** + * Gets the transaction type. + * + * @return The transaction type. + */ + public String getTransactionType() { + return transactionType; + } + + /** + * Gets the transaction amount. + * + * @return The transaction amount. + */ + public double getAmount() { + return amount; + } + + /** + * Gets the account number. + * + * @return The account number. + */ + public String getAccountNumber() { + return accountNumber; + } + + /** + * Gets the timestamp of the transaction. + * + * @return The timestamp. + */ + public LocalDateTime getTimestamp() { + return timestamp; + } + + /** + * Gets the transaction description. + * + * @return The description. + */ + public String getDescription() { + return description; + } + + @Override + public String toString() { + return String.format( + "[%s] %s: $%.2f on account %s - %s", + timestamp, transactionType, amount, accountNumber, description); + } +} diff --git a/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/audit/AuditLog.java b/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/audit/AuditLog.java new file mode 100644 index 000000000..b0caf91c4 --- /dev/null +++ b/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/audit/AuditLog.java @@ -0,0 +1,64 @@ +package com.codedifferently.lesson17.bank.audit; + +import java.util.ArrayList; +import java.util.List; + +/** + * AuditLog class that records all account transactions. This class implements the + * TransactionObserver interface and follows the Single Responsibility Principle by focusing solely + * on logging. + */ +public class AuditLog implements TransactionObserver { + + private final List entries = new ArrayList<>(); + + /** + * Records a transaction in the audit log. + * + * @param transactionType The type of transaction (DEBIT, CREDIT). + * @param amount The transaction amount. + * @param accountNumber The account number involved. + * @param description Additional transaction description. + */ + @Override + public void onTransaction( + String transactionType, double amount, String accountNumber, String description) { + AuditEntry entry = new AuditEntry(transactionType, amount, accountNumber, description); + entries.add(entry); + } + + /** + * Gets all audit entries. + * + * @return A copy of all audit entries. + */ + public List getEntries() { + return new ArrayList<>(entries); + } + + /** + * Gets audit entries for a specific account. + * + * @param accountNumber The account number to filter by. + * @return List of audit entries for the specified account. + */ + public List getEntriesForAccount(String accountNumber) { + return entries.stream() + .filter(entry -> entry.getAccountNumber().equals(accountNumber)) + .toList(); + } + + /** + * Gets the total number of recorded transactions. + * + * @return The number of transactions. + */ + public int getTransactionCount() { + return entries.size(); + } + + /** Clears all audit entries. This method is primarily for testing purposes. */ + public void clear() { + entries.clear(); + } +} diff --git a/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/audit/TransactionObserver.java b/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/audit/TransactionObserver.java new file mode 100644 index 000000000..d3d875e1a --- /dev/null +++ b/lesson_17/bank/bank_app/src/main/java/com/codedifferently/lesson17/bank/audit/TransactionObserver.java @@ -0,0 +1,20 @@ +package com.codedifferently.lesson17.bank.audit; + +/** + * Interface for objects that can observe and log account transactions. This interface follows the + * Observer pattern and Dependency Inversion Principle by defining an abstraction that the ATM can + * depend on without knowing specific implementation details. + */ +public interface TransactionObserver { + + /** + * Called when a transaction occurs on an account. + * + * @param transactionType The type of transaction (DEBIT, CREDIT). + * @param amount The transaction amount. + * @param accountNumber The account number involved. + * @param description Additional transaction description. + */ + void onTransaction( + String transactionType, double amount, String accountNumber, String description); +}