Unsolvable Python Puzzle

Main Question (100 Marks)

Problem Statement:
Write a Python program that simulates a banking system. Your program should support the following operations:

  1. Create Account
    • Users should be able to create an account with a unique account number, name, and initial balance. The account number should be a randomly generated 8-digit number.
    • Ensure that account numbers are unique.
  2. Deposit Money
    • Allow users to deposit money into their account. The program should update the account balance accordingly.
  3. Withdraw Money
    • Allow users to withdraw money from their account. Ensure that the account has sufficient funds before allowing the withdrawal. The program should update the account balance accordingly.
  4. Check Balance
    • Allow users to check their current account balance by providing their account number.
  5. Transaction History
    • For each account, store a transaction history (date, type of transaction, and amount). Allow users to view their transaction history.
  6. Transfer Money
    • Allow users to transfer money from one account to another. Ensure that the sender’s account has sufficient funds before allowing the transfer. Update the balances for both accounts accordingly, and record the transaction in both accounts’ histories.

Requirements:

  • Use classes to model the bank account and the banking system.
  • Implement error handling for invalid inputs (e.g., negative deposits/withdrawals, invalid account numbers).
  • Include meaningful comments and docstrings to explain your code.
  • Ensure the program is interactive, allowing users to choose options from a menu until they choose to exit.

Bonus Question (10 Marks)

Problem Statement:
Enhance the banking system by implementing the following feature:

  • Interest Calculation
    Add a method to calculate interest on the account balance. The interest rate should be a constant (e.g., 3% annually) and should be compounded monthly. Provide an option for the user to apply the interest to their account, updating the balance accordingly.

Requirements:

  • Implement the interest calculation accurately using the formula for compound interest.
  • Ensure that the interest can be applied multiple times, reflecting realistic monthly updates.
  • Add the interest applied as a transaction in the transaction history.

Scoring Criteria:

  • Correct implementation of each feature (80 Marks)
  • Code structure, clarity, and readability (10 Marks)
  • Proper error handling and validation (10 Marks)

Bonus:

  • Correct implementation of interest calculation and integration into the banking system (10 Marks)

So far the question is easy, but, the time limit screws everyone. 45 minutes to solve this question. (Note this is the simplified version of the problem, the original problem is 4 pages long written in paragraphs and is linked here: failed to link website)

To tackle this problem, let’s break it down into manageable steps, focusing on the requirements for both the main question and the bonus.


1. Class Design:

  • BankAccount: This class will represent an individual bank account.
    • Attributes:
      • account_number: A unique, randomly generated 8-digit number.
      • name: The name of the account holder.
      • balance: The current balance of the account.
      • transaction_history: A list to store the transaction history.
    • Methods:
      • deposit(amount): Adds the specified amount to the balance and records the transaction.
      • withdraw(amount): Deducts the specified amount if there are sufficient funds and records the transaction.
      • check_balance(): Returns the current balance.
      • view_transaction_history(): Returns the transaction history.
      • transfer(amount, target_account): Transfers money from this account to another and records the transaction for both accounts.
      • apply_interest(): Calculates and applies monthly interest to the balance and records it as a transaction (for the bonus part).
  • BankingSystem: This class will manage multiple bank accounts.
    • Attributes:
      • accounts: A dictionary or list to store all the accounts.
    • Methods:
      • create_account(name, initial_balance): Creates a new account and ensures unique account numbers.
      • get_account(account_number): Retrieves an account using the account number.
      • validate_account_number(account_number): Checks if an account number is valid.

2. Core Functionalities:

  • Create Account:
    • Ensure account numbers are unique.
    • Store the account in a data structure.
  • Deposit Money:
    • Validate the amount (should be positive).
    • Update the balance and transaction history.
  • Withdraw Money:
    • Ensure sufficient funds are available.
    • Validate the amount (should be positive).
    • Update the balance and transaction history.
  • Check Balance:
    • Simply return the balance of the account.
  • Transaction History:
    • Store date, type of transaction, and amount for each operation.
    • Allow viewing this history.
  • Transfer Money:
    • Validate sufficient funds in the sender’s account.
    • Update balances for both accounts.
    • Record the transaction in both accounts’ histories.

3. Error Handling:

  • Handle invalid inputs, such as negative amounts, non-existent account numbers, and withdrawals that exceed the balance.

4. Menu-Driven Interface:

  • The program should continuously offer a menu of operations (create account, deposit, withdraw, etc.) until the user chooses to exit.

5. Bonus: Interest Calculation:

  • Interest Calculation:
    • Use the formula for compound interest: A= P \left(1 + \frac{r}{n}\right)^{nt}A=P(1+nr​)nt where P is the principal, r is the annual interest rate, n is the number of times interest applied per time period, and t is the time the money is invested or borrowed for.
    • In this case, the interest is compounded monthly, so n=12n = 12n=12.
    • Provide an option for the user to apply the interest to their account, update the balance, and log the transaction.

6. Implementation Plan:

  • Step 1: Define the BankAccount class with the required attributes and methods.
  • Step 2: Define the BankingSystem class to manage multiple accounts.
  • Step 3: Implement the core functionalities (deposit, withdraw, etc.).
  • Step 4: Implement error handling.
  • Step 5: Create the menu-driven interface.
  • Step 6: Implement the bonus feature for interest calculation.

This structure ensures the problem is tackled methodically, addressing each requirement systematically. The focus should be on writing clear, concise code with appropriate comments and docstrings to explain the logic.

Idea 1: Simple Banking System in Python

import random
import datetime

class BankAccount:
    """Represents a single bank account."""
    
    def __init__(self, name, initial_balance):
        self.account_number = self._generate_account_number()
        self.name = name
        self.balance = initial_balance
        self.transaction_history = []
        self._add_transaction('Account Created', initial_balance)

    def _generate_account_number(self):
        """Generates a unique 8-digit account number."""
        return random.randint(10000000, 99999999)

    def _add_transaction(self, transaction_type, amount):
        """Records a transaction in the transaction history."""
        self.transaction_history.append({
            'date': datetime.datetime.now(),
            'type': transaction_type,
            'amount': amount
        })

    def deposit(self, amount):
        """Deposits money into the account."""
        if amount <= 0:
            raise ValueError("Deposit amount must be positive.")
        self.balance += amount
        self._add_transaction('Deposit', amount)
        return f"Deposited ${amount:.2f}. New Balance: ${self.balance:.2f}"

    def withdraw(self, amount):
        """Withdraws money from the account if sufficient funds are available."""
        if amount <= 0:
            raise ValueError("Withdrawal amount must be positive.")
        if amount > self.balance:
            raise ValueError("Insufficient funds.")
        self.balance -= amount
        self._add_transaction('Withdrawal', amount)
        return f"Withdrew ${amount:.2f}. New Balance: ${self.balance:.2f}"

    def check_balance(self):
        """Returns the current balance of the account."""
        return f"Account Balance: ${self.balance:.2f}"

    def view_transaction_history(self):
        """Returns the transaction history of the account."""
        if not self.transaction_history:
            return "No transactions found."
        history = []
        for transaction in self.transaction_history:
            history.append(f"{transaction['date']} - {transaction['type']}: ${transaction['amount']:.2f}")
        return "\n".join(history)

    def transfer(self, amount, target_account):
        """Transfers money from this account to another account."""
        if amount <= 0:
            raise ValueError("Transfer amount must be positive.")
        if amount > self.balance:
            raise ValueError("Insufficient funds.")
        self.withdraw(amount)
        target_account.deposit(amount)
        self._add_transaction('Transfer Out', amount)
        target_account._add_transaction('Transfer In', amount)
        return f"Transferred ${amount:.2f} to account {target_account.account_number}"

    def apply_interest(self, annual_rate=0.03):
        """Applies monthly compound interest to the account balance."""
        monthly_rate = annual_rate / 12
        interest = self.balance * monthly_rate
        self.balance += interest
        self._add_transaction('Interest Applied', interest)
        return f"Interest of ${interest:.2f} applied. New Balance: ${self.balance:.2f}"

class BankingSystem:
    """Manages multiple bank accounts."""
    
    def __init__(self):
        self.accounts = {}

    def create_account(self, name, initial_balance):
        """Creates a new account with a unique account number."""
        if initial_balance < 0:
            raise ValueError("Initial balance must be non-negative.")
        account = BankAccount(name, initial_balance)
        self.accounts[account.account_number] = account
        return f"Account created with Account Number: {account.account_number}"

    def get_account(self, account_number):
        """Retrieves an account by account number."""
        if account_number in self.accounts:
            return self.accounts[account_number]
        else:
            raise ValueError("Invalid account number.")

def main():
    banking_system = BankingSystem()
    
    while True:
        print("\nBanking System Menu")
        print("1. Create Account")
        print("2. Deposit Money")
        print("3. Withdraw Money")
        print("4. Check Balance")
        print("5. View Transaction History")
        print("6. Transfer Money")
        print("7. Apply Interest")
        print("8. Exit")
        choice = input("Choose an option: ")
        
        try:
            if choice == '1':
                name = input("Enter account holder name: ")
                initial_balance = float(input("Enter initial balance: "))
                print(banking_system.create_account(name, initial_balance))

            elif choice == '2':
                account_number = int(input("Enter account number: "))
                amount = float(input("Enter deposit amount: "))
                account = banking_system.get_account(account_number)
                print(account.deposit(amount))

            elif choice == '3':
                account_number = int(input("Enter account number: "))
                amount = float(input("Enter withdrawal amount: "))
                account = banking_system.get_account(account_number)
                print(account.withdraw(amount))

            elif choice == '4':
                account_number = int(input("Enter account number: "))
                account = banking_system.get_account(account_number)
                print(account.check_balance())

            elif choice == '5':
                account_number = int(input("Enter account number: "))
                account = banking_system.get_account(account_number)
                print(account.view_transaction_history())

            elif choice == '6':
                sender_account_number = int(input("Enter sender's account number: "))
                receiver_account_number = int(input("Enter receiver's account number: "))
                amount = float(input("Enter transfer amount: "))
                sender_account = banking_system.get_account(sender_account_number)
                receiver_account = banking_system.get_account(receiver_account_number)
                print(sender_account.transfer(amount, receiver_account))

            elif choice == '7':
                account_number = int(input("Enter account number: "))
                account = banking_system.get_account(account_number)
                print(account.apply_interest())

            elif choice == '8':
                print("Exiting Banking System. Have a great day!")
                break

            else:
                print("Invalid option. Please try again.")
                
        except ValueError as e:
            print(f"Error: {e}")

if __name__ == "__main__":
    main()

Merits of the Solution:

  1. Object-Oriented Design:
    • The program effectively uses classes (BankAccount and BankingSystem) to model the bank account system, ensuring a clean and modular design.
    • Each class has a well-defined purpose, with clear separation of concerns between account operations and system-wide management.
  2. Unique Account Numbers:
    • The account number generation ensures uniqueness by using a random 8-digit number. Though not completely foolproof for large-scale systems, it is sufficient for this application.
  3. Transaction History:
    • Each account maintains a detailed transaction history, recording the date, type of transaction, and amount, which provides transparency and traceability for all transactions.
  4. Error Handling:
    • The program includes robust error handling for invalid inputs (e.g., negative amounts, insufficient funds, invalid account numbers), improving the user experience by preventing and explaining errors.
  5. Interactive and User-Friendly:
    • The menu-driven interface makes the program user-friendly and interactive, allowing users to perform various banking operations until they choose to exit.
  6. Interest Calculation (Bonus):
    • The interest calculation feature accurately implements monthly compound interest, reflecting realistic financial scenarios. Interest is applied and recorded as a transaction, further enhancing the realism of the system.
  7. Code Clarity and Documentation:
    • The code includes meaningful comments and docstrings, making it easy to understand and maintain. The logical structure is clear, with each function and method doing one thing well.
  8. Scalability and Extensibility:
    • The design is flexible enough to allow for easy expansion of features, such as adding new types of transactions or supporting more complex interest calculations. The program can be extended without major changes to the existing structure.

Demerits of the Solution

  1. Account Number Generation:
    • Potential for Collision: The method for generating account numbers uses random 8-digit numbers. While the likelihood of collision (two accounts having the same number) is low, it’s not impossible. In a real banking system, this approach could lead to issues. A better approach might involve checking for existing account numbers before assigning a new one or using a more sophisticated method like UUIDs (Universally Unique Identifiers).
  2. Scalability Concerns:
    • In-Memory Storage: The BankingSystem class stores accounts in an in-memory dictionary. This approach limits scalability since it wouldn’t work well for a large number of accounts or in a real-world scenario where data persistence (e.g., using a database) is required. Additionally, the data would be lost if the program is restarted, which isn’t practical for a banking system.
  3. Interest Calculation:
    • Simplicity of the Interest Calculation: The interest is calculated and applied only when the user explicitly chooses to do so. In reality, interest accrues over time automatically and should be calculated periodically without user intervention. This design could be improved by automating the interest application process on a monthly basis.
  4. Limited Input Validation:
    • Assumption of Valid Inputs: The program expects the user to enter valid data types (e.g., integers for account numbers, floats for amounts). However, it doesn’t handle cases where the user might enter invalid data types (e.g., entering a string where a number is expected). Adding more robust input validation would improve the program’s reliability.
  5. No Security Measures:
    • Lack of Authentication: The program does not include any authentication or security features. In a real banking system, there would be a need for secure login, encryption of sensitive data, and protection against unauthorized access. While this may be outside the scope of the problem, it’s a significant omission for a real-world application.
  6. User Experience:
    • Basic Interaction: The menu-driven interface is functional but rudimentary. It lacks advanced features like input validation feedback, confirmation messages for critical operations (e.g., transfers), and user-friendly prompts. Additionally, there’s no way to view all account holders or their balances unless you know the account number, limiting the functionality for a user managing multiple accounts.
  7. No Data Persistence:
    • Volatile Data: Since all data is stored in memory, all accounts, transactions, and balances are lost when the program exits. In a real-world application, data persistence would be necessary to ensure that all account information is saved between sessions.
  8. Lack of Testing:
    • No Unit Tests: The solution does not include any unit tests or automated testing. Testing is crucial to ensure the reliability and correctness of each feature. Implementing tests would make the code more robust and easier to maintain, especially as new features are added.
  9. Hardcoded Interest Rate:
    • Lack of Flexibility: The interest rate is hardcoded within the apply_interest() method. A more flexible approach would allow different accounts to have different interest rates or allow the user to set the rate dynamically. This would make the system more adaptable to different financial products.
  10. No Account Deletion or Closure:
    • Limited Account Management: The system does not provide a way to close or delete an account, which would be necessary in a real banking system. This limits the functionality and makes it less realistic for long-term use.

As you can see the demerits far outnumber the merits, hence, this solution is not possible.

Idea 2: A more complicated approach

import random
import datetime
import json
import os

class BankAccount:
    """Represents a single bank account."""

    def __init__(self, name, initial_balance, pin, interest_rate=0.03):
        self.account_number = self._generate_account_number()
        self.name = name
        self.balance = initial_balance
        self.pin = pin
        self.interest_rate = interest_rate
        self.transaction_history = []
        self.creation_date = datetime.datetime.now()
        self._add_transaction('Account Created', initial_balance)

    def _generate_account_number(self):
        """Generates a unique 8-digit account number."""
        while True:
            account_number = random.randint(10000000, 99999999)
            if not BankingSystem.is_account_number_taken(account_number):
                return account_number

    def _add_transaction(self, transaction_type, amount):
        """Records a transaction in the transaction history."""
        self.transaction_history.append({
            'date': datetime.datetime.now(),
            'type': transaction_type,
            'amount': amount
        })

    def deposit(self, amount):
        """Deposits money into the account."""
        if amount <= 0:
            raise ValueError("Deposit amount must be positive.")
        self.balance += amount
        self._add_transaction('Deposit', amount)
        return f"Deposited ${amount:.2f}. New Balance: ${self.balance:.2f}"

    def withdraw(self, amount):
        """Withdraws money from the account if sufficient funds are available."""
        if amount <= 0:
            raise ValueError("Withdrawal amount must be positive.")
        if amount > self.balance:
            raise ValueError("Insufficient funds.")
        self.balance -= amount
        self._add_transaction('Withdrawal', amount)
        return f"Withdrew ${amount:.2f}. New Balance: ${self.balance:.2f}"

    def check_balance(self):
        """Returns the current balance of the account."""
        return f"Account Balance: ${self.balance:.2f}"

    def view_transaction_history(self):
        """Returns the transaction history of the account."""
        if not self.transaction_history:
            return "No transactions found."
        history = []
        for transaction in self.transaction_history:
            history.append(f"{transaction['date']} - {transaction['type']}: ${transaction['amount']:.2f}")
        return "\n".join(history)

    def transfer(self, amount, target_account):
        """Transfers money from this account to another account."""
        if amount <= 0:
            raise ValueError("Transfer amount must be positive.")
        if amount > self.balance:
            raise ValueError("Insufficient funds.")
        self.withdraw(amount)
        target_account.deposit(amount)
        self._add_transaction('Transfer Out', amount)
        target_account._add_transaction('Transfer In', amount)
        return f"Transferred ${amount:.2f} to account {target_account.account_number}"

    def apply_interest(self):
        """Applies monthly compound interest to the account balance."""
        monthly_rate = self.interest_rate / 12
        interest = self.balance * monthly_rate
        self.balance += interest
        self._add_transaction('Interest Applied', interest)
        return f"Interest of ${interest:.2f} applied. New Balance: ${self.balance:.2f}"

    def close_account(self):
        """Closes the account by setting the balance to zero and clearing the transaction history."""
        if self.balance > 0:
            raise ValueError("Account balance must be zero to close the account.")
        self.transaction_history.clear()
        return "Account closed successfully."

class BankingSystem:
    """Manages multiple bank accounts."""
    
    accounts_file = "accounts.json"
    
    def __init__(self):
        self.accounts = self._load_accounts()

    @staticmethod
    def is_account_number_taken(account_number):
        """Checks if the account number is already in use."""
        accounts = BankingSystem._load_accounts()
        return account_number in accounts

    @staticmethod
    def _load_accounts():
        """Loads accounts from a file."""
        if os.path.exists(BankingSystem.accounts_file):
            with open(BankingSystem.accounts_file, 'r') as file:
                return json.load(file)
        return {}

    def _save_accounts(self):
        """Saves accounts to a file."""
        with open(BankingSystem.accounts_file, 'w') as file:
            json.dump(self.accounts, file)

    def create_account(self, name, initial_balance, pin, interest_rate=0.03):
        """Creates a new account with a unique account number."""
        if initial_balance < 0:
            raise ValueError("Initial balance must be non-negative.")
        account = BankAccount(name, initial_balance, pin, interest_rate)
        self.accounts[account.account_number] = {
            "name": account.name,
            "balance": account.balance,
            "pin": account.pin,
            "interest_rate": account.interest_rate,
            "transaction_history": account.transaction_history,
            "creation_date": account.creation_date.isoformat()
        }
        self._save_accounts()
        return f"Account created with Account Number: {account.account_number}"

    def get_account(self, account_number, pin):
        """Retrieves an account by account number after verifying the pin."""
        account_info = self.accounts.get(str(account_number))
        if account_info and account_info["pin"] == pin:
            account = BankAccount(
                name=account_info["name"],
                initial_balance=account_info["balance"],
                pin=account_info["pin"],
                interest_rate=account_info["interest_rate"]
            )
            account.transaction_history = account_info["transaction_history"]
            account.creation_date = datetime.datetime.fromisoformat(account_info["creation_date"])
            return account
        else:
            raise ValueError("Invalid account number or PIN.")

    def delete_account(self, account_number, pin):
        """Deletes an account by account number after verifying the pin."""
        account = self.get_account(account_number, pin)
        if account.balance > 0:
            raise ValueError("Account balance must be zero before deletion.")
        del self.accounts[str(account_number)]
        self._save_accounts()
        return "Account deleted successfully."

def main():
    banking_system = BankingSystem()
    
    while True:
        print("\nBanking System Menu")
        print("1. Create Account")
        print("2. Deposit Money")
        print("3. Withdraw Money")
        print("4. Check Balance")
        print("5. View Transaction History")
        print("6. Transfer Money")
        print("7. Apply Interest")
        print("8. Close Account")
        print("9. Exit")
        choice = input("Choose an option: ")
        
        try:
            if choice == '1':
                name = input("Enter account holder name: ")
                initial_balance = float(input("Enter initial balance: "))
                pin = input("Set a 4-digit PIN: ")
                interest_rate = float(input("Enter interest rate (default is 0.03): ") or 0.03)
                print(banking_system.create_account(name, initial_balance, pin, interest_rate))

            elif choice == '2':
                account_number = int(input("Enter account number: "))
                pin = input("Enter your PIN: ")
                amount = float(input("Enter deposit amount: "))
                account = banking_system.get_account(account_number, pin)
                print(account.deposit(amount))

            elif choice == '3':
                account_number = int(input("Enter account number: "))
                pin = input("Enter your PIN: ")
                amount = float(input("Enter withdrawal amount: "))
                account = banking_system.get_account(account_number, pin)
                print(account.withdraw(amount))

            elif choice == '4':
                account_number = int(input("Enter account number: "))
                pin = input("Enter your PIN: ")
                account = banking_system.get_account(account_number, pin)
                print(account.check_balance())

            elif choice == '5':
                account_number = int(input("Enter account number: "))
                pin = input("Enter your PIN: ")
                account = banking_system.get_account(account_number, pin)
                print(account.view_transaction_history())

            elif choice == '6':
                sender_account_number = int(input("Enter sender's account number: "))
                pin = input("Enter your PIN: ")
                receiver_account_number = int(input("Enter receiver's account number: "))
                amount = float(input("Enter transfer amount: "))
                sender_account = banking_system.get_account(sender_account_number, pin)
                receiver_account = banking_system.get_account(receiver_account_number, pin)
                print(sender_account.transfer(amount, receiver_account))

            elif choice == '7':
                account_number = int(input("Enter account number: "))
                pin = input("Enter your PIN: ")
                account = banking_system.get_account(account_number, pin)
                print(account.apply_interest())

            elif choice == '8':
                account_number = int(input("Enter account number: "))
                pin = input("Enter your PIN: ")
                confirmation = input("Are you sure you want to close this account? (yes/no): ")
                if confirmation.lower() == 'yes':
                    account = banking_system.get_account(account_number, pin)
                    print(account.close_account())
                    print(banking_system.delete_account(account_number, pin))

            elif choice == '9':
                print("Exiting Banking System. Have a great day!")
                break

            else:
                print("Invalid option. Please try again.")
                
        except ValueError as e:
            print(f"Error: {e}")

if __name__ == "__main__":
    main()

Key Updates:

  1. Account Number Generation: Ensures uniqueness by checking existing account numbers.
  2. Data Persistence: Implements basic file-based persistence for account data.
  3. Automated Interest Calculation: Still manually triggered, but with added flexibility for interest rate.
  4. Input Validation: Added more checks for user inputs.
  5. Basic Security: Added a simple PIN-based authentication.
  6. Account Management: Added functionality to close and delete accounts.
  7. User Experience: Improved the interaction flow and added confirmations for critical operations.

Problems in the code:

  1. Account Number Collision Handling:
    • Current State: The _generate_account_number method ensures uniqueness but could become inefficient if the number of accounts grows significantly.
    • Improvement: Use a more robust method like UUIDs or a centralized system to manage account numbers.
  2. Scalability:
    • Current State: The program uses file-based persistence, which is not suitable for large-scale systems or concurrent access.
    • Improvement: Implement a database system (e.g., SQLite, PostgreSQL) to handle data persistence and improve scalability.
  3. Interest Calculation:
    • Current State: Interest is applied manually and is still simplified.
    • Improvement: Automate the interest application process by adding a background scheduler (e.g., using the schedule library) to apply interest periodically.
  4. Security:
    • Current State: Simple PIN-based authentication is implemented, but it’s not secure enough for a real banking application.
    • Improvement: Use more secure authentication methods such as hashed passwords, multi-factor authentication, and encryption for sensitive data.
  5. Concurrency:
    • Current State: The system does not handle concurrent access, which can lead to race conditions.
    • Improvement: Implement concurrency control mechanisms to handle simultaneous access and transactions.
  6. Error Handling:
    • Current State: Basic error handling is implemented, but it could be more comprehensive.
    • Improvement: Enhance error handling to cover more edge cases and provide more informative error messages to users.
  7. Testing:
    • Current State: No unit tests or automated tests are provided.
    • Improvement: Implement a comprehensive suite of unit tests and integration tests to ensure code reliability and correctness.
  8. User Experience:
    • Current State: The command-line interface is functional but basic.
    • Improvement: Develop a more user-friendly interface, possibly a graphical user interface (GUI) using libraries like tkinter or a web-based interface using frameworks like Flask or Django.
  9. Input Validation:
    • Current State: Basic input validation is implemented, but it could be more thorough.
    • Improvement: Enhance input validation to handle more cases and provide clear feedback to users for invalid inputs.
  10. Account Closure:
    • Current State: Accounts can only be closed if the balance is zero.
    • Improvement: Add functionality to transfer remaining balance to another account or to return the balance to the user before closing the account.
  11. Data Integrity:
    • Current State: The program does not handle potential data corruption issues.
    • Improvement: Implement checksums or other data integrity mechanisms to ensure that account data remains consistent and uncorrupted.
  12. Documentation:
    • Current State: Code documentation is minimal.
    • Improvement: Add detailed docstrings and comments to explain the code’s functionality and usage better.

Solution(I am tired now🥱)

import uuid
import datetime
import json
import os
import sqlite3
import hashlib
import threading
from apscheduler.schedulers.background import BackgroundScheduler
from apscheduler.triggers.interval import IntervalTrigger

class BankAccount:
    """Represents a single bank account."""

    def __init__(self, name, initial_balance, pin, interest_rate=0.03):
        self.account_number = str(uuid.uuid4())
        self.name = name
        self.balance = initial_balance
        self.pin = hashlib.sha256(pin.encode()).hexdigest()  # Store hashed PIN
        self.interest_rate = interest_rate
        self.transaction_history = []
        self.creation_date = datetime.datetime.now()
        self._add_transaction('Account Created', initial_balance)

    def _add_transaction(self, transaction_type, amount):
        """Records a transaction in the transaction history."""
        self.transaction_history.append({
            'date': datetime.datetime.now(),
            'type': transaction_type,
            'amount': amount
        })

    def deposit(self, amount):
        """Deposits money into the account."""
        if amount <= 0:
            raise ValueError("Deposit amount must be positive.")
        self.balance += amount
        self._add_transaction('Deposit', amount)
        return f"Deposited ${amount:.2f}. New Balance: ${self.balance:.2f}"

    def withdraw(self, amount):
        """Withdraws money from the account if sufficient funds are available."""
        if amount <= 0:
            raise ValueError("Withdrawal amount must be positive.")
        if amount > self.balance:
            raise ValueError("Insufficient funds.")
        self.balance -= amount
        self._add_transaction('Withdrawal', amount)
        return f"Withdrew ${amount:.2f}. New Balance: ${self.balance:.2f}"

    def check_balance(self):
        """Returns the current balance of the account."""
        return f"Account Balance: ${self.balance:.2f}"

    def view_transaction_history(self):
        """Returns the transaction history of the account."""
        if not self.transaction_history:
            return "No transactions found."
        history = []
        for transaction in self.transaction_history:
            history.append(f"{transaction['date']} - {transaction['type']}: ${transaction['amount']:.2f}")
        return "\n".join(history)

    def transfer(self, amount, target_account):
        """Transfers money from this account to another account."""
        if amount <= 0:
            raise ValueError("Transfer amount must be positive.")
        if amount > self.balance:
            raise ValueError("Insufficient funds.")
        self.withdraw(amount)
        target_account.deposit(amount)
        self._add_transaction('Transfer Out', amount)
        target_account._add_transaction('Transfer In', amount)
        return f"Transferred ${amount:.2f} to account {target_account.account_number}"

    def apply_interest(self):
        """Applies monthly compound interest to the account balance."""
        monthly_rate = self.interest_rate / 12
        interest = self.balance * monthly_rate
        self.balance += interest
        self._add_transaction('Interest Applied', interest)
        return f"Interest of ${interest:.2f} applied. New Balance: ${self.balance:.2f}"

    def close_account(self):
        """Closes the account by setting the balance to zero and clearing the transaction history."""
        if self.balance > 0:
            raise ValueError("Account balance must be zero to close the account.")
        self.transaction_history.clear()
        return "Account closed successfully."

class BankingSystem:
    """Manages multiple bank accounts."""
    
    def __init__(self):
        self.conn = sqlite3.connect('banking_system.db')
        self.cursor = self.conn.cursor()
        self.lock = threading.Lock()
        self._create_tables()
        self.scheduler = BackgroundScheduler()
        self.scheduler.add_job(self._apply_interest_to_all, IntervalTrigger(hours=730))  # Apply interest every month
        self.scheduler.start()

    def _create_tables(self):
        """Creates tables in the SQLite database."""
        self.cursor.execute('''
            CREATE TABLE IF NOT EXISTS accounts (
                account_number TEXT PRIMARY KEY,
                name TEXT,
                balance REAL,
                pin TEXT,
                interest_rate REAL,
                creation_date TEXT
            )
        ''')
        self.cursor.execute('''
            CREATE TABLE IF NOT EXISTS transactions (
                id INTEGER PRIMARY KEY AUTOINCREMENT,
                account_number TEXT,
                date TEXT,
                type TEXT,
                amount REAL
            )
        ''')
        self.conn.commit()

    def create_account(self, name, initial_balance, pin, interest_rate=0.03):
        """Creates a new account with a unique account number."""
        if initial_balance < 0:
            raise ValueError("Initial balance must be non-negative.")
        account = BankAccount(name, initial_balance, pin, interest_rate)
        with self.lock:
            self.cursor.execute('''
                INSERT INTO accounts (account_number, name, balance, pin, interest_rate, creation_date)
                VALUES (?, ?, ?, ?, ?, ?)
            ''', (account.account_number, account.name, account.balance, account.pin, account.interest_rate, account.creation_date.isoformat()))
            self.conn.commit()
        return f"Account created with Account Number: {account.account_number}"

    def get_account(self, account_number, pin):
        """Retrieves an account by account number after verifying the PIN."""
        with self.lock:
            self.cursor.execute('SELECT * FROM accounts WHERE account_number = ?', (account_number,))
            account_info = self.cursor.fetchone()
            if account_info and account_info[3] == hashlib.sha256(pin.encode()).hexdigest():
                account = BankAccount(
                    name=account_info[1],
                    initial_balance=account_info[2],
                    pin=pin,
                    interest_rate=account_info[4]
                )
                account.transaction_history = self._get_transaction_history(account_number)
                account.creation_date = datetime.datetime.fromisoformat(account_info[5])
                return account
            else:
                raise ValueError("Invalid account number or PIN.")

    def _get_transaction_history(self, account_number):
        """Retrieves transaction history from the database."""
        self.cursor.execute('SELECT date, type, amount FROM transactions WHERE account_number = ?', (account_number,))
        transactions = self.cursor.fetchall()
        history = []
        for transaction in transactions:
            history.append({
                'date': datetime.datetime.fromisoformat(transaction[0]),
                'type': transaction[1],
                'amount': transaction[2]
            })
        return history

    def delete_account(self, account_number, pin):
        """Deletes an account by account number after verifying the PIN."""
        account = self.get_account(account_number, pin)
        if account.balance > 0:
            raise ValueError("Account balance must be zero before deletion.")
        with self.lock:
            self.cursor.execute('DELETE FROM accounts WHERE account_number = ?', (account_number,))
            self.cursor.execute('DELETE FROM transactions WHERE account_number = ?', (account_number,))
            self.conn.commit()
        return "Account deleted successfully."

    def _apply_interest_to_all(self):
        """Applies interest to all accounts."""
        with self.lock:
            self.cursor.execute('SELECT account_number FROM accounts')
            accounts = self.cursor.fetchall()
            for account_number in accounts:
                account = self.get_account(account_number[0], '')  # PIN not needed for interest calculation
                account.apply_interest()
                # Update balance in database
                self.cursor.execute('UPDATE accounts SET balance = ? WHERE account_number = ?', (account.balance, account_number[0]))
                self.conn.commit()

def main():
    banking_system = BankingSystem()
    
    while True:
        print("\nBanking System Menu")
        print("1. Create Account")
        print("2. Deposit Money")
        print("3. Withdraw Money")
        print("4. Check Balance")
        print("5. View Transaction History")
        print("6. Transfer Money")
        print("7. Apply Interest")
        print("8. Close Account")
        print("9. Exit")
        choice = input("Choose an option: ")
        
        try:
            if choice == '1':
                name = input("Enter account holder name: ")
                initial_balance = float(input("Enter initial balance: "))
                pin = input("Set a 4-digit PIN: ")
                interest_rate = float(input("Enter interest rate (default is 0.03): ") or 0.03)
                print(banking_system.create_account(name, initial_balance, pin, interest_rate))
            elif choice == '2':
                account_number = input("Enter account number: ")
                pin = input("Enter your PIN: ")
                amount = float(input("Enter deposit amount: "))
                try:
                    account = banking_system.get_account(account_number, pin)
                    print(account.deposit(amount))
                except ValueError as e:
                    print(f"Error: {e}")

            elif choice == '3':
                account_number = input("Enter account number: ")
                pin = input("Enter your PIN: ")
                amount = float(input("Enter withdrawal amount: "))
                try:
                    account = banking_system.get_account(account_number, pin)
                    print(account.withdraw(amount))
                except ValueError as e:
                    print(f"Error: {e}")

            elif choice == '4':
                account_number = input("Enter account number: ")
                pin = input("Enter your PIN: ")
                try:
                    account = banking_system.get_account(account_number, pin)
                    print(account.check_balance())
                except ValueError as e:
                    print(f"Error: {e}")

            elif choice == '5':
                account_number = input("Enter account number: ")
                pin = input("Enter your PIN: ")
                try:
                    account = banking_system.get_account(account_number, pin)
                    print(account.view_transaction_history())
                except ValueError as e:
                    print(f"Error: {e}")

            elif choice == '6':
                source_account_number = input("Enter your account number: ")
                source_pin = input("Enter your PIN: ")
                target_account_number = input("Enter target account number: ")
                amount = float(input("Enter transfer amount: "))
                try:
                    source_account = banking_system.get_account(source_account_number, source_pin)
                    target_account = banking_system.get_account(target_account_number, source_pin)  # PIN validation not needed for target account
                    print(source_account.transfer(amount, target_account))
                except ValueError as e:
                    print(f"Error: {e}")

            elif choice == '7':
                # Apply interest manually for demonstration
                # In practice, interest is applied automatically by the scheduler
                account_number = input("Enter account number: ")
                pin = input("Enter your PIN: ")
                try:
                    account = banking_system.get_account(account_number, pin)
                    print(account.apply_interest())
                except ValueError as e:
                    print(f"Error: {e}")

            elif choice == '8':
                account_number = input("Enter account number: ")
                pin = input("Enter your PIN: ")
                try:
                    print(banking_system.delete_account(account_number, pin))
                except ValueError as e:
                    print(f"Error: {e}")

            elif choice == '9':
                print("Exiting the Banking System. Goodbye!")
                break

            else:
                print("Invalid choice. Please choose a valid option.")
        
        except ValueError as e:
            print(f"Error: {e}")

if __name__ == "__main__":
    main()

This is the best code I could create, would seek your help too :D.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply

Your email address will not be published. Required fields are marked *