Skip to main content
Glama
jcvalerio

MoneyWiz MCP Server

by jcvalerio
test_savings_service.py10.3 kB
"""Tests for SavingsService - TDD approach for Phase 3 features.""" from datetime import datetime, timedelta from decimal import Decimal from unittest.mock import AsyncMock, patch import pytest from moneywiz_mcp_server.models.analytics_result import ( CategoryExpense, IncomeExpenseAnalysis, ) from moneywiz_mcp_server.models.transaction import DateRange from moneywiz_mcp_server.services.savings_service import SavingsService class TestSavingsService: """Test suite for SavingsService following TDD principles.""" @pytest.fixture def mock_db_manager(self): """Create mock database manager.""" mock_db = AsyncMock() return mock_db @pytest.fixture def savings_service(self, mock_db_manager): """Create SavingsService instance with mocked dependencies.""" return SavingsService(mock_db_manager) @pytest.fixture def sample_income_expense_data(self): """Sample income vs expense data for testing.""" from datetime import datetime from moneywiz_mcp_server.models.currency_types import CurrencyAmounts return IncomeExpenseAnalysis( total_income=CurrencyAmounts({"USD": Decimal("5000.00")}), total_expenses=CurrencyAmounts({"USD": Decimal("4000.00")}), net_savings=CurrencyAmounts({"USD": Decimal("1000.00")}), savings_rate={"USD": Decimal("20.0")}, income_breakdown=[], expense_breakdown=[], analysis_period=DateRange( start_date=datetime(2024, 1, 1), end_date=datetime(2024, 3, 31) ), currencies_found=["USD"], primary_currency="USD", monthly_averages={}, ) @pytest.fixture def sample_category_expenses(self): """Sample category expense data for testing.""" return [ CategoryExpense( category_name="Groceries", category_id=1, total_amount=Decimal("800.00"), transaction_count=20, average_amount=Decimal("40.00"), percentage_of_total=Decimal("20.0"), ), CategoryExpense( category_name="Entertainment", category_id=2, total_amount=Decimal("600.00"), transaction_count=15, average_amount=Decimal("40.00"), percentage_of_total=Decimal("15.0"), ), CategoryExpense( category_name="Dining Out", category_id=3, total_amount=Decimal("400.00"), transaction_count=10, average_amount=Decimal("40.00"), percentage_of_total=Decimal("10.0"), ), ] @pytest.mark.asyncio async def test_get_savings_recommendations_basic( self, savings_service, sample_income_expense_data, sample_category_expenses ): """Test basic savings recommendations generation.""" # Arrange start_date = datetime.now() - timedelta(days=90) end_date = datetime.now() target_rate = 25.0 # Mock transaction service calls savings_service.db_manager = AsyncMock() # Create mock transaction service mock_transaction_service = AsyncMock() mock_transaction_service.get_income_vs_expense.return_value = ( sample_income_expense_data ) mock_transaction_service.get_expense_summary.return_value = { "category_breakdown": sample_category_expenses } # Patch the import to return our mock with patch( "moneywiz_mcp_server.services.transaction_service.TransactionService", return_value=mock_transaction_service, ): # Act result = await savings_service.get_savings_recommendations( start_date=start_date, end_date=end_date, target_savings_rate=target_rate, ) # Assert assert "current_state" in result assert "target_state" in result assert "recommendations" in result assert "insights" in result # Check current state current_state = result["current_state"] assert current_state["savings_rate"] == 20.0 assert current_state["total_income"] == 5000.0 assert current_state["total_expenses"] == 4000.0 # Check target state target_state = result["target_state"] assert target_state["target_savings_rate"] == 25.0 # Check recommendations exist assert len(result["recommendations"]) > 0 # Verify transaction service was called mock_transaction_service.get_income_vs_expense.assert_called_once() # get_expense_summary is called multiple times in the savings service assert mock_transaction_service.get_expense_summary.call_count >= 1 @pytest.mark.asyncio async def test_category_recommendations_high_spending( self, savings_service, sample_category_expenses ): """Test that high spending categories generate recommendations.""" # Arrange - Create a category with >20% spending high_spending_category = CategoryExpense( category_name="High Spending Category", category_id=4, total_amount=Decimal("1200.00"), transaction_count=30, average_amount=Decimal("40.00"), percentage_of_total=Decimal("30.0"), # >20% triggers recommendation ) categories = [high_spending_category, *sample_category_expenses] total_expenses = 4000.0 # Act result = await savings_service._get_category_recommendations( categories, total_expenses ) # Assert recommendations = result["recommendations"] assert len(recommendations) > 0 # Check for high spending category recommendation high_spending_recs = [ r for r in recommendations if r["category"] == "High Spending Category" ] assert len(high_spending_recs) > 0 # Verify recommendation details rec = high_spending_recs[0] assert rec["type"] == "category_reduction" assert rec["priority"] == "high" assert "reducing by 15%" in rec["description"].lower() @pytest.mark.asyncio async def test_discretionary_spending_recommendations( self, savings_service, sample_category_expenses ): """Test that discretionary categories generate specific recommendations.""" # Arrange - Add discretionary categories discretionary_category = CategoryExpense( category_name="Entertainment", category_id=5, total_amount=Decimal("600.00"), transaction_count=15, average_amount=Decimal("40.00"), percentage_of_total=Decimal("15.0"), ) categories = [discretionary_category] total_expenses = 4000.0 # Act result = await savings_service._get_category_recommendations( categories, total_expenses ) # Assert recommendations = result["recommendations"] entertainment_recs = [ r for r in recommendations if r["category"] == "Entertainment" ] if entertainment_recs: # Entertainment is in discretionary list rec = entertainment_recs[0] assert rec["type"] == "discretionary_reduction" assert rec["priority"] == "medium" assert rec["difficulty"] == "easy" assert "tips" in rec @pytest.mark.asyncio async def test_target_savings_calculation(self, savings_service): """Test calculation of needed expense reduction for target savings rate.""" # Arrange income = 5000.0 expenses = 4000.0 target_rate = 25.0 # Want 25% savings rate # Act needed_reduction = savings_service._calculate_needed_expense_reduction( income, expenses, target_rate ) # Assert # Target expenses should be 5000 * (1 - 0.25) = 3750 # Current expenses are 4000 # So need to reduce by 4000 - 3750 = 250 assert needed_reduction == 250.0 @pytest.mark.asyncio async def test_category_saving_tips(self, savings_service): """Test that category-specific saving tips are provided.""" # Test various categories dining_tips = savings_service._get_category_saving_tips("dining out") assert "cook more meals" in dining_tips[0].lower() entertainment_tips = savings_service._get_category_saving_tips("entertainment") assert "free local events" in entertainment_tips[0].lower() groceries_tips = savings_service._get_category_saving_tips("groceries") assert "meal plan" in groceries_tips[0].lower() # Test unknown category unknown_tips = savings_service._get_category_saving_tips("unknown category") assert "review spending" in unknown_tips[0].lower() @pytest.mark.asyncio async def test_fixed_vs_variable_analysis( self, savings_service, sample_category_expenses ): """Test fixed vs variable expense analysis.""" # Arrange start_date = datetime.now() - timedelta(days=90) end_date = datetime.now() # Mock transaction service mock_transaction_service = AsyncMock() mock_transaction_service.get_expense_summary.return_value = { "category_breakdown": sample_category_expenses } with patch( "moneywiz_mcp_server.services.transaction_service.TransactionService", return_value=mock_transaction_service, ): # Act result = await savings_service._analyze_fixed_vs_variable_expenses( start_date, end_date ) # Assert assert "recommendations" in result assert "insights" in result insights = result["insights"] assert "fixed_percentage" in insights assert "variable_percentage" in insights assert insights["fixed_percentage"] + insights["variable_percentage"] == 100.0 if __name__ == "__main__": pytest.main([__file__])

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/jcvalerio/moneywiz-mcp-server'

If you have feedback or need assistance with the MCP directory API, please join our Discord server