Skip to content

Commit

Permalink
adding transactions
Browse files Browse the repository at this point in the history
  • Loading branch information
caseybecking committed Nov 18, 2024
1 parent 73178b7 commit 18f782b
Show file tree
Hide file tree
Showing 34 changed files with 5,796 additions and 13 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -263,3 +263,4 @@ app/config.py

Pipfile.lock
database.erd
uploads/*
5 changes: 2 additions & 3 deletions pylintrc → .pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -290,7 +290,7 @@ exclude-too-few-public-methods=
ignored-parents=

# Maximum number of arguments for function / method.
max-args=5
max-args=10

# Maximum number of attributes for a class (see R0902).
max-attributes=7
Expand All @@ -308,7 +308,7 @@ max-locals=15
max-parents=7

# Maximum number of positional arguments for function / method.
; max-positional-arguments=5
max-positional-arguments=10

# Maximum number of public methods for a class (see R0904).
max-public-methods=20
Expand Down Expand Up @@ -444,7 +444,6 @@ disable=raw-checker-failed,
missing-class-docstring,
missing-function-docstring,
import-outside-toplevel,
too-many-positional-arguments,
too-many-arguments,
unused-import,
too-many-locals,
Expand Down
1 change: 1 addition & 0 deletions Pipfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ flask-login = "*"
python-dotenv = "*"
pylint = "*"
requests = "*"
babel = "*"

[dev-packages]

Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ flask run
- Account ID
- Amount
- Transaction Type
- External ID
- Description

### Nice to haves
- Items (thought here is to be able to track the items that make up the transaction)
Expand Down
17 changes: 17 additions & 0 deletions api/helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from app.config import Config

def allowed_file(filename):
return '.' in filename and filename.rsplit('.', 1)[1].lower() in Config.ALLOWED_EXTENSIONS

def positive_or_negative(amount):
if amount < 0:
return "Withdrawal"
elif amount == 0:
return "Deposit"
else:
return "Deposit"

def clean_dollar_value(amount):
_amount = amount.replace("$", "")
__amount = _amount.replace(",", "")
return float(__amount)
166 changes: 159 additions & 7 deletions api/transaction/controllers.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,28 @@
from flask import g, request, jsonify, make_response
import os
import csv
from datetime import datetime
from math import ceil
from flask import g, request, jsonify, make_response, session
from flask_restx import Resource, fields
from werkzeug.utils import secure_filename
from app import db
from api.transaction.models import TransactionModel
from api.categories.models import CategoriesModel
from api.institution_account.models import InstitutionAccountModel
from api.institution.models import InstitutionModel

from app.config import Config
from api.helpers import allowed_file, positive_or_negative, clean_dollar_value

transaction_model = g.api.model('Transaction', {
'user_id': fields.String(required=True, description='User ID'),
'categories_id': fields.String(required=True, description='Categories ID'),
'account_id': fields.String(required=True, description='Account ID'),
'amount': fields.Float(required=True, description='Amount'),
'transaction_type': fields.String(required=True, description='Transaction Type'),
'external_id': fields.String(description='External ID')
'external_id': fields.String(description='External ID'),
'external_date': fields.DateTime(description='External Date'),
'description': fields.String(description='Description')
})

@g.api.route('/transaction')
Expand All @@ -22,21 +36,159 @@ def post(self):
amount = data.get('amount')
transaction_type = data.get('transaction_type')
external_id = data.get('external_id')
external_date = data.get('external_date')
description = data.get('description')

new_transaction = TransactionModel(
user_id=user_id,
categories_id=categories_id,
account_id=account_id,
amount=amount,
transaction_type=transaction_type,
external_id=external_id
external_id=external_id,
external_date=external_date,
description=description
)
new_transaction.save()

return make_response(jsonify({'message': 'Transaction created successfully'}), 201)

def get(self):
transactions = TransactionModel.query.all()
_transactions = []
_transactions.append([transaction.to_dict() for transaction in transactions])
return make_response(jsonify({'transactions': _transactions}), 200)
# Extract page and per_page from query parameters, default to page 1, 10 items per page
page = request.args.get('page', default=1, type=int)
per_page = request.args.get('per_page', default=100, type=int)

# Query with pagination
transactions_query = TransactionModel.query.paginate(page=page, per_page=per_page, error_out=False)

# Get the items for the current page
transactions = transactions_query.items

# Convert transactions to dictionaries
_transactions = [transaction.to_dict() for transaction in transactions]

# Metadata for pagination
pagination_info = {
'total': transactions_query.total, # Total number of items
'pages': ceil(transactions_query.total / per_page), # Total number of pages
'current_page': transactions_query.page, # Current page number
'per_page': transactions_query.per_page # Items per page
}

# Return response with pagination metadata
return make_response(jsonify({'transactions': _transactions, 'pagination': pagination_info}), 200)


@g.api.route('/transaction/csv_import')
class TransactionCSVImport(Resource):
def post(self):
user_id = session.get('_user_id')
if 'file' not in request.files:
return make_response(jsonify({'message': 'No file'}), 400)

file = request.files['file']

if file.filename == '':
return make_response(jsonify({'message': 'Filename cannot be blank'}), 400)

if file and allowed_file(file.filename):
upload_folder = Config.UPLOAD_FOLDER
filename = secure_filename(file.filename)
file_path = os.path.join(upload_folder, filename)
file.save(file_path)
try:
with open(file_path, newline='') as csvfile:
csvreader = csv.reader(csvfile)
header = next(csvreader) # Extract header
rows = [row for row in csvreader]

for row in rows:
row_data = dict(zip(header, row))
transaction_exists = self.check_transaction_by_external_id(row_data['Transaction ID'])
if transaction_exists:
print(f"Transaction {row_data['Transaction ID']} already exists. Skipping...")
continue # Skip processing this row

# Ensure category exists or create it
category_id = self.ensure_category_exists(row_data['Category'])

# Ensure institution exists or create it
institution_id = self.ensure_institution_exists(row_data['Institution'])

# Ensure account exists or create it
account_id = self.ensure_account_exists(row_data['Account'], institution_id)

_amount = clean_dollar_value(row_data['Amount'])
_transaction_type = positive_or_negative(_amount)

dt_object = datetime.strptime(row_data['Date'], "%m/%d/%Y")
formatted_timestamp = dt_object.strftime("%Y-%m-%d %H:%M:%S")

# Create the transaction
self.create_transaction(
user_id=user_id,
categories_id=category_id,
account_id=account_id,
amount=_amount,
transaction_type=_transaction_type,
external_id=row_data['Transaction ID'],
external_date=formatted_timestamp,
description=row_data.get('Description', None)
)
except Exception as e:
return make_response(jsonify({'message': str(e)}), 400)
else:
return make_response(jsonify({'message': 'Invalid file type'}), 400)

return make_response(jsonify({'message': 'Transactions imported successfully'}), 201)

def check_transaction_by_external_id(self,external_id):
user_id = session.get('_user_id')
transaction = TransactionModel.query.filter_by(external_id=external_id, user_id=user_id).first()
return transaction is not None

def ensure_category_exists(self,category_name):
user_id = session.get('_user_id')
category = CategoriesModel.query.filter_by(name=category_name, user_id=user_id).first()
if not category:
print(f"Category '{category_name}' does not exist. Creating...")
# category = CategoriesModel(name=category_name)
# db.session.add(category)
# db.session.commit()
return category.id

def ensure_institution_exists(self,institution_name):
user_id = session.get('_user_id')
institution = InstitutionModel.query.filter_by(name=institution_name,user_id=user_id).first()
if not institution:
print(f"InstitutionModel '{institution_name}' does not exist. Creating...")
institution = InstitutionModel(name=institution_name,user_id=user_id,location="Unknown",description="Unknown")
db.session.add(institution)
db.session.commit()
return institution.id

def ensure_account_exists(self,account_name, institution_id):
user_id = session.get('_user_id')
account = InstitutionAccountModel.query.filter_by(name=account_name,user_id=user_id).first()
if not account:
print(f"InstitutionAccountModel '{account_name}' does not exist. Creating...")
account = InstitutionAccountModel(name=account_name, institution_id=institution_id, user_id=user_id, number="Unknown", status='active', balance=0)
db.session.add(account)
db.session.commit()
return account.id

def create_transaction(self,user_id, categories_id, account_id, amount, transaction_type, external_id, external_date, description):
print(f"Creating transaction {external_id}...")
transaction = TransactionModel(
user_id=user_id,
categories_id=categories_id,
account_id=account_id,
amount=amount,
transaction_type=transaction_type,
external_id=external_id,
external_date=external_date,
description=description
)
db.session.add(transaction)
db.session.commit()
print(f"Transaction {external_id} created successfully.")
15 changes: 14 additions & 1 deletion api/transaction/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,22 @@ class TransactionModel(Base):
amount (float): The amount of the transaction.
type (str): The type of the transaction.
external_id (str): The external ID of the transaction.
external_date (datetime): The external date of the transaction.
"""

__tablename__ = 'transaction'
user_id = db.Column('user_id', db.Text, db.ForeignKey('user.id'), nullable=False)
categories_id = db.Column('categories_id', db.Text, db.ForeignKey('categories.id'), nullable=False)
categories = db.relationship('CategoriesModel', backref='transaction')
account_id = db.Column('account_id', db.Text, db.ForeignKey('account.id'), nullable=False)
account = db.relationship('InstitutionAccountModel', backref='transaction')
amount = db.Column(db.Float, nullable=False)
transaction_type = db.Column(db.String(255), nullable=False)
external_id = db.Column(db.String(255), nullable=False)
external_date = db.Column(db.DateTime, nullable=True)
description = db.Column(db.String(255), nullable=True)

def __init__(self, user_id, categories_id, account_id, amount, transaction_type, external_id):
def __init__(self, user_id, categories_id, account_id, amount, transaction_type, external_id, external_date, description):
"""
Initialize a TransactionModel instance.
Expand All @@ -33,13 +38,17 @@ def __init__(self, user_id, categories_id, account_id, amount, transaction_type,
amount (float): The amount of the transaction.
transaction_type (str): The transaction_type of the transaction.
external_id (str): The external ID of the transaction.
external_date (datetime): The external date of the transaction.
description (str): The description of the transaction.
"""
self.user_id = user_id
self.categories_id = categories_id
self.account_id = account_id
self.amount = amount
self.transaction_type = transaction_type
self.external_id = external_id
self.external_date = external_date
self.description = description

def __repr__(self):
"""
Expand All @@ -61,10 +70,14 @@ def to_dict(self):
'id': self.id,
'user_id': self.user_id,
'categories_id': self.categories_id,
'categories': self.categories.to_dict(),
'account_id': self.account_id,
'account': self.account.to_dict(),
'amount': self.amount,
'transaction_type': self.transaction_type,
'external_id': self.external_id,
'external_date': self.external_date,
'description': self.description,
'created_at': self.created_at,
'updated_at': self.updated_at
}
Expand Down
21 changes: 19 additions & 2 deletions app/__init__.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# CSV
import csv
# Datetime
from datetime import datetime
# Flask
import babel.dates
from flask import Flask
from flask import g
# Flask Restx
from flask_restx import Namespace
from flask_restx import Api
from flask_login import LoginManager
# babel
import babel
# Baseline
from app.config import Config
from app.database import db
Expand Down Expand Up @@ -34,6 +37,8 @@ def create_app():
app.register_blueprint(institution_account_blueprint)
from app.categories.controllers import categories_blueprint
app.register_blueprint(categories_blueprint)
from app.transactions.controllers import transactions_blueprint
app.register_blueprint(transactions_blueprint)

# Models
from api.user.models import User
Expand Down Expand Up @@ -71,4 +76,16 @@ def load_user(user_id):

app.running = True


@app.template_filter()
def format_datetime(value, _format='medium'):
_value = datetime.strptime(value, '%a, %d %b %Y %H:%M:%S %Z')
if _format == 'full':
_format="EEEE, d. MMMM y 'at' HH:mm"
elif _format == 'medium':
_format="EE dd.MM.y HH:mm"
elif _format == 'short':
_format="MM/dd/YYYY"
return babel.dates.format_datetime(_value, _format)

return app
Loading

0 comments on commit 18f782b

Please sign in to comment.