- Add comprehensive DayWrapUpService integrating worktime, activity, and cost tracking - Implement daily wrap-up command with auto-estimation and cost distribution features - Support multiple output formats (summary, detailed, JSON) with rich formatting - Add intelligent recommendations based on daily work patterns and data - Create estimate command for automatic worktime distribution based on activities - Include period wrap-up functionality for multi-day reporting and analysis - Add 15 comprehensive test cases covering all service and CLI functionality - Enable one-command workflow: estimate time, distribute costs, generate reports - Integrate seamlessly with existing worktime, activity, and cost tracking systems Features demonstrated: - Daily summary with 3h30m worktime across 2 issues - Proportional cost distribution (€150: 71.4% to #122, 28.6% to #123) - Activity tracking integration showing 3 activities across 2 issues - Intelligent recommendations for worktime and cost optimization 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
621 lines
26 KiB
Python
621 lines
26 KiB
Python
"""
|
|
Tests for Issue #124 - Single command Day-Wrap-Up
|
|
|
|
This module contains comprehensive tests for the day wrap-up functionality
|
|
that consolidates daily work summaries, activity tracking, cost distribution,
|
|
and reporting into a single convenient command.
|
|
"""
|
|
|
|
import pytest
|
|
import tempfile
|
|
from datetime import datetime, date, timedelta
|
|
from unittest.mock import Mock, patch, MagicMock
|
|
from pathlib import Path
|
|
from decimal import Decimal
|
|
import json
|
|
|
|
from markitect.finance.day_wrapup_commands import DayWrapUpService, wrapup, _display_daily_summary, _display_period_summary
|
|
|
|
|
|
class TestDayWrapUpService:
|
|
"""Test suite for DayWrapUpService."""
|
|
|
|
def setup_method(self):
|
|
"""Set up test fixtures with temporary database."""
|
|
self.temp_db = tempfile.NamedTemporaryFile(suffix='.db', delete=False)
|
|
self.temp_db.close()
|
|
self.db_path = self.temp_db.name
|
|
self.service = DayWrapUpService(self.db_path)
|
|
|
|
def teardown_method(self):
|
|
"""Clean up test fixtures."""
|
|
Path(self.db_path).unlink(missing_ok=True)
|
|
|
|
def test_service_initialization(self):
|
|
"""Test that service initializes properly with all trackers."""
|
|
assert self.service.db_path == self.db_path
|
|
assert self.service.worktime_tracker is not None
|
|
assert self.service.activity_tracker is not None
|
|
assert self.service.session_tracker is not None
|
|
|
|
def test_get_worktime_summary_no_data(self):
|
|
"""Test worktime summary when no data exists."""
|
|
today = date.today()
|
|
summary = self.service._get_worktime_summary(today)
|
|
|
|
assert summary['total_minutes'] == 0
|
|
assert summary['total_hours'] == 0.0
|
|
assert summary['issues_worked'] == 0
|
|
assert summary['entries'] == []
|
|
assert summary['cost_allocated'] is None
|
|
assert summary['cost_per_minute'] is None
|
|
|
|
def test_get_worktime_summary_with_data(self):
|
|
"""Test worktime summary with logged data."""
|
|
today = date.today()
|
|
|
|
# Log some worktime
|
|
self.service.worktime_tracker.log_worktime(124, 90, work_date=today, description="Main work")
|
|
self.service.worktime_tracker.log_worktime(125, 60, work_date=today, description="Side work")
|
|
|
|
summary = self.service._get_worktime_summary(today)
|
|
|
|
assert summary['total_minutes'] == 150 # 90 + 60
|
|
assert summary['total_hours'] == 2.5
|
|
assert summary['issues_worked'] == 2
|
|
assert summary['entries'] == 2
|
|
assert len(summary['issue_breakdown']) == 2
|
|
assert 124 in summary['issue_breakdown']
|
|
assert 125 in summary['issue_breakdown']
|
|
assert summary['issue_breakdown'][124]['minutes'] == 90
|
|
assert summary['issue_breakdown'][125]['minutes'] == 60
|
|
|
|
def test_get_activity_summary_no_data(self):
|
|
"""Test activity summary when no data exists."""
|
|
today = date.today()
|
|
summary = self.service._get_activity_summary(today)
|
|
|
|
assert summary['total_activities'] == 0
|
|
assert summary['unique_issues'] == 0
|
|
assert summary['activities_by_type'] == {}
|
|
assert summary['activities'] == []
|
|
|
|
def test_get_activity_summary_with_data(self):
|
|
"""Test activity summary with logged data."""
|
|
today = date.today()
|
|
|
|
# Log some activities
|
|
from markitect.issues.activity_tracker import ActivityType
|
|
self.service.activity_tracker.log_activity(124, ActivityType.CREATED, activity_date=today, activity_details="Created issue")
|
|
self.service.activity_tracker.log_activity(124, ActivityType.MODIFIED, activity_date=today, activity_details="Updated issue")
|
|
self.service.activity_tracker.log_activity(125, ActivityType.CREATED, activity_date=today, activity_details="Created another")
|
|
|
|
summary = self.service._get_activity_summary(today)
|
|
|
|
assert summary['total_activities'] == 3
|
|
assert summary['unique_issues'] == 2
|
|
assert 'created' in summary['activities_by_type']
|
|
assert 'modified' in summary['activities_by_type']
|
|
assert summary['activities_by_type']['created'] == 2
|
|
assert summary['activities_by_type']['modified'] == 1
|
|
assert len(summary['activities']) == 3
|
|
|
|
def test_get_cost_summary_no_distribution(self):
|
|
"""Test cost summary when no cost distribution exists."""
|
|
today = date.today()
|
|
summary = self.service._get_cost_summary(today)
|
|
|
|
assert summary['daily_total'] == 0.0
|
|
assert summary['issue_costs'] == {}
|
|
assert summary['has_cost_allocation'] is False
|
|
|
|
def test_get_cost_summary_with_distribution(self):
|
|
"""Test cost summary with cost distribution data."""
|
|
today = date.today()
|
|
|
|
# Log worktime and distribute costs
|
|
self.service.worktime_tracker.log_worktime(124, 120, work_date=today) # 2 hours
|
|
self.service.worktime_tracker.log_worktime(125, 60, work_date=today) # 1 hour
|
|
|
|
distribution = self.service.worktime_tracker.distribute_daily_costs(
|
|
work_date=today,
|
|
total_daily_cost=Decimal('90.00') # €90 total
|
|
)
|
|
|
|
summary = self.service._get_cost_summary(today)
|
|
|
|
assert summary['daily_total'] == 90.0
|
|
assert summary['has_cost_allocation'] is True
|
|
assert len(summary['issue_costs']) == 2
|
|
assert summary['issue_costs'][124] == 60.0 # 2/3 of €90
|
|
assert summary['issue_costs'][125] == 30.0 # 1/3 of €90
|
|
|
|
def test_generate_recommendations_no_data(self):
|
|
"""Test recommendation generation with no data."""
|
|
summary = {
|
|
'worktime': {'total_minutes': 0, 'total_hours': 0.0, 'issues_worked': 0},
|
|
'activities': {'total_activities': 0, 'unique_issues': 0},
|
|
'costs': {'has_cost_allocation': False}
|
|
}
|
|
|
|
recommendations = self.service._generate_recommendations(summary)
|
|
|
|
assert len(recommendations) >= 2
|
|
assert any("No worktime logged" in rec for rec in recommendations)
|
|
assert any("No issue activities logged" in rec for rec in recommendations)
|
|
|
|
def test_generate_recommendations_with_data(self):
|
|
"""Test recommendation generation with various data conditions."""
|
|
# Test low worktime
|
|
summary = {
|
|
'worktime': {'total_minutes': 120, 'total_hours': 2.0, 'issues_worked': 1},
|
|
'activities': {'total_activities': 5, 'unique_issues': 1},
|
|
'costs': {'has_cost_allocation': False}
|
|
}
|
|
|
|
recommendations = self.service._generate_recommendations(summary)
|
|
assert any("Low worktime logged" in rec for rec in recommendations)
|
|
assert any("no costs distributed" in rec for rec in recommendations)
|
|
|
|
# Test high worktime
|
|
summary['worktime'] = {'total_minutes': 660, 'total_hours': 11.0, 'issues_worked': 1}
|
|
recommendations = self.service._generate_recommendations(summary)
|
|
assert any("High worktime logged" in rec for rec in recommendations)
|
|
|
|
# Test many issues
|
|
summary['activities'] = {'total_activities': 10, 'unique_issues': 6}
|
|
recommendations = self.service._generate_recommendations(summary)
|
|
assert any("Many issues worked on" in rec for rec in recommendations)
|
|
|
|
def test_perform_auto_estimation_no_activities(self):
|
|
"""Test auto estimation when no activities exist."""
|
|
today = date.today()
|
|
|
|
result = self.service.perform_auto_estimation(today, 8.0)
|
|
|
|
assert result['estimated'] is False
|
|
assert "No active issues found" in result['reason']
|
|
assert result['active_issues'] == []
|
|
|
|
def test_perform_auto_estimation_with_existing_time(self):
|
|
"""Test auto estimation when time is already logged."""
|
|
today = date.today()
|
|
|
|
# Log some worktime first
|
|
self.service.worktime_tracker.log_worktime(124, 60, work_date=today)
|
|
|
|
result = self.service.perform_auto_estimation(today, 8.0)
|
|
|
|
assert result['estimated'] is False
|
|
assert "Time already logged" in result['reason']
|
|
assert result['existing_minutes'] == 60
|
|
|
|
def test_perform_auto_estimation_success(self):
|
|
"""Test successful auto estimation."""
|
|
today = date.today()
|
|
|
|
# Create activities for issues
|
|
from markitect.issues.activity_tracker import ActivityType
|
|
self.service.activity_tracker.log_activity(124, ActivityType.CREATED, activity_date=today)
|
|
self.service.activity_tracker.log_activity(125, ActivityType.MODIFIED, activity_date=today)
|
|
|
|
result = self.service.perform_auto_estimation(today, 6.0)
|
|
|
|
assert result['estimated'] is True
|
|
assert 'estimation_result' in result
|
|
estimation = result['estimation_result']
|
|
assert estimation['total_minutes'] == 360 # 6 hours
|
|
assert estimation['issues_count'] == 2
|
|
assert len(estimation['issue_estimates']) == 2
|
|
|
|
# Verify worktime entries were created
|
|
entries = self.service.worktime_tracker.get_worktime_entries(work_date=today)
|
|
assert len(entries) == 2
|
|
assert all(e.entry_type == "estimated" for e in entries)
|
|
|
|
def test_distribute_daily_costs(self):
|
|
"""Test daily cost distribution functionality."""
|
|
today = date.today()
|
|
|
|
# Log worktime first
|
|
self.service.worktime_tracker.log_worktime(124, 180, work_date=today) # 3 hours
|
|
self.service.worktime_tracker.log_worktime(125, 120, work_date=today) # 2 hours
|
|
# Total: 5 hours (300 minutes)
|
|
|
|
result = self.service.distribute_daily_costs(today, Decimal('150.00'))
|
|
|
|
assert result['total_cost'] == 150.0
|
|
assert result['total_minutes'] == 300
|
|
assert result['cost_per_minute'] == 0.5
|
|
assert result['distributions'][124]['cost_allocated'] == 90.0 # 3/5 * €150
|
|
assert result['distributions'][125]['cost_allocated'] == 60.0 # 2/5 * €150
|
|
|
|
def test_generate_daily_summary_integration(self):
|
|
"""Test complete daily summary generation."""
|
|
today = date.today()
|
|
|
|
# Create comprehensive test data
|
|
from markitect.issues.activity_tracker import ActivityType
|
|
|
|
# Log worktime
|
|
self.service.worktime_tracker.log_worktime(124, 120, work_date=today, description="Main feature")
|
|
self.service.worktime_tracker.log_worktime(125, 60, work_date=today, description="Bug fix")
|
|
|
|
# Log activities
|
|
self.service.activity_tracker.log_activity(124, ActivityType.CREATED, activity_date=today)
|
|
self.service.activity_tracker.log_activity(124, ActivityType.MODIFIED, activity_date=today)
|
|
self.service.activity_tracker.log_activity(125, ActivityType.CLOSED, activity_date=today)
|
|
|
|
# Distribute costs
|
|
self.service.distribute_daily_costs(today, Decimal('90.00'))
|
|
|
|
# Generate summary
|
|
summary = self.service.generate_daily_summary(today)
|
|
|
|
# Verify summary structure
|
|
assert summary['date'] == today
|
|
assert 'worktime' in summary
|
|
assert 'activities' in summary
|
|
assert 'costs' in summary
|
|
assert 'recommendations' in summary
|
|
|
|
# Verify worktime data
|
|
worktime = summary['worktime']
|
|
assert worktime['total_minutes'] == 180
|
|
assert worktime['total_hours'] == 3.0
|
|
assert worktime['issues_worked'] == 2
|
|
assert worktime['cost_allocated'] == 90.0
|
|
|
|
# Verify activity data
|
|
activities = summary['activities']
|
|
assert activities['total_activities'] == 3
|
|
assert activities['unique_issues'] == 2
|
|
|
|
# Verify cost data
|
|
costs = summary['costs']
|
|
assert costs['daily_total'] == 90.0
|
|
assert costs['has_cost_allocation'] is True
|
|
|
|
# Verify recommendations exist
|
|
assert isinstance(summary['recommendations'], list)
|
|
|
|
|
|
class TestDayWrapUpCommands:
|
|
"""Test suite for day wrap-up CLI commands."""
|
|
|
|
@patch('markitect.finance.day_wrapup_commands.DayWrapUpService')
|
|
def test_daily_command_basic(self, mock_service_class):
|
|
"""Test the daily wrap-up command with basic functionality."""
|
|
mock_service = Mock()
|
|
mock_service_class.return_value = mock_service
|
|
|
|
# Mock the summary data
|
|
mock_summary = {
|
|
'date': date.today(),
|
|
'worktime': {
|
|
'total_minutes': 180,
|
|
'total_hours': 3.0,
|
|
'issues_worked': 2,
|
|
'entries': 2,
|
|
'issue_breakdown': {124: {'minutes': 120, 'entries': 1}, 125: {'minutes': 60, 'entries': 1}},
|
|
'cost_allocated': 90.0,
|
|
'cost_per_minute': 0.5
|
|
},
|
|
'activities': {
|
|
'total_activities': 3,
|
|
'unique_issues': 2,
|
|
'activities_by_type': {'created': 2, 'modified': 1},
|
|
'activities': []
|
|
},
|
|
'costs': {
|
|
'daily_total': 90.0,
|
|
'issue_costs': {124: 60.0, 125: 30.0},
|
|
'has_cost_allocation': True
|
|
},
|
|
'recommendations': ["💰 Costs distributed successfully"]
|
|
}
|
|
mock_service.generate_daily_summary.return_value = mock_summary
|
|
|
|
from click.testing import CliRunner
|
|
runner = CliRunner()
|
|
|
|
result = runner.invoke(wrapup, ['daily'])
|
|
|
|
assert result.exit_code == 0
|
|
assert "📊 Daily Wrap-Up" in result.output
|
|
assert "⏰ WORKTIME SUMMARY" in result.output
|
|
assert "📝 ACTIVITIES SUMMARY" in result.output
|
|
assert "💰 COST SUMMARY" in result.output
|
|
assert "💡 RECOMMENDATIONS" in result.output
|
|
|
|
@patch('markitect.finance.day_wrapup_commands.DayWrapUpService')
|
|
def test_daily_command_with_auto_estimate(self, mock_service_class):
|
|
"""Test daily command with auto-estimation enabled."""
|
|
mock_service = Mock()
|
|
mock_service_class.return_value = mock_service
|
|
|
|
# Mock estimation result
|
|
mock_estimation = {
|
|
'estimated': True,
|
|
'estimation_result': {
|
|
'total_minutes': 480,
|
|
'issues_count': 2,
|
|
'issue_estimates': {124: 240, 125: 240}
|
|
}
|
|
}
|
|
mock_service.perform_auto_estimation.return_value = mock_estimation
|
|
|
|
# Mock summary
|
|
mock_service.generate_daily_summary.return_value = {
|
|
'date': date.today(),
|
|
'worktime': {'total_minutes': 0, 'total_hours': 0.0, 'issues_worked': 0, 'entries': []},
|
|
'activities': {'total_activities': 0, 'unique_issues': 0, 'activities_by_type': {}, 'activities': []},
|
|
'costs': {'daily_total': 0.0, 'issue_costs': {}, 'has_cost_allocation': False},
|
|
'recommendations': []
|
|
}
|
|
|
|
from click.testing import CliRunner
|
|
runner = CliRunner()
|
|
|
|
result = runner.invoke(wrapup, ['daily', '--auto-estimate', '--estimate-hours', '8'])
|
|
|
|
assert result.exit_code == 0
|
|
assert "🤖 Auto-estimating worktime" in result.output
|
|
assert "✅ Estimated 8.0h across 2 issues" in result.output
|
|
mock_service.perform_auto_estimation.assert_called_once_with(date.today(), 8.0)
|
|
|
|
@patch('markitect.finance.day_wrapup_commands.DayWrapUpService')
|
|
def test_daily_command_with_cost_distribution(self, mock_service_class):
|
|
"""Test daily command with cost distribution."""
|
|
mock_service = Mock()
|
|
mock_service_class.return_value = mock_service
|
|
|
|
# Mock distribution result
|
|
mock_distribution = {
|
|
'total_cost': 120.0,
|
|
'total_minutes': 240,
|
|
'issues_count': 2,
|
|
'distributions': {124: {'cost_allocated': 80.0}, 125: {'cost_allocated': 40.0}}
|
|
}
|
|
mock_service.distribute_daily_costs.return_value = mock_distribution
|
|
|
|
# Mock summary
|
|
mock_service.generate_daily_summary.return_value = {
|
|
'date': date.today(),
|
|
'worktime': {'total_minutes': 0, 'total_hours': 0.0, 'issues_worked': 0, 'entries': []},
|
|
'activities': {'total_activities': 0, 'unique_issues': 0, 'activities_by_type': {}, 'activities': []},
|
|
'costs': {'daily_total': 0.0, 'issue_costs': {}, 'has_cost_allocation': False},
|
|
'recommendations': []
|
|
}
|
|
|
|
from click.testing import CliRunner
|
|
runner = CliRunner()
|
|
|
|
result = runner.invoke(wrapup, ['daily', '--distribute-cost', '120'])
|
|
|
|
assert result.exit_code == 0
|
|
assert "💰 Distributing €120.00" in result.output
|
|
assert "✅ Distributed €120.00 across 2 issues" in result.output
|
|
mock_service.distribute_daily_costs.assert_called_once()
|
|
|
|
@patch('markitect.finance.day_wrapup_commands.DayWrapUpService')
|
|
def test_daily_command_json_format(self, mock_service_class):
|
|
"""Test daily command with JSON output format."""
|
|
mock_service = Mock()
|
|
mock_service_class.return_value = mock_service
|
|
|
|
mock_summary = {
|
|
'date': date.today(),
|
|
'worktime': {'total_minutes': 120, 'total_hours': 2.0, 'issues_worked': 1, 'entries': 1},
|
|
'activities': {'total_activities': 2, 'unique_issues': 1, 'activities_by_type': {}, 'activities': []},
|
|
'costs': {'daily_total': 0.0, 'issue_costs': {}, 'has_cost_allocation': False},
|
|
'recommendations': []
|
|
}
|
|
mock_service.generate_daily_summary.return_value = mock_summary
|
|
|
|
from click.testing import CliRunner
|
|
runner = CliRunner()
|
|
|
|
result = runner.invoke(wrapup, ['daily', '--format', 'json'])
|
|
|
|
assert result.exit_code == 0
|
|
# Should be valid JSON
|
|
output_data = json.loads(result.output.strip())
|
|
assert 'date' in output_data
|
|
assert 'worktime' in output_data
|
|
assert 'activities' in output_data
|
|
assert 'costs' in output_data
|
|
|
|
@patch('markitect.finance.day_wrapup_commands.DayWrapUpService')
|
|
def test_estimate_command(self, mock_service_class):
|
|
"""Test the estimate command."""
|
|
mock_service = Mock()
|
|
mock_service_class.return_value = mock_service
|
|
|
|
mock_estimation = {
|
|
'estimated': True,
|
|
'estimation_result': {
|
|
'work_date': date.today(),
|
|
'total_minutes': 480, # 8 hours
|
|
'distribution_method': 'activity_based',
|
|
'issue_estimates': {124: 300, 125: 180}, # 5h and 3h
|
|
'issues_count': 2
|
|
}
|
|
}
|
|
mock_service.perform_auto_estimation.return_value = mock_estimation
|
|
|
|
from click.testing import CliRunner
|
|
runner = CliRunner()
|
|
|
|
today = date.today().strftime('%Y-%m-%d')
|
|
result = runner.invoke(wrapup, ['estimate', today, '--hours', '8'])
|
|
|
|
assert result.exit_code == 0
|
|
assert "✅ Estimated worktime" in result.output
|
|
assert "Total Hours: 8.0h" in result.output
|
|
assert "Issues: 2" in result.output
|
|
assert "Estimated Time Distribution:" in result.output
|
|
|
|
@patch('markitect.finance.day_wrapup_commands.DayWrapUpService')
|
|
def test_period_command(self, mock_service_class):
|
|
"""Test the period wrap-up command."""
|
|
mock_service = Mock()
|
|
mock_service_class.return_value = mock_service
|
|
|
|
# Mock worktime report
|
|
mock_worktime_report = {
|
|
'period': '2025-10-01 to 2025-10-04',
|
|
'total_entries': 8,
|
|
'total_time': {'hours': 20, 'minutes': 30, 'total_minutes': 1230},
|
|
'unique_issues': 3,
|
|
'unique_dates': 4,
|
|
'average_minutes_per_day': 307.5
|
|
}
|
|
mock_service.worktime_tracker.get_worktime_report.return_value = mock_worktime_report
|
|
|
|
# Mock activity summary
|
|
mock_activity_summary = {
|
|
'total_activities': 15,
|
|
'unique_issues': 4,
|
|
'activities_by_type': {'created': 8, 'modified': 5, 'closed': 2}
|
|
}
|
|
mock_service.activity_tracker.get_activity_summary.return_value = mock_activity_summary
|
|
|
|
from click.testing import CliRunner
|
|
runner = CliRunner()
|
|
|
|
result = runner.invoke(wrapup, ['period', '2025-10-01', '2025-10-04'])
|
|
|
|
assert result.exit_code == 0
|
|
assert "📈 Period Wrap-Up" in result.output
|
|
assert "⏰ WORKTIME OVERVIEW" in result.output
|
|
assert "📝 ACTIVITIES OVERVIEW" in result.output
|
|
assert "Total Time: 20h 30m" in result.output
|
|
assert "Total Activities: 15" in result.output
|
|
|
|
|
|
class TestDayWrapUpIntegration:
|
|
"""Integration tests for the complete day wrap-up system."""
|
|
|
|
def setup_method(self):
|
|
"""Set up integration test fixtures."""
|
|
self.temp_db = tempfile.NamedTemporaryFile(suffix='.db', delete=False)
|
|
self.temp_db.close()
|
|
self.db_path = self.temp_db.name
|
|
|
|
def teardown_method(self):
|
|
"""Clean up integration test fixtures."""
|
|
Path(self.db_path).unlink(missing_ok=True)
|
|
|
|
def test_complete_day_workflow(self):
|
|
"""Test a complete daily workflow from start to finish."""
|
|
service = DayWrapUpService(self.db_path)
|
|
today = date.today()
|
|
|
|
# 1. Start with empty day
|
|
initial_summary = service.generate_daily_summary(today)
|
|
assert initial_summary['worktime']['total_minutes'] == 0
|
|
assert initial_summary['activities']['total_activities'] == 0
|
|
|
|
# 2. Log some activities
|
|
from markitect.issues.activity_tracker import ActivityType
|
|
service.activity_tracker.log_activity(124, ActivityType.CREATED, activity_date=today, activity_details="Started new feature")
|
|
service.activity_tracker.log_activity(124, ActivityType.MODIFIED, activity_date=today, activity_details="Made progress")
|
|
service.activity_tracker.log_activity(125, ActivityType.CLOSED, activity_date=today, activity_details="Fixed bug")
|
|
|
|
# 3. Perform auto-estimation
|
|
estimation = service.perform_auto_estimation(today, 7.5)
|
|
assert estimation['estimated'] is True
|
|
assert estimation['estimation_result']['total_minutes'] == 450 # 7.5 hours
|
|
|
|
# 4. Distribute costs
|
|
distribution = service.distribute_daily_costs(today, Decimal('112.50')) # €15 per hour
|
|
assert distribution['total_cost'] == 112.5
|
|
assert distribution['cost_per_minute'] == 0.25 # €0.25 per minute
|
|
|
|
# 5. Generate final summary
|
|
final_summary = service.generate_daily_summary(today)
|
|
|
|
# Verify complete summary
|
|
assert final_summary['worktime']['total_hours'] == 7.5
|
|
assert final_summary['worktime']['issues_worked'] == 2
|
|
assert final_summary['worktime']['cost_allocated'] == 112.5
|
|
|
|
assert final_summary['activities']['total_activities'] == 3
|
|
assert final_summary['activities']['unique_issues'] == 2
|
|
|
|
assert final_summary['costs']['daily_total'] == 112.5
|
|
assert final_summary['costs']['has_cost_allocation'] is True
|
|
assert len(final_summary['costs']['issue_costs']) == 2
|
|
|
|
# Verify recommendations are helpful
|
|
recommendations = final_summary['recommendations']
|
|
assert len(recommendations) >= 0 # Should have reasonable recommendations
|
|
|
|
def test_multi_day_period_summary(self):
|
|
"""Test period summary across multiple days."""
|
|
service = DayWrapUpService(self.db_path)
|
|
|
|
# Create data across multiple days
|
|
dates = [date.today() - timedelta(days=i) for i in range(3)] # Last 3 days
|
|
|
|
for i, test_date in enumerate(dates):
|
|
# Log different amounts of work each day
|
|
hours = 6 + i * 2 # 6, 8, 10 hours
|
|
minutes = hours * 60
|
|
|
|
service.worktime_tracker.log_worktime(124, minutes // 2, work_date=test_date, description=f"Day {i+1} main work")
|
|
service.worktime_tracker.log_worktime(125 + i, minutes // 2, work_date=test_date, description=f"Day {i+1} side work")
|
|
|
|
# Log activities
|
|
from markitect.issues.activity_tracker import ActivityType
|
|
service.activity_tracker.log_activity(124, ActivityType.CREATED, activity_date=test_date)
|
|
service.activity_tracker.log_activity(125 + i, ActivityType.MODIFIED, activity_date=test_date)
|
|
|
|
# Generate period report
|
|
start_date = dates[-1] # Oldest date
|
|
end_date = dates[0] # Most recent date
|
|
|
|
worktime_report = service.worktime_tracker.get_worktime_report(
|
|
start_date=start_date,
|
|
end_date=end_date
|
|
)
|
|
|
|
# Verify period data
|
|
assert worktime_report['total_entries'] == 6 # 2 entries per day * 3 days
|
|
assert worktime_report['total_time']['total_minutes'] == 1440 # 6+8+10 = 24 hours
|
|
assert worktime_report['unique_issues'] == 4 # Issues 124, 125, 126, 127
|
|
assert worktime_report['unique_dates'] == 3
|
|
|
|
# Verify daily averages
|
|
expected_avg = 1440 / 3 # 480 minutes per day on average
|
|
assert abs(worktime_report['average_minutes_per_day'] - expected_avg) < 1
|
|
|
|
def test_error_handling_and_edge_cases(self):
|
|
"""Test error handling and edge cases."""
|
|
service = DayWrapUpService(self.db_path)
|
|
today = date.today()
|
|
|
|
# Test estimation with no activities
|
|
estimation = service.perform_auto_estimation(today, 8.0)
|
|
assert estimation['estimated'] is False
|
|
assert "No active issues found" in estimation['reason']
|
|
|
|
# Test cost distribution with no worktime
|
|
distribution = service.distribute_daily_costs(today, Decimal('100.00'))
|
|
assert 'message' in distribution
|
|
assert "No worktime entries found" in distribution['message']
|
|
|
|
# Test summary generation with partial data
|
|
from markitect.issues.activity_tracker import ActivityType
|
|
service.activity_tracker.log_activity(124, ActivityType.CREATED, activity_date=today)
|
|
|
|
summary = service.generate_daily_summary(today)
|
|
assert summary['worktime']['total_minutes'] == 0 # No worktime logged
|
|
assert summary['activities']['total_activities'] == 1 # But activity exists
|
|
assert "No worktime logged" in ' '.join(summary['recommendations'])
|
|
|
|
# Test recommendations for edge cases
|
|
service.worktime_tracker.log_worktime(124, 720, work_date=today) # 12 hours - excessive
|
|
summary = service.generate_daily_summary(today)
|
|
assert any("High worktime logged" in rec for rec in summary['recommendations']) |