From 69c130596b8d5ff4757318673db2a988ba51505e Mon Sep 17 00:00:00 2001 From: kelvyn2012 Date: Mon, 21 Jul 2025 12:14:15 +0100 Subject: [PATCH 1/2] implemented full crud transaction API --- backend/app.py | 27 +++++--- backend/models/__init__.py | 3 + backend/models/budget.py | 15 ++++ backend/models/category.py | 10 +++ backend/models/transaction.py | 11 +++ backend/routes/budget_routes.py | 106 +++++++++++++++++++++++++++++ backend/services/budget_service.py | 28 ++++++++ 7 files changed, 189 insertions(+), 11 deletions(-) create mode 100644 backend/models/__init__.py create mode 100644 backend/models/budget.py create mode 100644 backend/models/category.py create mode 100644 backend/models/transaction.py create mode 100644 backend/routes/budget_routes.py create mode 100644 backend/services/budget_service.py diff --git a/backend/app.py b/backend/app.py index 892d585..3cd3294 100644 --- a/backend/app.py +++ b/backend/app.py @@ -4,12 +4,15 @@ from flask_cors import CORS from backend.database import db, migrate + def create_app(config_class): - app = Flask(__name__, - template_folder='../frontend/templates', - static_folder='../frontend/static') + app = Flask( + __name__, + template_folder="../frontend/templates", + static_folder="../frontend/static", + ) app.config.from_object(config_class) - + # Initialize extensions db.init_app(app) migrate.init_app(app, db) @@ -17,23 +20,25 @@ def create_app(config_class): CORS(app) # Landing page route - @app.route('/') + @app.route("/") def index(): - return render_template('index.html') + return render_template("index.html") # Health check endpoint - @app.route('/api') + @app.route("/api") def api_status(): - return jsonify({'message': 'API is working', 'status': 'success'}), 200 - + return jsonify({"message": "API is working", "status": "success"}), 200 + # Register blueprints from backend.routes.auth import auth_bp + from backend.routes.budget_routes import budget_bp + app.register_blueprint(budget_bp, url_prefix="/api/budgets") - app.register_blueprint(auth_bp, url_prefix='/api/auth') + app.register_blueprint(auth_bp, url_prefix="/api/auth") # Create tables with app.app_context(): db.create_all() - + return app diff --git a/backend/models/__init__.py b/backend/models/__init__.py new file mode 100644 index 0000000..f78dd25 --- /dev/null +++ b/backend/models/__init__.py @@ -0,0 +1,3 @@ +from .budget import Budget +from .transaction import Transaction +from .category import Category diff --git a/backend/models/budget.py b/backend/models/budget.py new file mode 100644 index 0000000..c24edaf --- /dev/null +++ b/backend/models/budget.py @@ -0,0 +1,15 @@ +from backend.database import db +from datetime import datetime + +class Budget(db.Model): + __tablename__ = 'budgets' + + id = db.Column(db.Integer, primary_key=True) + category_id = db.Column(db.Integer, db.ForeignKey('categories.id'), nullable=False) + user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False) + amount = db.Column(db.Float, nullable=False) + period = db.Column(db.String(20), nullable=False) # e.g., 'monthly', 'weekly' + created_at = db.Column(db.DateTime, default=datetime.utcnow) + + category = db.relationship('Category', backref='budgets') + user = db.relationship('User', backref='budgets') diff --git a/backend/models/category.py b/backend/models/category.py new file mode 100644 index 0000000..1e67d24 --- /dev/null +++ b/backend/models/category.py @@ -0,0 +1,10 @@ +from backend.database import db + +class Category(db.Model): + __tablename__ = 'categories' + + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String(100), nullable=False) + + def __repr__(self): + return f"" diff --git a/backend/models/transaction.py b/backend/models/transaction.py new file mode 100644 index 0000000..7554d22 --- /dev/null +++ b/backend/models/transaction.py @@ -0,0 +1,11 @@ +from backend.database import db +from datetime import datetime + +class Transaction(db.Model): + __tablename__ = 'transactions' + + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column(db.Integer, nullable=False) + category_id = db.Column(db.Integer, nullable=False) + amount = db.Column(db.Float, nullable=False) + created_at = db.Column(db.DateTime, default=datetime.utcnow) diff --git a/backend/routes/budget_routes.py b/backend/routes/budget_routes.py new file mode 100644 index 0000000..a89d705 --- /dev/null +++ b/backend/routes/budget_routes.py @@ -0,0 +1,106 @@ +from flask import Blueprint, request, jsonify +from flask_jwt_extended import jwt_required, get_jwt_identity +from sqlalchemy import func +from backend.database import db +from backend.models.budget import Budget +from backend.models.transaction import Transaction +from backend.services.budget_service import calculate_spending + + +budget_bp = Blueprint("budget_bp", __name__) + + +@budget_bp.route("/ping", methods=["GET"]) +def ping(): + return {"message": "Budget service is up"}, 200 + + +@budget_bp.route("/api/budgets", methods=["POST"]) +@jwt_required() +def create_budget(): + user_id = get_jwt_identity() + data = request.json + + # Validate input... + budget = Budget( + category_id=data["category_id"], + user_id=user_id, + amount=data["amount"], + period=data["period"], + ) + db.session.add(budget) + db.session.commit() + return jsonify({"message": "Budget created", "budget": budget.id}), 201 + + +@budget_bp.route("/api/budgets", methods=["GET"]) +@jwt_required() +def list_budgets(): + user_id = get_jwt_identity() + budgets = Budget.query.filter_by(user_id=user_id).all() + return jsonify( + [ + { + "id": b.id, + "category_id": b.category_id, + "amount": b.amount, + "period": b.period, + } + for b in budgets + ] + ) + + +@budget_bp.route("/api/budgets/", methods=["PUT"]) +@jwt_required() +def update_budget(budget_id): + user_id = get_jwt_identity() + budget = Budget.query.filter_by(id=budget_id, user_id=user_id).first_or_404() + data = request.json + budget.amount = data.get("amount", budget.amount) + budget.period = data.get("period", budget.period) + db.session.commit() + return jsonify({"message": "Budget updated"}) + + +@budget_bp.route("/api/budgets/", methods=["DELETE"]) +@jwt_required() +def delete_budget(budget_id): + user_id = get_jwt_identity() + budget = Budget.query.filter_by(id=budget_id, user_id=user_id).first_or_404() + db.session.delete(budget) + db.session.commit() + return jsonify({"message": "Budget deleted"}) + + +@budget_bp.route("/api/budgets/status", methods=["GET"]) +@jwt_required() +def budget_status(): + user_id = get_jwt_identity() + budgets = Budget.query.filter_by(user_id=user_id).all() + status_list = [] + + for b in budgets: + spent = calculate_spending(user_id, b.category_id, b.period) + status_list.append( + { + "budget_id": b.id, + "category_id": b.category_id, + "amount_budgeted": b.amount, + "amount_spent": spent, + "status": "over" if spent > b.amount else "under", + "difference": round(spent - b.amount, 2), + } + ) + + return jsonify(status_list) + + +def check_alerts(user_id): + alerts = [] + budgets = Budget.query.filter_by(user_id=user_id).all() + for b in budgets: + spent = calculate_spending(user_id, b.category_id, b.period) + if spent > b.amount: + alerts.append(f"You've exceeded your budget for category {b.category_id}.") + return alerts diff --git a/backend/services/budget_service.py b/backend/services/budget_service.py new file mode 100644 index 0000000..0fa289c --- /dev/null +++ b/backend/services/budget_service.py @@ -0,0 +1,28 @@ +# services/budget_service.py +from datetime import datetime, timedelta +from sqlalchemy import func +from backend.database import db +from backend.models.transaction import Transaction + + +def calculate_spending(user_id, category_id, period): + now = datetime.utcnow() + + if period == "monthly": + start = datetime(now.year, now.month, 1) + elif period == "weekly": + start = now - timedelta(days=now.weekday()) # Monday start + else: + return 0 + + total = ( + db.session.query(func.coalesce(func.sum(Transaction.amount), 0)) + .filter( + Transaction.user_id == user_id, + Transaction.category_id == category_id, + Transaction.created_at >= start, + ) + .scalar() + ) + + return total From cd838ee0473d158f0dd1552714f62f1e6b8a64e0 Mon Sep 17 00:00:00 2001 From: kelvyn2012 Date: Mon, 21 Jul 2025 12:22:57 +0100 Subject: [PATCH 2/2] removed the temporary transaction and category models which was used to test the budget model --- backend/models/category.py | 10 ---------- backend/models/transaction.py | 11 ----------- 2 files changed, 21 deletions(-) delete mode 100644 backend/models/category.py delete mode 100644 backend/models/transaction.py diff --git a/backend/models/category.py b/backend/models/category.py deleted file mode 100644 index 1e67d24..0000000 --- a/backend/models/category.py +++ /dev/null @@ -1,10 +0,0 @@ -from backend.database import db - -class Category(db.Model): - __tablename__ = 'categories' - - id = db.Column(db.Integer, primary_key=True) - name = db.Column(db.String(100), nullable=False) - - def __repr__(self): - return f"" diff --git a/backend/models/transaction.py b/backend/models/transaction.py deleted file mode 100644 index 7554d22..0000000 --- a/backend/models/transaction.py +++ /dev/null @@ -1,11 +0,0 @@ -from backend.database import db -from datetime import datetime - -class Transaction(db.Model): - __tablename__ = 'transactions' - - id = db.Column(db.Integer, primary_key=True) - user_id = db.Column(db.Integer, nullable=False) - category_id = db.Column(db.Integer, nullable=False) - amount = db.Column(db.Float, nullable=False) - created_at = db.Column(db.DateTime, default=datetime.utcnow)