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/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