diff --git a/financial-tracker/api-gateway/app/app.py b/financial-tracker/api-gateway/app/app.py index 922eb4868871b2a59c6a9c494fd756c336cc727e..51520fefdfbec433127a227d38d1a4b6b2d301bb 100644 --- a/financial-tracker/api-gateway/app/app.py +++ b/financial-tracker/api-gateway/app/app.py @@ -174,6 +174,7 @@ def fetch_currencies(): except requests.exceptions.RequestException as e: return jsonify({"error": "User service unavailable"}), 503 + @app.route('/timezones', methods=["GET"]) @jwt_required() def fetch_timezones(): @@ -194,41 +195,27 @@ def fetch_timezones(): # TRANSACTION-SERVICE ROUTES -@app.route('/transactions', methods=["GET", "POST"]) -def handle_transactions(): - """ - Forward GET/POST transaction requests to the transaction-service. - """ - try: - if request.method == "GET": - response = requests.get(f"{TRANSACTION_SERVICE_URL}/transactions", params=request.args) - else: - response = requests.post(f"{TRANSACTION_SERVICE_URL}/transactions", json=request.get_json()) - - return jsonify(response.json()), response.status_code - - except requests.exceptions.RequestException as e: - app.logger.error(f"Error contacting transaction-service: {e}") - return jsonify({"error": "Transaction service unavailable"}), 503 +@app.route('/transactions', methods=['GET']) +def fetch_transactions(): + """Fetch transactions for the logged-in user.""" + token = request.headers.get("Authorization") # Get token from headers + if not token: + app.logger.error("Authorization token is missing.") + return jsonify({"error": "Authorization token is missing"}), 400 -@app.route('/transactions/<int:tx_id>', methods=["PUT", "DELETE"]) -def handle_transaction_by_id(tx_id): - """ - Forward PUT/DELETE requests to transaction-service for specific transaction. - """ try: - if request.method == "PUT": - response = requests.put(f"{TRANSACTION_SERVICE_URL}/transactions/{tx_id}", json=request.get_json()) - else: - response = requests.delete(f"{TRANSACTION_SERVICE_URL}/transactions/{tx_id}") + headers = {"Authorization": token} # Forward token + response = requests.get(f"{TRANSACTION_SERVICE_URL}/transactions", headers=headers) + app.logger.info(f"Response from transaction-service (expenses): {response.status_code} - {response.json()}") return jsonify(response.json()), response.status_code - + except requests.exceptions.RequestException as e: app.logger.error(f"Error contacting transaction-service: {e}") return jsonify({"error": "Transaction service unavailable"}), 503 + @app.route('/expenses', methods=['POST']) def add_expense(): """Forward request to transaction-service to add an expense.""" @@ -263,6 +250,7 @@ def fetch_expenses(): headers = {"Authorization": token} # Forward token response = requests.get(f"{TRANSACTION_SERVICE_URL}/expenses", headers=headers) app.logger.info(f"Response from transaction-service (expenses): {response.status_code} - {response.json()}") + return jsonify(response.json()), response.status_code except requests.exceptions.RequestException as e: return jsonify({"error": "Transaction service unavailable"}), 503 @@ -612,7 +600,7 @@ def get_weekly_dashboard_stats(): except requests.exceptions.RequestException as e: app.logger.error(f"Error contacting analytics-service: {e}") return jsonify({"error": "Analytics service unavailable."}), 503 - + @app.route('/dashboard/monthly-stats', methods=['POST']) @jwt_required() diff --git a/financial-tracker/docker-compose.yml b/financial-tracker/docker-compose.yml index c7266aedc2682934bd093569da7f1d059ffca7ab..eaf99d2e3feb03d2706ae9a7619a7a8f6c7c55a0 100644 --- a/financial-tracker/docker-compose.yml +++ b/financial-tracker/docker-compose.yml @@ -91,23 +91,17 @@ services: notification-service: build: - context: ./notification-service + context: . + dockerfile: notification-service/Dockerfile ports: - - "5005:5005" # Combined HTTP and WebSocket port + - "5005:5005" environment: - DATABASE_URL=postgresql://user:password@notification-db:5432/notification_db - - JWT_SECRET_KEY=3b5e41af18179f530c5881a5191e15f0ab35eed2fefdc068fda254eed3fb1ecb - - EMAIL_USER=your_email_user - - EMAIL_PASS=your_email_password depends_on: - notification-db: - condition: service_healthy + - notification-db networks: - app-network - healthcheck: - test: ["CMD", "curl", "-f", "http://localhost:5005/health"] - interval: 30s - retries: 3 + frontend: build: diff --git a/financial-tracker/frontend/src/App.js b/financial-tracker/frontend/src/App.js index 81be08f0daf3901256220c9c9b140b6979d57454..b56cdb7c8be5a058401d59983d2f3c9551145f55 100644 --- a/financial-tracker/frontend/src/App.js +++ b/financial-tracker/frontend/src/App.js @@ -6,6 +6,7 @@ import Register from "./pages/Register"; import Login from "./pages/Login"; import Dashboard from "./pages/Dashboard"; import Income from "./pages/Income"; +import Transactions from "./pages/Transactions"; import Expenses from "./pages/Expenses"; import Budget from "./pages/Budget"; import Profile from "./pages/Profile"; @@ -42,10 +43,11 @@ function App() { {token ? ( <> <li><Link to="/dashboard">Dashboard</Link></li> - <li><Link to="/income">Income</Link></li> + <li><Link to="/income">Income</Link></li> <li><Link to="/expenses">Expenses</Link></li> - <li><Link to="/budget">Budget</Link></li> - <li><Link to="/profile">Profile</Link></li> + <li><Link to="/budget">Budget</Link></li> + <li><Link to="/transactions">Transactions</Link></li> + <li><Link to="/profile">Profile</Link></li> <li> <Link to="/" onClick={() => { localStorage.removeItem("token"); setToken(null); }}> Logout @@ -68,8 +70,9 @@ function App() { <Route path="/dashboard" element={<Dashboard setToken={setToken} />} /> <Route path="/income" element={<Income />} /> <Route path="/expenses" element={<Expenses />} /> - <Route path="/budget" element={<Budget />} /> - <Route path="/profile" element={<Profile setToken={setToken} />} /> + <Route path="/budget" element={<Budget />} /> + <Route path="/transactions" element={<Transactions setToken={setToken}/>} /> + <Route path="/profile" element={<Profile />} /> </Routes> </div> </Router> diff --git a/financial-tracker/frontend/src/pages/Transactions.js b/financial-tracker/frontend/src/pages/Transactions.js new file mode 100644 index 0000000000000000000000000000000000000000..b932716714d10dca68350154d20b6ebbad0d9b71 --- /dev/null +++ b/financial-tracker/frontend/src/pages/Transactions.js @@ -0,0 +1,274 @@ +import React, { useState, useEffect } from "react"; +import axios from "axios"; +import { jwtDecode } from 'jwt-decode'; +import { useNavigate } from "react-router-dom"; +import ReactPaginate from 'react-paginate'; +import "../styles/Transactions.css"; + +const Transactions = ({ setToken }) => { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [transactions, setTransactions] = useState([]); + const [filteredTransactions, setFilteredTransactions] = useState([]); + const transactionsPerPage = 10; + const [currentPage, setCurrentPage] = useState(0); + const [sortConfig, setSortConfig] = useState({ key: "", direction: "asc" }); + const [showFilters, setShowFilters] = useState(false); + const [filters, setFilters] = useState({ + category: "", + minAmount: "", + maxAmount: "", + startDate: "", + endDate: "", + dateRange: "" + }); + const navigate = useNavigate(); + + useEffect(() => { + const token = localStorage.getItem("token"); + + if (!token) { + navigate("/login"); + return; + } + + axios + .get("http://localhost:8000/auth/me", { + headers: { Authorization: `Bearer ${token}` }, + }) + .then((response) => { + setUser(response.data); + fetchTransactions(token); + setLoading(false); + }) + .catch(() => { + localStorage.removeItem("token"); + setToken(null); + navigate("/login"); + }); + }, [navigate, setToken]); + + const fetchTransactions = async (token) => { + try { + console.log("Fetching transactions"); + + if (!token) { + console.error("No token found, user might not be logged in."); + return; + } + + const decoded = jwtDecode(token); + const user_id = decoded.sub; + if (!user_id) { + console.error("User ID is undefined. Check JWT structure."); + return; + } + + const response = await axios.get("http://localhost:8000/transactions", { + headers: { Authorization: `Bearer ${token}` }, + }); + + console.log("Response from transactions:", response.data); + + const incomes = response.data.incomes.map(income => ({ + ...income, + type: 'income' + })) || []; + + const expenses = response.data.expenses.map(expense => ({ + ...expense, + type: 'expense' + })) || []; + + const combinedTransactions = [...incomes, ...expenses]; + combinedTransactions.sort((a, b) => new Date(b.date) - new Date(a.date)); + + setTransactions(combinedTransactions); + setFilteredTransactions(combinedTransactions); + } catch (error) { + console.error("Fetching transactions:", error); + setError("Error fetching transactions"); + } + }; + + const applyFilters = () => { + let filtered = transactions.filter(transaction => { + const { category, minAmount, maxAmount, startDate, endDate } = filters; + return ( + (!category || + (transaction.type === 'income' ? transaction.source.toLowerCase().includes(category.toLowerCase()) : transaction.category.toLowerCase().includes(category.toLowerCase()))) && + (!minAmount || parseFloat(transaction.amount) >= parseFloat(minAmount)) && + (!maxAmount || parseFloat(transaction.amount) <= parseFloat(maxAmount)) && + (!startDate || new Date(transaction.date) >= new Date(startDate)) && + (!endDate || new Date(transaction.date) <= new Date(endDate)) + ); + }); + + setFilteredTransactions(filtered); + }; + + const sortData = (key) => { + let direction = "asc"; + if (sortConfig.key === key && sortConfig.direction === "asc") { + direction = "desc"; + } + + const sortedData = [...filteredTransactions].sort((a, b) => { + if (key === "date") { + return direction === "asc" + ? new Date(a[key]) - new Date(b[key]) + : new Date(b[key]) - new Date(a[key]); + } else if (key === "amount") { + return direction === "asc" + ? parseFloat(a[key]) - parseFloat(b[key]) + : parseFloat(b[key]) - parseFloat(a[key]); + } + return 0; + }); + + setFilteredTransactions(sortedData); + setSortConfig({ key, direction }); + }; + + const handlePageClick = (event) => { + const selectedPage = event.selected; + setCurrentPage(selectedPage); + }; + + const formatCurrency = (amount, currency) => { + return new Intl.NumberFormat('en-US', { + style: 'currency', + currency: currency || 'USD' + }).format(amount); + }; + + const formatDate = (dateString) => { + const options = { year: 'numeric', month: 'short', day: 'numeric' }; + return new Date(dateString).toLocaleDateString(undefined, options); + }; + + + // Paginate filtered transactions + const currentTransactions = filteredTransactions.slice(currentPage * transactionsPerPage, (currentPage + 1) * transactionsPerPage); + + return ( + <div className="transactions-container"> + <h1>Transaction History</h1> + + {loading ? <p>Loading...</p> : ( + <> + {/* Filters Section */} + <div className="filter-toggle"> + <button onClick={() => setShowFilters(!showFilters)}> + {showFilters ? "Hide Filters" : "Show Filters"} + </button> + </div> + + {showFilters && ( + <div className="filter-dropdown"> + <div className="filter-column"> + <label>Category/Source</label> + <input + type="text" + placeholder="Search category/source" + value={filters.category} + onChange={(e) => setFilters({ ...filters, category: e.target.value })} + /> + </div> + + <div className="filter-column"> + <label>Min Amount</label> + <input + type="number" + step="0.01" + placeholder="Min amount" + value={filters.minAmount} + onChange={(e) => setFilters({ ...filters, minAmount: e.target.value })} + /> + + <label>Max Amount</label> + <input + type="number" + step="0.01" + placeholder="Max amount" + value={filters.maxAmount} + onChange={(e) => setFilters({ ...filters, maxAmount: e.target.value })} + /> + </div> + + <div className="filter-column"> + <label>Start Date</label> + <input + type="date" + value={filters.startDate} + onChange={(e) => setFilters({ ...filters, startDate: e.target.value })} + /> + + <label>End Date</label> + <input + type="date" + value={filters.endDate} + onChange={(e) => setFilters({ ...filters, endDate: e.target.value })} + /> + </div> + + <div className="apply-filters-container"> + <button className="apply-filters-btn" onClick={applyFilters}>Apply Filters</button> + </div> + </div> + )} + + {/* Transactions Table */} + <div className="table-responsive"> + <table className="transactions-table"> + <thead> + <tr> + <th onClick={() => sortData("date")} className="sortable"> + Date {sortConfig.key === "date" && (sortConfig.direction === "asc" ? "↑" : "↓")} + </th> + <th>Description</th> + <th onClick={() => sortData("amount")} className="sortable"> + Amount {sortConfig.key === "amount" && (sortConfig.direction === "asc" ? "↑" : "↓")} + </th> + </tr> + </thead> + <tbody> + {currentTransactions.length > 0 ? ( + currentTransactions.map((transaction) => ( + <tr key={transaction.id}> + <td>{formatDate(transaction.date)}</td> + <td>{transaction.type === 'income' ? transaction.source : transaction.category}</td> + <td className={`transaction-amount ${transaction.type}`}> + {transaction.type === 'income' ? '+' : '-'}{formatCurrency(transaction.amount, transaction.currency)}</td> + </tr> + )) + ) : ( + <tr> + <td colSpan="4" style={{ textAlign: "center" }}>No transactions found.</td> + </tr> + )} + </tbody> + </table> + </div> + + {/* Pagination */} + <ReactPaginate + previousLabel={"<< Previous"} + nextLabel={"Next >>"} + breakLabel={"..."} + pageCount={Math.ceil(filteredTransactions.length / transactionsPerPage)} + marginPagesDisplayed={2} + pageRangeDisplayed={5} + onPageChange={handlePageClick} + containerClassName={"pagination"} + activeClassName={"active"} + forcePage={currentPage} + /> + </> + )} + </div> + ); +}; + +export default Transactions; \ No newline at end of file diff --git a/financial-tracker/frontend/src/styles/Transactions.css b/financial-tracker/frontend/src/styles/Transactions.css new file mode 100644 index 0000000000000000000000000000000000000000..269750f2983e454de661566e8cdf4f5f7e36d7d0 --- /dev/null +++ b/financial-tracker/frontend/src/styles/Transactions.css @@ -0,0 +1,331 @@ +/* Main Container */ +.transactions-container { + width: 80%; + margin: auto; +} + +/* Header */ +.transactions-container h1 { + font-size: 2.5em; + text-align: center; + margin-bottom: 20px; +} + +/* Filters Section */ +.filters-section { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 20px; + margin-bottom: 30px; + padding: 20px; + border-radius: 10px; + background-color: #f9f9f9; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.filter-group { + flex: 1; + min-width: 200px; +} + +.filter-group label { + display: block; + font-weight: bold; + margin-bottom: 8px; + font-size: 14px; +} + +.filter-group select, +.filter-group input { + width: 100%; + padding: 8px; + font-size: 14px; + border: 1px solid #ccc; + border-radius: 5px; + box-sizing: border-box; +} + +.date-filter-group { + display: flex; + gap: 10px; +} + +.custom-date-inputs { + margin-top: 10px; +} + +.custom-date-inputs label { + display: block; + margin-top: 8px; + font-size: 14px; + color: #555; +} + +.custom-date-inputs input[type="date"] { + width: 100%; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + margin-top: 4px; +} + +.date-range-selector { + margin-bottom: 15px; +} + +.date-range-selector select { + width: 100%; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; + background-color: white; +} + +.custom-date-range { + display: flex; + gap: 10px; + margin-top: 10px; +} + +.custom-date-range input { + flex: 1; + padding: 8px; + border: 1px solid #ddd; + border-radius: 4px; +} + + +.apply-filters-container { + display: flex; + gap: 10px; + margin-top: 15px; + align-items: center; + +} + +.apply-filters-btn { + flex: 1; + /* This makes them equally share available space */ + padding: 10px 15px; + border: none; + border-radius: 5px; + font-size: 14px; + font-weight: 500; + cursor: pointer; + text-align: center; + transition: all 0.3s ease; + height: 40px; + /* Fixed height */ + box-sizing: border-box; + /* Include padding in height calculation */ + display: inline-flex; + align-items: center; + justify-content: center; + line-height: 1; + vertical-align: middle; + position: relative; + top: -7px; + /* Moves button up by 5px */ +} + +.apply-filters-btn { + background-color: #4CAF50; + /* Green */ + color: white; +} + +.apply-filters-btn:hover { + background-color: #45a049; +} + + + +/* Transactions Table */ +.transactions-table-container { + overflow-x: auto; + margin-top: 20px; +} + +.transactions-table { + width: 100%; + border-collapse: collapse; + margin-bottom: 20px; +} + +.transactions-table th, +.transactions-table td { + padding: 12px 15px; + text-align: left; + border-bottom: 1px solid #ddd; +} + +.transactions-table th { + background-color: #f4f4f4; + font-weight: bold; + position: sticky; + top: 0; +} + +.transactions-table tr:hover { + background-color: #f1f1f1; +} + +.transaction-amount.income { + color: #27ae60; +} + +.transaction-amount.expense { + color: #e74c3c; +} + +/* Type-specific styling */ +.tx-income { + border-left: 4px solid #28a745; +} + +.tx-expense { + border-left: 4px solid #dc3545; +} + +.income-amount { + color: #28a745; + font-weight: bold; +} + +.expense-amount { + color: #dc3545; + font-weight: bold; +} + +/* Pagination */ + +.pagination { + display: flex; + justify-content: center; + align-items: center; + gap: 15px; + margin-top: 20px; +} + +.pagination button { + padding: 8px 15px; + background-color: #007bff; + color: white; + border: none; + border-radius: 4px; + cursor: pointer; +} + +.pagination button:disabled { + background-color: #cccccc; + cursor: not-allowed; +} + +.pagination-info { + font-size: 14px; +} + +/* No Transactions Message */ +.no-transactions { + text-align: center; + padding: 40px; + font-size: 18px; + color: #666; +} + +/* Loading State */ +.loading { + text-align: center; + padding: 40px; + font-size: 18px; +} + +/* Error Message */ +.error-message { + text-align: center; + padding: 20px; + background-color: #f8d7da; + color: #721c24; + border-radius: 5px; + margin-bottom: 20px; +} + +/* Responsive Adjustments */ +@media (max-width: 768px) { + .transactions-container { + width: 95%; + } + + .filters-section { + flex-direction: column; + gap: 15px; + } + + .filter-group { + width: 100%; + } + + .transactions-table th, + .transactions-table td { + padding: 8px; + font-size: 14px; + } + + .date-filter-group { + flex-direction: column; + gap: 10px; + } +} + +@media (max-width: 480px) { + .transactions-container { + width: 100%; + padding: 10px; + } + + .transactions-table { + font-size: 13px; + } + + .pagination { + flex-wrap: wrap; + } +} + +.tx-income { + background-color: rgba(40, 167, 69, 0.05); +} + +.tx-expense { + background-color: rgba(220, 53, 69, 0.05); +} + +.income-amount { + color: #28a745; + font-weight: 600; +} + +.expense-amount { + color: #dc3545; + font-weight: 600; +} + +.income-type { + color: #28a745; +} + +.expense-type { + color: #dc3545; +} + +.no-transactions { + text-align: center; + padding: 20px; + color: #6c757d; +} + +.table-responsive { + overflow-x: auto; + margin: 20px 0; +} \ No newline at end of file diff --git a/financial-tracker/notification-service/.env b/financial-tracker/notification-service/.env deleted file mode 100644 index 9361d60bd3551bb35d6ba377df05f41379363512..0000000000000000000000000000000000000000 --- a/financial-tracker/notification-service/.env +++ /dev/null @@ -1,2 +0,0 @@ -MONGO_URI=mongodb://mongo:27017/notifications -JWT_SECRET=your_jwt_secret diff --git a/financial-tracker/notification-service/Dockerfile b/financial-tracker/notification-service/Dockerfile index cece852b7a973bc2c6055207f9c70c29f5e79088..65bb80b55761cb37a53583e0f968d541c5c96b7a 100644 --- a/financial-tracker/notification-service/Dockerfile +++ b/financial-tracker/notification-service/Dockerfile @@ -1,14 +1,18 @@ -FROM node:20-alpine - +FROM python:3.12 + WORKDIR /app - -COPY package*.json ./ - -RUN npm install - -COPY . . - + +# Set default database URL +ENV DATABASE_URL=postgresql://user:password@notification-db:5432/notification_db + +# Copy requirements.txt from root and install dependencies +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# Copy only the current service's code +COPY ./notification-service/app /app + +# Expose the service port EXPOSE 5005 -EXPOSE 8080 - -CMD ["npm", "start"] + +CMD ["python", "app.py"] \ No newline at end of file diff --git a/financial-tracker/notification-service/app/controllers/notificationController.js b/financial-tracker/notification-service/app/controllers/notificationController.js deleted file mode 100644 index 8ef39d141a8a1133e55e27c0f9fc59b2a9960534..0000000000000000000000000000000000000000 --- a/financial-tracker/notification-service/app/controllers/notificationController.js +++ /dev/null @@ -1,38 +0,0 @@ -const Notification = require("../models/Notification"); - -exports.createNotification = async (req, res) => { - try { - const notification = new Notification(req.body); - await notification.save(); - res.status(201).json(notification); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}; - -exports.getNotifications = async (req, res) => { - try { - const notifications = await Notification.find({ userId: req.params.userId }); - res.json(notifications); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}; - -exports.markAsRead = async (req, res) => { - try { - await Notification.findByIdAndUpdate(req.params.id, { read: true }); - res.status(200).json({ message: "Notification marked as read" }); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}; - -exports.deleteNotification = async (req, res) => { - try { - await Notification.findByIdAndDelete(req.params.id); - res.status(200).json({ message: "Notification deleted" }); - } catch (err) { - res.status(500).json({ error: err.message }); - } -}; diff --git a/financial-tracker/notification-service/app/middleware/auth.js b/financial-tracker/notification-service/app/middleware/auth.js deleted file mode 100644 index 4687a20f2dad73ae0a3404cd67cdc311490a41bf..0000000000000000000000000000000000000000 --- a/financial-tracker/notification-service/app/middleware/auth.js +++ /dev/null @@ -1,14 +0,0 @@ -const jwt = require("jsonwebtoken"); - -module.exports = (req, res, next) => { - const token = req.header("Authorization")?.replace("Bearer ", ""); - if (!token) return res.status(401).json({ error: "Access denied. No token provided." }); - - try { - const decoded = jwt.verify(token, process.env.JWT_SECRET); - req.user = decoded; - next(); - } catch (err) { - res.status(400).json({ error: "Invalid token." }); - } -}; \ No newline at end of file diff --git a/financial-tracker/notification-service/app/models/Notification.js b/financial-tracker/notification-service/app/models/Notification.js deleted file mode 100644 index 59bf51ad33e849bd7f565d7545110f4af00ac092..0000000000000000000000000000000000000000 --- a/financial-tracker/notification-service/app/models/Notification.js +++ /dev/null @@ -1,11 +0,0 @@ -const mongoose = require("mongoose"); - -const NotificationSchema = new mongoose.Schema({ - userId: { type: mongoose.Schema.Types.ObjectId, ref: "User", required: true }, - message: { type: String, required: true }, - type: { type: String, enum: ["budget", "transaction", "bill", "general"], default: "general" }, - date: { type: Date, default: Date.now }, - read: { type: Boolean, default: false }, -}); - -module.exports = mongoose.model("Notification", NotificationSchema); \ No newline at end of file diff --git a/financial-tracker/notification-service/app/routes/notifications.js b/financial-tracker/notification-service/app/routes/notifications.js deleted file mode 100644 index cf13216926ced3d57ba1889bbef8a45c9924be85..0000000000000000000000000000000000000000 --- a/financial-tracker/notification-service/app/routes/notifications.js +++ /dev/null @@ -1,11 +0,0 @@ -const express = require("express"); -const router = express.Router(); -const controller = require("../controllers/notificationController"); -const auth = require("../middleware/auth"); - -router.post("/", auth, controller.createNotification); -router.get("/:userId", auth, controller.getNotifications); -router.patch("/read/:id", auth, controller.markAsRead); -router.delete("/:id", auth, controller.deleteNotification); - -module.exports = router; diff --git a/financial-tracker/notification-service/package.json b/financial-tracker/notification-service/package.json deleted file mode 100644 index 8f5539a03b6c9ee50bfe8ba05487407bc76daf03..0000000000000000000000000000000000000000 --- a/financial-tracker/notification-service/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "notification-service", - "version": "1.0.0", - "description": "Notification service for Financial Tracker", - "main": "app.js", - "scripts": { - "start": "node app.js", - "dev": "nodemon app.js" - }, - "dependencies": { - "cors": "^2.8.5", - "express": "^4.18.2", - "jsonwebtoken": "^9.0.2", - "nodemailer": "^6.9.7", - "pg": "^8.11.3", - "ws": "^8.14.2", - "dotenv": "^16.3.1" - }, - "devDependencies": { - "nodemon": "^3.0.1" - } -} \ No newline at end of file diff --git a/financial-tracker/notification-service/routes/notifications.js b/financial-tracker/notification-service/routes/notifications.js deleted file mode 100644 index f376597018ae012baa982bb3a789fd90fff815ce..0000000000000000000000000000000000000000 --- a/financial-tracker/notification-service/routes/notifications.js +++ /dev/null @@ -1,199 +0,0 @@ -const express = require('express'); -const router = express.Router(); -const { Pool } = require('pg'); -const jwt = require('jsonwebtoken'); - -// PostgreSQL connection -const pool = new Pool({ - user: "user", - host: "notification-db", - database: "notification_db", - password: "password", - port: 5432, -}); - -// Get all notifications -router.get('/', async (req, res) => { - try { - const result = await pool.query('SELECT * FROM notifications ORDER BY created_at DESC'); - res.json(result.rows); - } catch (error) { - console.error('Get all notifications error:', error); - res.status(500).json({ error: 'Failed to fetch notifications' }); - } -}); - -// Create notification -router.post('/', async (req, res) => { - try { - const { user_id, title, message, type, priority = 'normal', metadata = {} } = req.body; - - // Check user's notification preferences - const prefsResult = await pool.query( - 'SELECT * FROM notification_preferences WHERE user_id = $1', - [user_id] - ); - - const prefs = prefsResult.rows[0] || { - budget_alerts: true, - transaction_alerts: true, - bill_reminders: true, - email_notifications: true, - push_notifications: true - }; - - // Create notification in database - const result = await pool.query( - `INSERT INTO notifications - (user_id, title, message, type, priority, metadata) - VALUES ($1, $2, $3, $4, $5, $6) - RETURNING *`, - [user_id, title, message, type, priority, metadata] - ); - - const notification = result.rows[0]; - res.status(201).json(notification); - } catch (error) { - console.error('Create notification error:', error); - res.status(500).json({ error: 'Failed to create notification' }); - } -}); - -// Get user's notifications -router.get('/:userId', async (req, res) => { - try { - const { userId } = req.params; - const { unread_only } = req.query; - - let query = 'SELECT * FROM notifications WHERE user_id = $1'; - if (unread_only === 'true') { - query += ' AND read = FALSE'; - } - query += ' ORDER BY created_at DESC'; - - const result = await pool.query(query, [userId]); - res.json(result.rows); - } catch (error) { - console.error('Get notifications error:', error); - res.status(500).json({ error: 'Failed to fetch notifications' }); - } -}); - -// Mark notification as read -router.put('/:id/read', async (req, res) => { - try { - const { id } = req.params; - const result = await pool.query( - 'UPDATE notifications SET read = TRUE WHERE id = $1 RETURNING *', - [id] - ); - - if (result.rows.length === 0) { - return res.status(404).json({ error: 'Notification not found' }); - } - - res.json(result.rows[0]); - } catch (error) { - console.error('Mark as read error:', error); - res.status(500).json({ error: 'Failed to update notification' }); - } -}); - -// Delete notification -router.delete('/:id', async (req, res) => { - try { - const { id } = req.params; - const result = await pool.query( - 'DELETE FROM notifications WHERE id = $1 RETURNING *', - [id] - ); - - if (result.rows.length === 0) { - return res.status(404).json({ error: 'Notification not found' }); - } - - res.json({ message: 'Notification deleted successfully' }); - } catch (error) { - console.error('Delete notification error:', error); - res.status(500).json({ error: 'Failed to delete notification' }); - } -}); - -// Get notification preferences -router.get('/preferences/:userId', async (req, res) => { - try { - const { userId } = req.params; - const result = await pool.query( - 'SELECT * FROM notification_preferences WHERE user_id = $1', - [userId] - ); - - if (result.rows.length === 0) { - // Return default preferences if none set - return res.json({ - user_id: userId, - budget_alerts: true, - transaction_alerts: true, - bill_reminders: true, - email_notifications: true, - push_notifications: true - }); - } - - res.json(result.rows[0]); - } catch (error) { - console.error('Get preferences error:', error); - res.status(500).json({ error: 'Failed to fetch notification preferences' }); - } -}); - -// Update notification preferences -router.put('/preferences/:userId', async (req, res) => { - try { - const { userId } = req.params; - const { - budget_alerts, - transaction_alerts, - bill_reminders, - email_notifications, - push_notifications - } = req.body; - - const result = await pool.query( - `INSERT INTO notification_preferences - (user_id, budget_alerts, transaction_alerts, bill_reminders, email_notifications, push_notifications) - VALUES ($1, $2, $3, $4, $5, $6) - ON CONFLICT (user_id) - DO UPDATE SET - budget_alerts = $2, - transaction_alerts = $3, - bill_reminders = $4, - email_notifications = $5, - push_notifications = $6 - RETURNING *`, - [userId, budget_alerts, transaction_alerts, bill_reminders, email_notifications, push_notifications] - ); - - res.json(result.rows[0]); - } catch (error) { - console.error('Update preferences error:', error); - res.status(500).json({ error: 'Failed to update notification preferences' }); - } -}); - -// Mark all notifications as read -router.put('/read-all/:userId', async (req, res) => { - try { - const { userId } = req.params; - await pool.query( - 'UPDATE notifications SET read = TRUE WHERE user_id = $1', - [userId] - ); - res.json({ message: 'All notifications marked as read' }); - } catch (error) { - console.error('Mark all as read error:', error); - res.status(500).json({ error: 'Failed to update notifications' }); - } -}); - -module.exports = router; \ No newline at end of file diff --git a/financial-tracker/transaction-service/Dockerfile b/financial-tracker/transaction-service/Dockerfile index e9e7b574dd32be04b6e02f5de089e86e5cef09e4..5f14f49e26d470d5a0f7b6ae4c2cf150afdd6df1 100644 --- a/financial-tracker/transaction-service/Dockerfile +++ b/financial-tracker/transaction-service/Dockerfile @@ -10,11 +10,9 @@ COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt # Copy only the current service's code -COPY ./transaction-service/app/app.py ./app.py +COPY ./transaction-service/app /app # Expose the service port EXPOSE 5002 -CMD ["python", "app.py"] -CMD ["sh", "-c", "echo Starting Transaction Service && ls -l && python app.py"] - +CMD ["python", "app.py"] \ No newline at end of file diff --git a/financial-tracker/transaction-service/app/app.py b/financial-tracker/transaction-service/app/app.py index beaadaac746eb0f7848604975aa8378f09ec7ab5..b373f794d063a9affe50f94dc13643c7d5d735e4 100644 --- a/financial-tracker/transaction-service/app/app.py +++ b/financial-tracker/transaction-service/app/app.py @@ -8,7 +8,6 @@ from flask_jwt_extended import ( jwt_required, get_jwt_identity, JWTManager ) from flask_sqlalchemy import SQLAlchemy -from sqlalchemy import Enum import enum import logging import os @@ -135,7 +134,6 @@ class EndRepeatEnum(enum.Enum): NEVER = "never" ON_DATE = "on_date" - # DATABASE MODELS class Expense(db.Model): id = db.Column(db.Integer, primary_key=True) @@ -205,8 +203,7 @@ class Income(db.Model): "end_date": self.end_date.strftime('%Y-%m-%d') if self.end_date else None } - -#INCOMES +#INCOME ROUTES @app.route('/income', methods=['POST']) @jwt_required() def add_income(): @@ -419,6 +416,7 @@ def edit_income(income_id): income.recurring = data["recurring"] db.session.commit() + app.logger.info(f"Income updated: {income.to_dict()}") return jsonify({"message": "Income updated", "income": income.to_dict()}), 200 @@ -822,12 +820,48 @@ def get_categories(): return jsonify({"error": "An error occurred while fetching categories"}), 500 +#TRANSACTIONS ROUTE +@app.route('/transactions', methods=['GET']) +@jwt_required() +def get_transactions(): + """Retrieve all transactions (expenses and incomes) for the logged-in user""" + try: + user_id = get_jwt_identity() # Extract user_id from JWT token + user_id = int(user_id) + app.logger.info(f"Retrieved user_id from JWT: {user_id}") # Log user_id + + if not user_id: + app.logger.error("User ID is None or invalid.") + return jsonify({"error": "Invalid user authentication"}), 401 + + # Retrieve expenses + expenses_response, status_code = get_expenses() + expenses_data = expenses_response.get_json() if status_code == 200 else {} + + app.logger.info(f"Expenses Data: {expenses_data}") + + # Retrieve incomes + incomes_response, status_code = get_income() + incomes_data = incomes_response.get_json() if status_code == 200 else {} + + app.logger.info(f"Incomes Data: {incomes_data}") + + return jsonify({ + "incomes": incomes_data.get("incomes", []), + "expenses": expenses_data.get("expenses", []), + }), 200 + + except Exception as e: + app.logger.error(f"Error retrieving transactions: {e}", exc_info=True) + return jsonify({"error": "An error occurred while fetching transactions"}), 500 + + # HEALTH CHECK ROUTE @app.route('/health') def health(): """Health check endpoint to verify service is running.""" app.logger.info("Health check requested") - return jsonify({"status": "User service is running"}), 200 + return jsonify({"status": "Transaction service is running"}), 200 # INITIALISATION & RUNNING APP diff --git a/financial-tracker/transaction-service/requirements.txt b/financial-tracker/transaction-service/requirements.txt deleted file mode 100644 index 5f1fd100f7cb7cccbefae104a4436fd8d80d1a93..0000000000000000000000000000000000000000 --- a/financial-tracker/transaction-service/requirements.txt +++ /dev/null @@ -1,15 +0,0 @@ -Flask==3.0.0 -Flask-SQLAlchemy==3.1.1 -Flask-JWT-Extended==4.6.0 -Flask-CORS==4.0.0 -psycopg2-binary==2.9.9 -python-dotenv==1.0.0 -requests==2.31.0 -SQLAlchemy==2.0.23 -Werkzeug==3.0.1 -python-dateutil==2.8.2 -pytest==7.4.3 -pytest-cov==4.1.0 -black==23.11.0 -flake8==6.1.0 -mypy==1.7.1 \ No newline at end of file diff --git a/financial-tracker/user-service/requirements.txt b/financial-tracker/user-service/requirements.txt deleted file mode 100644 index 5d623d49e3224c9a7b1f6cee6d0ff8596b82beee..0000000000000000000000000000000000000000 --- a/financial-tracker/user-service/requirements.txt +++ /dev/null @@ -1,8 +0,0 @@ -Flask==3.1.0 -Flask-JWT-Extended==4.7.1 -Flask-SQLAlchemy==3.1.1 -Flask-CORS==5.0.1 -psycopg2-binary==2.9.9 -python-dotenv==1.0.1 -pytz==2024.1 -Werkzeug>=3.1.0 \ No newline at end of file