diff --git a/financial-tracker/analytics-service/app/app.py b/financial-tracker/analytics-service/app/app.py index 8969cecb4eeae8596661a3a4d66e15dc07435baa..3a70b16774f6865f73b6577966139d198077c4eb 100644 --- a/financial-tracker/analytics-service/app/app.py +++ b/financial-tracker/analytics-service/app/app.py @@ -163,7 +163,8 @@ def get_todays_stats(): # Calculate total income and store transactions for income in incomes: - if income['date'] == today: + income_date = datetime.fromisoformat(income['date'].split('T')[0]).date() + if income_date == today: total_income += income['amount'] app.logger.info(f"Total income for today: £{total_income}") @@ -183,7 +184,8 @@ def get_todays_stats(): # Calculate total expenses and categorise spending for expense in expenses: - if expense['date'] == today: + expense_date = datetime.fromisoformat(expense['date'].split('T')[0]).date() + if expense_date == today: total_expenses += expense['amount'] category = expense.get('category', 'Uncategorised') # Categorise spending @@ -199,7 +201,8 @@ def get_todays_stats(): # Collect income transactions for today for income in incomes: - if income['date'] == today: + income_date = datetime.fromisoformat(income['date'].split('T')[0]).date() + if income_date == today: recent_transactions.append({ 'date': income['date'], 'source': income['source'], diff --git a/financial-tracker/api-gateway/app/app.py b/financial-tracker/api-gateway/app/app.py index b454c42d188d367b74a8c2ccd259e3fdf12e679e..2cc378e0f67c82acacdced4c53f3e764bb2e5e8e 100644 --- a/financial-tracker/api-gateway/app/app.py +++ b/financial-tracker/api-gateway/app/app.py @@ -16,6 +16,7 @@ USER_SERVICE_URL = os.getenv('USER_SERVICE_URL', 'http://user-service:5001') TRANSACTION_SERVICE_URL = os.getenv("TRANSACTION_SERVICE_URL", "http://transaction-service:5002") BUDGET_SERVICE_URL = os.getenv('BUDGET_SERVICE_URL', 'http://budget-service:5003') ANALYTICS_SERVICE_URL = os.getenv('ANALYTICS_SERVICE_URL', 'http://analytics-service:5004') +NOTIFICATION_SERVICE_URL = os.getenv('NOTIFICATION_SERVICE_URL', 'http://notification-service:5005') app.config["JWT_SECRET_KEY"] = os.getenv( "JWT_SECRET_KEY", "3b5e41af18179f530c5881a5191e15f0ab35eed2fefdc068fda254eed3fb1ecb" ) @@ -656,7 +657,7 @@ def get_user_notifications(user_id): try: headers = {"Authorization": token} - response = requests.get(f"{os.getenv('NOTIFICATION_SERVICE_URL', 'http://notification-service:5005')}/notifications/user/{user_id}", headers=headers) + response = requests.get(f"{NOTIFICATION_SERVICE_URL}/notifications/user/{user_id}", headers=headers) return jsonify(response.json()), response.status_code except requests.exceptions.RequestException as e: app.logger.error(f"Error contacting notification-service: {e}") @@ -674,7 +675,7 @@ def mark_notification_read(notification_id): try: headers = {"Authorization": token} - response = requests.put(f"{os.getenv('NOTIFICATION_SERVICE_URL', 'http://notification-service:5005')}/notifications/{notification_id}/read", headers=headers) + response = requests.put(f"{NOTIFICATION_SERVICE_URL}/notifications/{notification_id}/read", headers=headers) return jsonify(response.json()), response.status_code except requests.exceptions.RequestException as e: app.logger.error(f"Error contacting notification-service: {e}") @@ -692,7 +693,7 @@ def delete_notification(notification_id): try: headers = {"Authorization": token} - response = requests.delete(f"{os.getenv('NOTIFICATION_SERVICE_URL', 'http://notification-service:5005')}/notifications/{notification_id}", headers=headers) + response = requests.delete(f"{NOTIFICATION_SERVICE_URL}/notifications/{notification_id}", headers=headers) return jsonify(response.json()), response.status_code except requests.exceptions.RequestException as e: app.logger.error(f"Error contacting notification-service: {e}") @@ -710,7 +711,7 @@ def get_notification_preferences(user_id): try: headers = {"Authorization": token} - response = requests.get(f"{os.getenv('NOTIFICATION_SERVICE_URL', 'http://notification-service:5005')}/notifications/preferences/{user_id}", headers=headers) + response = requests.get(f"{NOTIFICATION_SERVICE_URL}/notifications/preferences/{user_id}", headers=headers) return jsonify(response.json()), response.status_code except requests.exceptions.RequestException as e: app.logger.error(f"Error contacting notification-service: {e}") @@ -728,7 +729,7 @@ def update_notification_preferences(): try: headers = {"Authorization": token, "Content-Type": "application/json"} - response = requests.put(f"{os.getenv('NOTIFICATION_SERVICE_URL', 'http://notification-service:5005')}/notifications/preferences", headers=headers, json=request.get_json()) + response = requests.put(f"{NOTIFICATION_SERVICE_URL}/notifications/preferences", headers=headers, json=request.get_json()) return jsonify(response.json()), response.status_code except requests.exceptions.RequestException as e: app.logger.error(f"Error contacting notification-service: {e}") @@ -746,7 +747,7 @@ def trigger_budget_limit_notification(): try: headers = {"Authorization": token, "Content-Type": "application/json"} - response = requests.post(f"{os.getenv('NOTIFICATION_SERVICE_URL', 'http://notification-service:5005')}/notifications/trigger/budget_limit", headers=headers, json=request.get_json()) + response = requests.post(f"{NOTIFICATION_SERVICE_URL}/notifications/trigger/budget_limit", headers=headers, json=request.get_json()) return jsonify(response.json()), response.status_code except requests.exceptions.RequestException as e: app.logger.error(f"Error contacting notification-service: {e}") @@ -764,7 +765,7 @@ def trigger_large_transaction_notification(): try: headers = {"Authorization": token, "Content-Type": "application/json"} - response = requests.post(f"{os.getenv('NOTIFICATION_SERVICE_URL', 'http://notification-service:5005')}/notifications/trigger/large_transaction", headers=headers, json=request.get_json()) + response = requests.post(f"{NOTIFICATION_SERVICE_URL}/notifications/trigger/large_transaction", headers=headers, json=request.get_json()) return jsonify(response.json()), response.status_code except requests.exceptions.RequestException as e: app.logger.error(f"Error contacting notification-service: {e}") @@ -782,7 +783,7 @@ def trigger_upcoming_bill_notification(): try: headers = {"Authorization": token, "Content-Type": "application/json"} - response = requests.post(f"{os.getenv('NOTIFICATION_SERVICE_URL', 'http://notification-service:5005')}/notifications/trigger/upcoming_bill", headers=headers, json=request.get_json()) + response = requests.post(f"{NOTIFICATION_SERVICE_URL}/notifications/trigger/upcoming_bill", headers=headers, json=request.get_json()) return jsonify(response.json()), response.status_code except requests.exceptions.RequestException as e: app.logger.error(f"Error contacting notification-service: {e}") diff --git a/financial-tracker/docker-compose.yml b/financial-tracker/docker-compose.yml index eaf99d2e3feb03d2706ae9a7619a7a8f6c7c55a0..5127472818b41aad7ac1b17e5655e8c2869e541a 100644 --- a/financial-tracker/docker-compose.yml +++ b/financial-tracker/docker-compose.yml @@ -1,5 +1,3 @@ - - services: api-gateway: build: @@ -101,7 +99,11 @@ services: - notification-db networks: - app-network - + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:5005/health"] + interval: 30s + retries: 3 + timeout: 10s frontend: build: diff --git a/financial-tracker/frontend/src/pages/Budget.js b/financial-tracker/frontend/src/pages/Budget.js index 1ac08a07644f1e689fa5f718034e2dc447ef4183..2d3a91168bee71898a093bb3e354b234b35902bc 100644 --- a/financial-tracker/frontend/src/pages/Budget.js +++ b/financial-tracker/frontend/src/pages/Budget.js @@ -198,7 +198,8 @@ const Budget = () => { } }, [isSessionValid]); - const addBudget = async () => { + const addBudget = async (e) => { + e.preventDefault(); // Prevent default form submission try { const token = localStorage.getItem("token"); if (!token) return console.error("No token found, user might not be logged in."); @@ -320,15 +321,21 @@ const Budget = () => { console.log("Categories Response:", response.data); if (Array.isArray(response.data.categories)) { + console.log("Setting categories:", response.data.categories); setCategories(response.data.categories); } else { console.error("Categories data is not in expected format", response.data); + setCategories([]); } } catch (error) { console.error("Error fetching categories:", error); + setCategories([]); } }; + // Debug: Log categories when they change + console.log("Current categories in component:", categories); + const sortData = (key) => { let direction = "asc"; if (sortConfig.key === key && sortConfig.direction === "asc") { @@ -416,7 +423,9 @@ const Budget = () => { onChange={handleBudgetChange} required > - <option value="" disabled>Select a category</option> + <option value="" disabled> + {categories.length > 0 ? "Select a category" : "No categories available - add expenses first"} + </option> {categories.map((cat) => ( <option key={cat} value={cat}>{cat}</option> ))} diff --git a/financial-tracker/frontend/src/pages/Notifications.js b/financial-tracker/frontend/src/pages/Notifications.js index b15115ccdceff10276c3f80daa78a3895b5e5f72..796c5a246a794fd85a24adbcdca9fd17f6ed5484 100644 --- a/financial-tracker/frontend/src/pages/Notifications.js +++ b/financial-tracker/frontend/src/pages/Notifications.js @@ -29,20 +29,65 @@ const Notifications = () => { const fetchNotifications = async () => { try { + setError(null); // Clear any previous errors const token = localStorage.getItem("token"); - if (!token) return; + if (!token) { + setError("Authentication required. Please log in again."); + window.location.href = "/login"; + return; + } const decoded = jwtDecode(token); const user_id = decoded.sub; + if (!user_id) { + setError("Invalid user session. Please log in again."); + window.location.href = "/login"; + return; + } + + console.log(`Fetching notifications for user ${user_id}...`); + const response = await axios.get(`http://localhost:8000/api/notifications/user/${user_id}`, { - headers: { Authorization: `Bearer ${token}` } + headers: { Authorization: `Bearer ${token}` }, + timeout: 10000 // 10 second timeout }); - setNotifications(response.data.notifications || []); + console.log("Notifications response:", response.data); + + // Handle different response formats gracefully + const notificationsData = response.data.notifications || response.data || []; + + if (Array.isArray(notificationsData)) { + setNotifications(notificationsData); + } else { + console.warn("Unexpected response format:", response.data); + setNotifications([]); + } + } catch (error) { console.error('Error fetching notifications:', error); - setError('Error fetching notifications'); + + // Provide specific error messages based on error type + if (error.code === 'ECONNABORTED') { + setError('Request timeout. Please check your connection and try again.'); + } else if (error.response?.status === 401) { + setError('Authentication expired. Please log in again.'); + setTimeout(() => { + localStorage.removeItem("token"); + window.location.href = "/login"; + }, 2000); + } else if (error.response?.status === 403) { + setError('Access denied. You can only view your own notifications.'); + } else if (error.response?.status === 503) { + setError('Notification service is temporarily unavailable. Please try again in a moment.'); + } else if (error.response?.status >= 500) { + setError('Server error occurred. Please try again later.'); + } else if (error.message?.includes('Network Error')) { + setError('Network connection error. Please check your internet connection.'); + } else { + setError('Unable to load notifications. Please refresh the page and try again.'); + } } finally { setLoading(false); } @@ -51,21 +96,43 @@ const Notifications = () => { const fetchPreferences = async () => { try { const token = localStorage.getItem("token"); - if (!token) return; + if (!token) { + console.warn("No token found for preferences fetch"); + return; + } const decoded = jwtDecode(token); const user_id = decoded.sub; + if (!user_id) { + console.warn("No user_id found in token"); + return; + } + const response = await axios.get(`http://localhost:8000/api/notifications/preferences/${user_id}`, { - headers: { Authorization: `Bearer ${token}` } + headers: { Authorization: `Bearer ${token}` }, + timeout: 5000 }); if (response.data.preferences) { setPreferences(response.data.preferences); + } else { + // Use default preferences if none exist + console.log("No preferences found, using defaults"); + setPreferences({ + budget_limits: true, + large_transactions: true, + upcoming_bills: true + }); } } catch (error) { console.error('Error fetching preferences:', error); - // Use default preferences if error + // Use default preferences on error - don't show error to user for preferences + setPreferences({ + budget_limits: true, + large_transactions: true, + upcoming_bills: true + }); } };