Python 28: Banking System (part 2)
Tags: python, beginners, daily, classes
Published: 2024-01-03 07:54:15

Today I continued my banking system exercise from yesterday. I admit I had to use a lot of ChatGPT (and started using GitHub Copilot) to build this and it sort of grew over my head, even tho I was able to make it work. 

Doesn’t feel like I mastered classes yet, so I’ll probably do some smaller exercise with them in the coming days. Suggestions would always be welcome :)

Anyways here is my working banking system:

```python
"""
Exercise: Simple Banking System 2 - This is a continuation of the previous days exercise.
Create a basic banking system.
Allow users to create a new account, deposit money, withdraw money, and check their balance. With a master password one can also check all balances.
Account details are stored in JSON file, so they are continuous even after the program is closed.
Use classes to represent users and accounts.
"""

import json
import logging

logging.basicConfig(level=logging.INFO)

class Account:
    def __init__(self, account_id, holder_name, initial_balance=0):
        self.account_id = account_id
        self.holder_name = holder_name
        self.balance = initial_balance

    def deposit(self, amount):
        if amount > 0:
            self.balance += amount
            logging.info(f"Deposited {amount}. New balance of {self.holder_name}: {self.balance}")
        else:
            logging.warning("Invalid deposit amount.")

    def withdraw(self, amount):
        if 0 < amount <= self.balance:
            self.balance -= amount
            logging.info(f"Withdrew {amount}. New balance of {self.holder_name}: {self.balance}")
        else:
            logging.warning("Invalid withdrawal amount or insufficient funds.")

    def get_balance(self):
        return self.balance

class Bank:
    def __init__(self):
        self.accounts = {}
        self.used_ids = set()
        self.load_accounts()

    def save_accounts(self):
        if not self.accounts:
            logging.warning("No accounts to save.")
            return

        with open('accounts.json', 'w') as file:
            data = {account_id: {'holder_name': account.holder_name, 'balance': account.balance} for account_id, account in self.accounts.items()}
            json.dump(data, file)
            logging.debug(f"Saved accounts: {data}")

    def load_accounts(self):
        try:
            with open('accounts.json', 'r') as file:
                data = json.load(file)
                if not data:
                    logging.warning("JSON file is empty.")
                    return

                for account_id, account_info in data.items():
                    account_id = int(account_id)  # Convert to int since JSON keys are always strings
                    self.accounts[account_id] = Account(account_id, account_info['holder_name'], account_info['balance'])
                    self.used_ids.add(account_id)

                logging.debug(f"Loaded accounts: {self.accounts.keys()}")
                logging.debug(f"Used IDs after loading: {self.used_ids}")

        except (FileNotFoundError, json.JSONDecodeError) as e:
            logging.warning(f"Error loading account data: {e}")
            logging.info("No existing account data found or file is empty. Starting fresh.")

    def create_account(self, holder_name, initial_balance=0):
        account_id = self.generate_account_id()
        new_account = Account(account_id, holder_name, initial_balance)
        self.accounts[account_id] = new_account
        self.used_ids.add(account_id)
        logging.info(f"Account created with ID: {account_id}")
        return account_id

    def generate_account_id(self):
        # Find the lowest unused ID
        current_id = 1
        while current_id in self.used_ids:
            current_id += 1
        return current_id

    def get_account(self, account_id):
        return self.accounts.get(account_id, None)

def main():
    bank = Bank()
    try:
        while True:
            user_input = input('What would you like to do? Options: n = new account, d = deposit money, w = withdraw money, c = check balance, l = list all accounts, e = exit\n')
            if user_input == 'n':
                holder_name = input('Enter your name: ')
                initial_balance = float(input('Enter initial deposit: '))
                account_id = bank.create_account(holder_name, initial_balance)
                bank.save_accounts()
            elif user_input == 'd' or user_input == 'w' or user_input == 'c':
                try:
                    while True:
                        account_id = int(input('Enter account ID: '))
                        if account_id in bank.used_ids:
                            break
                        else:
                            logging.warning("Invalid account ID.")
                    account = bank.get_account(account_id)
                    if user_input == 'd':
                        amount = float(input('Enter deposit amount: '))
                        account.deposit(amount)
                        bank.save_accounts()
                    elif user_input == 'w':
                        amount = float(input('Enter withdrawal amount: '))
                        account.withdraw(amount)
                        bank.save_accounts()
                    elif user_input == 'c':
                        print(f"Current balance: {account.get_balance()}")
                except ValueError:
                    logging.warning("Invalid account ID.")
            elif user_input == 'l':
                master_password = input('Enter master password: ')
                if master_password == '42':
                    if not bank.accounts:  # Check if the bank's accounts dictionary is empty
                        print("No accounts.")
                    else:
                        print("Accounts:")
                        for account_id, account in bank.accounts.items():
                            print(f"{account_id}: {account.holder_name} - {account.balance}")
                else:
                    logging.warning("Invalid master password.")
            elif user_input == 'e':
                break
            else:
                logging.info("Invalid option selected.")
    except KeyboardInterrupt:
        logging.error('KeyboardInterrupt')
    except ValueError:
        logging.error("Invalid input. Please enter a number.")
    
    logging.debug('main() end')

if __name__ == '__main__':
    main()
```