Skip to content

Commit

Permalink
Deposit, Withdraw & Transfer Handlers
Browse files Browse the repository at this point in the history
  • Loading branch information
roisol144 committed Nov 3, 2024
1 parent 8562dbd commit 4d44c8d
Show file tree
Hide file tree
Showing 12 changed files with 288 additions and 154 deletions.
3 changes: 2 additions & 1 deletion .env
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ FERNET_KEY=cYLeyg4Ljx31xEcdd9kWqSnIgo3RkusJEmOwZcKqrNM=
DATABASE_URL = postgres://postgres:solo6755@postgres:5432/postgres
TEST_DATABASE_URL = postgres://postgres:solo6755@postgres:5432/test_db # fix it
FLASK_ENV = development
TOKEN_EXPIRATION_HOURS = 1
TOKEN_EXPIRATION_HOURS = 1
SALT = 52c9f9f1cf12deafde09e6257abbd06b
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,16 @@ jobs:
# Install dependencies
- name: Install dependencies
run: |
source venv/bin/activate
python -m venv venv
source venv/bin/activate
pip install -r requirements.txt
- name: Lint with Flake8
run: |
source venv/bin/activate
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
- name: Tests
run: |
source venv/bin/activate
python test_app.py
Binary file modified __pycache__/auth_utils.cpython-39.pyc
Binary file not shown.
Binary file modified __pycache__/users.cpython-39.pyc
Binary file not shown.
21 changes: 20 additions & 1 deletion auth_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,19 @@
from functools import wraps
import logging
import os
import hashlib
from dotenv import load_dotenv
from cryptography.fernet import Fernet
load_dotenv()

SECRET_KEY = os.environ.get('SECRET_KEY')
# Keys
fernet_key = os.environ.get('FERNET_KEY')

# Check if fernet_key is None and raise an error if it is
if fernet_key is None:
raise ValueError("FERNET_KEY not found in environment variables.")

fernet = Fernet(fernet_key.encode())
TOKEN_EXPIRATION_HOURS = int(os.environ.get('TOKEN_EXPIRATION_HOURS'))

Expand All @@ -32,7 +40,7 @@ def verify_token(token):
logging.debug(f"Authentication token valid for user {payload['sub']}")
return payload['sub']
except jwt.ExpiredSignatureError:
logging.debug(f"Token Expired for user {payload['sub']}.")
logging.debug("Token Expired.")
return None
except jwt.InvalidTokenError:
logging.debug("Invalid token was supplied.")
Expand All @@ -42,6 +50,17 @@ def verify_token(token):
def encrypt_account_number(account_number):
encrypted_account_number = fernet.encrypt(account_number.encode())
return encrypted_account_number

def hash_account_number(account_number):
salt = os.getenv('SALT')
if salt is None:
raise ValueError("SALT not found in environment variables.")

if not isinstance(account_number, str):
raise ValueError("Account number must be a string.")

hashed_account_number = hashlib.sha256((account_number + salt).encode()).hexdigest()
return hashed_account_number



195 changes: 154 additions & 41 deletions bank_accounts.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
from flask import Blueprint, request, jsonify, g
import logging
from flask_bcrypt import Bcrypt
from db import get_db_connection, get_user_by_email, check_is_valid_user_id, check_is_valid_account_number, get_account_id_by_user_id
from db import get_db_connection, get_accounts_numbers_by_user_id, check_is_valid_user_id, check_is_valid_account_number, get_accounts_id_by_user_id
from exceptions import UserNotFoundError, DatabaseConnectionError, InsufficientFundsError, AccountNotFoundError
from uuid import uuid4
import datetime
import os
from auth_utils import encrypt_account_number
from auth_utils import encrypt_account_number, hash_account_number
import hashlib
from enum import Enum
import random


# logging config
Expand All @@ -30,8 +31,6 @@ class Status(Enum):
SUSPENDED = 'SUSPENDED'
CLOSED = 'CLOSED'



# Create a Blueprint for users
bank_accounts_bp = Blueprint('bank_accounts', __name__)
bcrypt = Bcrypt()
Expand Down Expand Up @@ -62,43 +61,50 @@ def get_bank_accounts():
]

return jsonify(accounts_list), 200


except DatabaseConnectionError as e:
return jsonify({'error': 'Internal Error'}), 500
except Exception as e:
logging.debug("General Error occured.")
return jsonify({'error': 'Internal Error'}), 500

except DatabaseConnectionError as e:
return jsonify({'error': 'Internal Error'}), 500


@bank_accounts_bp.route('/bank_accounts', methods=['POST'])
def create_bank_account():
data = request.get_json()
user_id = data['user_id']
currency = data['currency']
account_type = data['account_type']

try:
user_id = check_is_valid_user_id(str(data['user_id']))
if not user_id:
raise UserNotFoundError("Invalid user id.")
if not currency: # Might need to validate it in the case of an unexpected value
currency = Currency.USD.value
if not account_type:
account_type = AccountType.CHECKINGS.value # Might need to validate it in the case of an unexpected value

# Required params
account_id = str(uuid4())
account_number = str(int(uuid4()))[:6]
account_number = str(random.randint(100000, 999999))
logging.debug(f"Account number: {account_number}")
salt = os.urandom(16).hex()
salt = str(os.getenv('SALT'))
hashed_account_number = hashlib.sha256((account_number + salt).encode()).hexdigest()
encrypted_account_number = encrypt_account_number(account_number).decode('utf-8')
currency = Currency.USD.value
balance = 0 # change it to other default value that will be config
account_type = AccountType.CHECKINGS.value
created_at = datetime.datetime.now()
status = Status.ACTIVE.value

cursor, conn = get_db_connection()
cursor.execute("BEGIN;")

cursor.execute("""
INSERT INTO bank_accounts (id, user_id, account_number, balance, type, currency, created_at, status, hashed_account_number)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s) RETURNING id;
""", (account_id, user_id, encrypted_account_number, balance, account_type, currency, created_at, status, hashed_account_number))
conn.commit()
cursor.close()
conn.close()

Expand All @@ -107,71 +113,178 @@ def create_bank_account():
except UserNotFoundError as e:
return jsonify({'error' : 'User not found'}), 404
except Exception as e:
conn.rollback()
return jsonify({'error': 'Internal Error'}), 500

return jsonify({'account_id': account_id, 'account_number': encrypted_account_number, 'created_at': created_at}), 201



@bank_accounts_bp.route('/bank_accounts/deposit', methods=['POST'])
def deposit():
data = request.get_json()
amount = data['amount']
transfer_id = str(uuid4())
account_number_for_deposit = data['account_number']
current_user_id = g.current_user_id
transaction_type = "DEPOSIT"
user_hashed_accounts_numbers_list = get_accounts_numbers_by_user_id(current_user_id)
hashed_account_number = hash_account_number(account_number_for_deposit)

if not amount or not isinstance(amount, (int, float)):
raise ValueError("Invalid/Missing amount.")
if not user_hashed_accounts_numbers_list:
raise AccountNotFoundError("User has no bank accounts.")

if hashed_account_number not in user_hashed_accounts_numbers_list:
raise AccountNotFoundError("No account found.")

try:
if amount <= 0:
return jsonify({'error': 'Amount must be positive.'}), 400

cursor, conn = get_db_connection()
cursor.execute("BEGIN;")

cursor.execute("UPDATE bank_accounts SET balance = balance + %s WHERE hashed_account_number = %s", (amount, hashed_account_number))
cursor.execute("""
INSERT INTO transactions (id, from_account, to_account, amount, type, transaction_date)
VALUES (%s, %s, %s, %s, %s, %s)""",
(transfer_id, hashed_account_number, hashed_account_number, amount, transaction_type, datetime.datetime.now()))
conn.commit()
return jsonify({'message': 'Deposit Successfully!'}), 200

except ValueError as e:
conn.rollback()
return jsonify({'error': str(e)}), 400

except AccountNotFoundError as e:
conn.rollback()
return jsonify({'error': str(e)}), 400

except Exception as e:
conn.rollback()
return jsonify({'error': 'Deposit failed.'}), 400

finally:
if cursor:
cursor.close()
if conn:
conn.close()

@bank_accounts_bp.route('/bank_accounts/withdraw', methods=['POST'])
def withdraw():
data = request.get_json()
amount_to_withdraw = data['amount']
transaction_type = "WITHDRAW"
account_number_for_withdrawal = data['account_number']
transfer_id = str(uuid4())
current_user_id = g.current_user_id
user_hashed_accounts_numbers_list = get_accounts_numbers_by_user_id(current_user_id)
hashed_account_number = hash_account_number(account_number_for_withdrawal)

if not amount_to_withdraw or not isinstance(amount_to_withdraw, (int, float)):
raise ValueError("Invalid/Missing amount.")
if not user_hashed_accounts_numbers_list:
raise AccountNotFoundError("User has no bank accounts.")

if hashed_account_number not in user_hashed_accounts_numbers_list:
raise AccountNotFoundError("No account found.")

try:
if amount_to_withdraw <= 0:
return jsonify({'error': 'Amount must be positive.'}),400
cursor, conn = get_db_connection()

if hashed_account_number not in user_hashed_accounts_numbers_list:
raise AccountNotFoundError("No account found.")

cursor.execute("SELECT balance FROM bank_accounts WHERE hashed_account_number = %s", (hashed_account_number,))
account_to_withdraw_from = cursor.fetchone()
if account_to_withdraw_from['balance'] < amount_to_withdraw:
raise InsufficientFundsError("Insufficient Funds In Account.")

cursor.execute("BEGIN;")
cursor.execute("UPDATE bank_accounts SET balance = balance - %s WHERE hashed_account_number = %s", (amount_to_withdraw, hashed_account_number))
cursor.execute("""
INSERT INTO transactions (id, from_account, to_account, amount, type, transaction_date)
VALUES (%s, %s, %s, %s, %s, %s)""",
(transfer_id, hashed_account_number, hashed_account_number, amount_to_withdraw, transaction_type, datetime.datetime.now()))
conn.commit()
return jsonify({'message': 'Withdraw Succeded!'}), 200

except InsufficientFundsError as e:
return jsonify({'error': str(e)}), 400

except AccountNotFoundError as e:
return jsonify({'error': str(e)}), 400

except ValueError as e:
return jsonify({'error': str(e)}), 400

except Exception as e:
logging.debug(f"Error: {e}")
return jsonify({'error' : 'General Erorr'}), 500

finally:
if cursor:
cursor.close()
if conn:
conn.close()



@bank_accounts_bp.route('/bank_accounts/transfer', methods=['POST'])
def transfer():
data = request.get_json()
transfer_id = str(uuid4())
to_account_number = data.get('to_account_number')
from_account_number = data.get('from_account_number')
hashed_account_number_of_reciever = hash_account_number(to_account_number)
hashed_account_nunber_of_sender = hash_account_number(from_account_number)
tansaction_type = "TRANSFER"
amount = data.get('amount')
transaction_type = data.get('transaction_type')
logging.debug(f"Parsed data: transfer_id={transfer_id}, to_account_number={to_account_number}, amount={amount}, transaction_type={transaction_type}")


# Validate input data
if not amount or not isinstance(amount, (int, float)):
return jsonify({'error': 'Invalid amount'}), 400
if not transaction_type or transaction_type not in ['WITHDRAW', 'DEPOSIT']:
return jsonify({'error': 'Invalid transaction type'}), 400

try:
if amount <= 0:
return jsonify({'error': 'Amount must be positive'}), 400

from_account_id = get_account_id_by_user_id(g.current_user_id)
logging.debug(f"From account ID: {from_account_id}")

if not from_account_id:
raise AccountNotFoundError("Account not found for the current user")
if not to_account_number:
raise ValueError("Account number is required for deposit.")

# Establish db connection and transaction
cursor, conn = get_db_connection()

cursor.execute("BEGIN;")

# Check if the sender's account exists
cursor.execute("SELECT balance FROM bank_accounts WHERE id = %s", (from_account_id,))
# Note to self - Check how to auth works here, how its possible to take money from account - will it auth first ?
cursor.execute("SELECT balance FROM bank_accounts WHERE hashed_account_number = %s", (hashed_account_nunber_of_sender,))
sender_account = cursor.fetchone()
if not sender_account:
raise AccountNotFoundError("Sender's account not found")

sender_balance = sender_account['balance']

if sender_balance < amount:
raise InsufficientFundsError("Insufficient funds.")

if transaction_type == 'WITHDRAW':
to_account_id = None
cursor.execute("UPDATE bank_accounts SET balance = balance - %s WHERE id = %s", (amount, from_account_id))

elif transaction_type == 'DEPOSIT':
if not to_account_number:
raise ValueError("Account number is required for deposit.")

receiver_account_id = check_is_valid_account_number(to_account_number)
if not receiver_account_id:
raise ValueError("Invalid receiver account number")
# Validate Bank Account Number -
cursor.execute("SELECT balance FROM bank_accounts WHERE hashed_account_number = %s", (hashed_account_number_of_reciever,))
account_to_transfer_money_to = cursor.fetchone()
# Check if no account found
if not account_to_transfer_money_to:
raise AccountNotFoundError("No Account Found.")

cursor.execute("UPDATE bank_accounts SET balance = balance - %s WHERE id = %s", (amount, from_account_id))
cursor.execute("UPDATE bank_accounts SET balance = balance + %s WHERE id = %s", (amount, receiver_account_id))
to_account_id = receiver_account_id

cursor.execute("UPDATE bank_accounts SET balance = balance - %s WHERE hashed_account_number = %s", (amount, hashed_account_nunber_of_sender))
cursor.execute("UPDATE bank_accounts SET balance = balance + %s WHERE hashed_account_number = %s", (amount, hashed_account_number_of_reciever))
cursor.execute("""
INSERT INTO transactions (id, from_account, to_account, amount, type, transaction_date)
VALUES (%s, %s, %s, %s, %s, %s)""",
(transfer_id, from_account_id, to_account_id, amount, transaction_type, datetime.datetime.now()))

(transfer_id, hashed_account_number_of_reciever, hashed_account_nunber_of_sender, amount, tansaction_type, datetime.datetime.now()))
conn.commit()
return jsonify({'message': 'Transfer successful'}), 200

except (DatabaseConnectionError, InsufficientFundsError, ValueError, AccountNotFoundError) as e:
Expand Down
Loading

0 comments on commit 4d44c8d

Please sign in to comment.