diff --git a/.gitignore b/.gitignore index 940489f..b5848e7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,9 @@ .venv/ .env .idea/ +.vscode/ +__pycache__/ +*.pyc +instance/ +db.sqlite3 + diff --git a/To-Do.md b/To-Do.md new file mode 100644 index 0000000..238ab43 --- /dev/null +++ b/To-Do.md @@ -0,0 +1,17 @@ +# Project To-Do and Feature Ideas + +This file tracks potential new features and improvements for the Interbend banking system. + +## Feature Suggestions + +1. **Automated Payroll System:** + * **Description:** Instead of requiring users to manually call the `/collect` endpoint, a scheduled script could run periodically (e.g., every 24 hours) to automatically distribute salaries to all eligible users. + * **Benefits:** Improves user experience, ensures consistent pay, and reduces repeated API calls to the server. + * **Important:** Due to the concept of this whole system it needs to be considered to only pay users who are attend. Bad Idea. + * **Alternative:** Implement system to make sure you can only collect if the host is online. Make admin route to open server (set global bool) + +2. **User Transaction History:** + * **Description:** Create a new API endpoint (e.g., `GET /transactions`) that allows an authenticated user to retrieve a paginated list of their own transaction history. + * **Benefits:** Provides users with transparency and a way to track their finances, which is a core feature of any banking application. + + * FINISHED: PLEASE CHECK IF WORKING! \ No newline at end of file diff --git a/config.py b/config.py index 9dfe8d4..62a2adf 100644 --- a/config.py +++ b/config.py @@ -18,4 +18,4 @@ class Config: # Admin ADMIN_KEY = os.getenv('ADMIN_KEY') - COLLECT_COOLDOWN = os.getenv('COLLECT_COOLDOWN') + COLLECT_COOLDOWN = int(os.getenv('COLLECT_COOLDOWN', 24)) diff --git a/interbend/routes/transaction_routes.py b/interbend/routes/transaction_routes.py index e206a06..c8a2155 100644 --- a/interbend/routes/transaction_routes.py +++ b/interbend/routes/transaction_routes.py @@ -20,59 +20,81 @@ def get_balance(): def collect(): bid = request.bid cooldown = Config.COLLECT_COOLDOWN + try: with db.cursor(dictionary=True) as cur: + # 1. Get user job cur.execute("SELECT * FROM user_jobs WHERE bid = %s", (bid,)) user_jt = cur.fetchone() - if not user_jt: - return jsonify({"error": "You dont have any Jobs"}), 404 - active_cooldown = user_jt["collected"] - except mysql.connector.Error as err: - current_app.logger.error(f"Database error in collect, salary: {err}") - return jsonify({"error": "A database error occurred, please try again later."}), 500 - if active_cooldown + timedelta(hours=cooldown) > datetime.now(timezone.utc): - remaining_time = (active_cooldown + timedelta(hours=cooldown)) - datetime.now(timezone.utc) - hours = int(remaining_time.total_seconds() // 3600) - minutes = int(remaining_time.total_seconds() % 3600 // 60) - return jsonify({"error": f"You can only collect your salary every {cooldown} hours. Please wait {hours}h {minutes}m."}), 429 - job = user_jt["job_id"] - try: - with db.cursor(dictionary=True) as cur: - cur.execute("SELECT * FROM jobs WHERE job_id = %i", (job,)) + if not user_jt: + return jsonify({"error": "You dont have any Jobs"}), 404 + + # 2. Check cooldown + active_cooldown = user_jt.get("collected") + if active_cooldown and (active_cooldown + timedelta(hours=cooldown) > datetime.now(timezone.utc)): + remaining_time = (active_cooldown + timedelta(hours=cooldown)) - datetime.now(timezone.utc) + hours = int(remaining_time.total_seconds() // 3600) + minutes = int(remaining_time.total_seconds() % 3600 // 60) + return jsonify({"error": f"You can only collect your salary every {cooldown} hours. Please wait {hours}h {minutes}m."}), 429 + + # 3. Get job details + job_id = user_jt["job_id"] + cur.execute("SELECT * FROM jobs WHERE job_id = %s", (job_id,)) job_data = cur.fetchone() - except mysql.connector.Error as err: - current_app.logger.error(f"Database error in collect, salary: {err}") - return jsonify({"error": "A database error occurred, please try again later."}), 500 - if not job_data: - return jsonify({"error": "Invalid Job","message":"If you believe this is an error, contact a " - "Administrator"}), 404 - salary_class = job_data["salary_class"] - try: - with db.cursor(dictionary=True) as cur: - cur.execute("SELECT * FROM salary WHERE class = %i", (salary_class,)) + if not job_data: + return jsonify({"error": "Invalid Job", "message": "If you believe this is an error, contact an Administrator"}), 404 + + # 4. Get salary details + salary_class = job_data["salary_class"] + cur.execute("SELECT * FROM salary WHERE class = %s", (salary_class,)) salary_data = cur.fetchone() - except mysql.connector.Error as err: - current_app.logger.error(f"Database error in collect, salary: {err}") - return jsonify({"error": "A database error occurred, please try again later."}), 500 - if not salary_data: - return jsonify({"error": "Invalid Salary Class"}), 500 - amount = salary_data["money"] - try: - with db.cursor(dictionary=True) as cur: - cur.execute("UPDATE users SET balance = balance + %s WHERE bid = %s", (amount, bid,)) - cur.execute("UPDATE user_jobs SET collected = %s WHERE bid = %s", (datetime.now(timezone.utc), bid,)) - cur.execute("INSERT INTO transactions (source, target, amount, type, timestamp, status) VALUES (%s, %s, " - "%s, %s, %s, %s)", "NULL", bid, amount, "salary", datetime.now(timezone.utc), "completed",) + if not salary_data: + return jsonify({"error": "Invalid Salary Class"}), 500 + + amount = salary_data["money"] + + # 5. Perform transaction + db.start_transaction() + cur.execute("UPDATE users SET balance = balance + %s WHERE bid = %s", (amount, bid)) + cur.execute("UPDATE user_jobs SET collected = %s WHERE bid = %s", (datetime.now(timezone.utc), bid)) + cur.execute( + "INSERT INTO transactions (source, target, amount, type, timestamp, status) VALUES (%s, %s, %s, %s, %s, %s)", + ("SYSTEM", bid, amount, "salary", datetime.now(timezone.utc), "completed") + ) cur.execute("SELECT balance FROM users WHERE bid = %s", (bid,)) - new_bal2 = cur.fetchone() - new_bal = new_bal2["balance"] + new_balance = cur.fetchone()["balance"] db.commit() + + return jsonify({"message": "Salary Collected", "New Balance": new_balance}), 200 + except mysql.connector.Error as err: db.rollback() - current_app.logger.error(f"Database error in collect, salary: {err}") + current_app.logger.error(f"Database error in /collect: {err}") return jsonify({"error": "A database error occurred, please try again later."}), 500 - return jsonify({"message":"Salary Collected","New Balance":new_bal}), 200 + except Exception as e: + db.rollback() + current_app.logger.error(f"An unexpected error occurred in /collect: {e}") + return jsonify({"error": "An unexpected server error occurred."}), 500 +@transactions_bp.route('/transactions', methods=['GET']) +@jwt_required +def get_transactions(): + user_bid = request.bid + limit = request.args.get('limit', default=10, type=int) + try: + with db.cursor(dictionary=True) as cur: + cur.execute("SELECT * FROM transactions WHERE source = %s OR target = %s ORDER BY timestamp DESC LIMIT %s", (user_bid, user_bid, limit)) + transactions = cur.fetchall() + return jsonify({"transactions": transactions}), 200 + except mysql.connector.Error as err: + current_app.logger.error(f"Database error in /transactions: {err}") + return jsonify({"error": "A database error occurred, please try again later."}), 500 + except Exception as e: + current_app.logger.error(f"An unexpected error occurred in /transactions: {e}") + return jsonify({"error": "An unexpected server error occurred."}), 500 + + +# this should be fine @transactions_bp.route('/transfer', methods=['POST']) @jwt_required def transfer():