Files
markitect-main/tests/test_issue_122_worktime_tracking.py
tegwick 1d86bf1bbd fix: eliminate all test suite warnings - Issue #129
Comprehensive fix for test suite warnings across multiple issue test files:

### SQLite3 Date Adapter Warnings (Python 3.12)
- Fixed 101 warnings in Issue 113 (activity_tracker.py)
- Fixed 55 warnings in Issue 114 (allocation_engine.py)
- Fixed 148 warnings in Issue 122 (worktime_tracker.py + test file)
- Fixed 18 warnings in Issue 124 (day_wrapup_commands.py + worktime_tracker.py)

### Pytest-asyncio Configuration
- Added asyncio_default_fixture_loop_scope = function to pytest.ini
- Eliminates pytest-asyncio deprecation warning

### Runtime Warnings for Unawaited Coroutines
- Fixed 2 warnings in Issue 59 (gitea plugin async mocking)
- Enhanced AsyncTestCase with better coroutine cleanup
- Improved async mock management in test utilities

### Technical Changes
- Convert Python date/datetime objects to ISO strings before SQLite queries
- Use .isoformat() with defensive hasattr() checks for backward compatibility
- Simplified async test mocking to avoid coroutine creation
- Enhanced cleanup_async_mocks() function for comprehensive cleanup

### Results
- Before: ~324 warnings across test suite
- After: 0 warnings - completely clean test suite
- All 216+ tests pass with zero warning noise

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-06 02:11:28 +02:00

794 lines
30 KiB
Python

"""
Tests for Issue #122 - Daily worktime estimation and distribution of associated cost
This module contains comprehensive tests for the worktime tracking system
that estimates daily work time and distributes costs proportionally based
on time allocation across issues.
"""
import pytest
import sqlite3
from datetime import datetime, date, timedelta
from unittest.mock import Mock, patch, MagicMock
from pathlib import Path
import tempfile
import json
from decimal import Decimal
from markitect.finance.worktime_tracker import WorktimeTracker, WorktimeEntry, DailySummary
from markitect.finance.worktime_commands import worktime, _parse_duration, _format_duration
class TestWorktimeEntry:
"""Test suite for WorktimeEntry dataclass."""
def test_worktime_entry_creation(self):
"""Test that WorktimeEntry objects can be created properly."""
entry = WorktimeEntry(
id=1,
issue_id=122,
work_date=date.today(),
duration_minutes=90,
description="Working on worktime tracking"
)
assert entry.id == 1
assert entry.issue_id == 122
assert entry.work_date == date.today()
assert entry.duration_minutes == 90
assert entry.description == "Working on worktime tracking"
def test_worktime_entry_defaults(self):
"""Test that WorktimeEntry has proper default values."""
entry = WorktimeEntry()
assert entry.id is None
assert entry.issue_id is None
assert entry.work_date is None
assert entry.start_time is None
assert entry.end_time is None
assert entry.duration_minutes is None
assert entry.description is None
assert entry.entry_type == "manual"
assert entry.created_at is None
assert entry.updated_at is None
class TestDailySummary:
"""Test suite for DailySummary dataclass."""
def test_daily_summary_creation(self):
"""Test that DailySummary objects can be created properly."""
entries = [
WorktimeEntry(id=1, issue_id=122, duration_minutes=90),
WorktimeEntry(id=2, issue_id=123, duration_minutes=60)
]
summary = DailySummary(
work_date=date.today(),
total_minutes=150,
issue_count=2,
entries=entries,
cost_per_minute=Decimal('0.1'),
total_cost_allocated=Decimal('15.0')
)
assert summary.work_date == date.today()
assert summary.total_minutes == 150
assert summary.issue_count == 2
assert len(summary.entries) == 2
assert summary.cost_per_minute == Decimal('0.1')
assert summary.total_cost_allocated == Decimal('15.0')
class TestWorktimeTracker:
"""Test suite for WorktimeTracker service."""
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.tracker = WorktimeTracker(self.db_path)
def teardown_method(self):
"""Clean up test fixtures."""
Path(self.db_path).unlink(missing_ok=True)
def test_tracker_initialization(self):
"""Test that tracker initializes properly with database."""
assert self.tracker.db_path == self.db_path
assert self.tracker.finance_models is not None
# Verify worktime tables were created
with self.tracker.finance_models.get_connection() as conn:
cursor = conn.cursor()
cursor.execute("SELECT name FROM sqlite_master WHERE type='table' ORDER BY name")
tables = [row[0] for row in cursor.fetchall()]
expected_tables = ['worktime_entries', 'daily_worktime_summaries', 'worktime_cost_distributions']
for table in expected_tables:
assert table in tables
def test_log_worktime_basic(self):
"""Test logging basic worktime entry."""
entry_id = self.tracker.log_worktime(
issue_id=122,
duration_minutes=90,
description="Implementing worktime tracking"
)
assert entry_id is not None
# Verify entry was stored
entries = self.tracker.get_worktime_entries(issue_id=122)
assert len(entries) == 1
assert entries[0].issue_id == 122
assert entries[0].duration_minutes == 90
assert entries[0].description == "Implementing worktime tracking"
def test_log_worktime_with_timestamps(self):
"""Test logging worktime with start and end times."""
now = datetime.now()
start_time = now.replace(hour=9, minute=0, second=0, microsecond=0)
end_time = now.replace(hour=10, minute=30, second=0, microsecond=0)
entry_id = self.tracker.log_worktime(
issue_id=122,
duration_minutes=90,
start_time=start_time,
end_time=end_time,
description="Morning work session"
)
entries = self.tracker.get_worktime_entries(issue_id=122)
assert len(entries) == 1
assert entries[0].start_time.hour == 9
assert entries[0].end_time.hour == 10
assert entries[0].end_time.minute == 30
def test_log_worktime_validation(self):
"""Test worktime logging validation."""
# Test negative duration
with pytest.raises(ValueError, match="Duration must be positive"):
self.tracker.log_worktime(issue_id=122, duration_minutes=-30)
# Test zero duration
with pytest.raises(ValueError, match="Duration must be positive"):
self.tracker.log_worktime(issue_id=122, duration_minutes=0)
def test_get_worktime_entries_filtering(self):
"""Test worktime entry retrieval with various filters."""
today = date.today()
yesterday = today - timedelta(days=1)
# Create test entries
self.tracker.log_worktime(122, 60, work_date=today, description="Today's work")
self.tracker.log_worktime(123, 90, work_date=today, description="Today's other work")
self.tracker.log_worktime(122, 45, work_date=yesterday, description="Yesterday's work")
# Test filtering by issue
issue_122_entries = self.tracker.get_worktime_entries(issue_id=122)
assert len(issue_122_entries) == 2
assert all(e.issue_id == 122 for e in issue_122_entries)
# Test filtering by date
today_entries = self.tracker.get_worktime_entries(work_date=today)
assert len(today_entries) == 2
assert all(e.work_date == today for e in today_entries)
# Test date range filtering
range_entries = self.tracker.get_worktime_entries(start_date=yesterday, end_date=today)
assert len(range_entries) == 3
def test_get_daily_summary(self):
"""Test daily worktime summary generation."""
today = date.today()
# Log multiple entries for today
self.tracker.log_worktime(122, 90, work_date=today)
self.tracker.log_worktime(123, 60, work_date=today)
self.tracker.log_worktime(122, 30, work_date=today) # Second entry for same issue
summary = self.tracker.get_daily_summary(today)
assert summary is not None
assert summary.work_date == today
assert summary.total_minutes == 180 # 90 + 60 + 30
assert summary.issue_count == 2 # Issues 122 and 123
assert len(summary.entries) == 3
def test_estimate_daily_worktime_equal_distribution(self):
"""Test daily worktime estimation with equal distribution."""
today = date.today()
issues = [122, 123, 124]
result = self.tracker.estimate_daily_worktime(
work_date=today,
total_hours=6.0,
issues=issues,
distribution_method="equal"
)
assert result['work_date'] == today
assert result['total_minutes'] == 360 # 6 hours
assert result['distribution_method'] == "equal"
assert result['issues_count'] == 3
# Each issue should get 120 minutes (2 hours)
for issue_id in issues:
assert result['issue_estimates'][issue_id] == 120
# Verify entries were created
entries = self.tracker.get_worktime_entries(work_date=today)
assert len(entries) == 3
assert all(e.entry_type == "estimated" for e in entries)
def test_estimate_daily_worktime_activity_based(self):
"""Test daily worktime estimation with activity-based distribution."""
today = date.today()
# Mock activity data - issue 122 has more activities
with patch.object(self.tracker, '_get_activity_weights_for_date') as mock_weights:
mock_weights.return_value = {122: 5, 123: 2, 124: 1} # Different activity levels
result = self.tracker.estimate_daily_worktime(
work_date=today,
total_hours=8.0,
issues=[122, 123, 124],
distribution_method="activity_based"
)
# Verify distribution is proportional to activities
total_weight = 5 + 2 + 1 # 8
expected_122 = int((5/8) * 480) # 300 minutes
expected_123 = int((2/8) * 480) # 120 minutes
expected_124 = int((1/8) * 480) # 60 minutes
assert result['issue_estimates'][122] == expected_122
assert result['issue_estimates'][123] == expected_123
assert result['issue_estimates'][124] == expected_124
def test_distribute_daily_costs(self):
"""Test daily cost distribution based on time allocation."""
today = date.today()
# Log different amounts of time for different issues
self.tracker.log_worktime(122, 120, work_date=today) # 2 hours
self.tracker.log_worktime(123, 60, work_date=today) # 1 hour
self.tracker.log_worktime(124, 120, work_date=today) # 2 hours
# Total: 5 hours (300 minutes)
total_cost = Decimal('150.00') # €150 for the day
result = self.tracker.distribute_daily_costs(
work_date=today,
total_daily_cost=total_cost
)
assert result['work_date'] == today
assert result['total_cost'] == 150.0
assert result['total_minutes'] == 300
assert result['cost_per_minute'] == 0.5 # €150 / 300 minutes
# Check cost distribution
assert result['distributions'][122]['cost_allocated'] == 60.0 # 120 min * €0.5
assert result['distributions'][123]['cost_allocated'] == 30.0 # 60 min * €0.5
assert result['distributions'][124]['cost_allocated'] == 60.0 # 120 min * €0.5
# Check percentages
assert result['distributions'][122]['percentage'] == 40.0 # 120/300 * 100
assert result['distributions'][123]['percentage'] == 20.0 # 60/300 * 100
assert result['distributions'][124]['percentage'] == 40.0 # 120/300 * 100
def test_distribute_daily_costs_no_worktime(self):
"""Test cost distribution when no worktime is logged."""
today = date.today()
total_cost = Decimal('100.00')
result = self.tracker.distribute_daily_costs(
work_date=today,
total_daily_cost=total_cost
)
assert 'message' in result
assert "No worktime entries found" in result['message']
def test_get_worktime_report(self):
"""Test comprehensive worktime reporting."""
today = date.today()
yesterday = today - timedelta(days=1)
# Create test data across multiple days and issues
self.tracker.log_worktime(122, 90, work_date=yesterday)
self.tracker.log_worktime(123, 60, work_date=yesterday)
self.tracker.log_worktime(122, 120, work_date=today)
self.tracker.log_worktime(124, 45, work_date=today)
report = self.tracker.get_worktime_report(
start_date=yesterday,
end_date=today
)
assert report['total_entries'] == 4
assert report['total_time']['total_minutes'] == 315 # 90+60+120+45
assert report['total_time']['hours'] == 5
assert report['total_time']['minutes'] == 15
assert report['unique_issues'] == 3 # Issues 122, 123, 124
assert report['unique_dates'] == 2
# Check issue breakdown
assert 122 in report['issue_breakdown']
assert report['issue_breakdown'][122]['total_minutes'] == 210 # 90+120
assert report['issue_breakdown'][122]['entry_count'] == 2
assert report['issue_breakdown'][122]['unique_dates'] == 2
def test_delete_worktime_entry(self):
"""Test deleting worktime entries."""
entry_id = self.tracker.log_worktime(122, 90, description="Test entry")
# Verify entry exists
entries = self.tracker.get_worktime_entries(issue_id=122)
assert len(entries) == 1
# Delete entry
success = self.tracker.delete_worktime_entry(entry_id)
assert success is True
# Verify entry is gone
entries = self.tracker.get_worktime_entries(issue_id=122)
assert len(entries) == 0
# Try to delete non-existent entry
success = self.tracker.delete_worktime_entry(99999)
assert success is False
def test_update_worktime_entry(self):
"""Test updating worktime entries."""
entry_id = self.tracker.log_worktime(122, 90, description="Original description")
# Update duration and description
success = self.tracker.update_worktime_entry(
entry_id=entry_id,
duration_minutes=120,
description="Updated description"
)
assert success is True
# Verify updates
entries = self.tracker.get_worktime_entries(issue_id=122)
assert len(entries) == 1
assert entries[0].duration_minutes == 120
assert entries[0].description == "Updated description"
# Try to update non-existent entry
success = self.tracker.update_worktime_entry(
entry_id=99999,
duration_minutes=60
)
assert success is False
class TestWorktimeCommands:
"""Test suite for worktime CLI commands."""
def test_parse_duration_minutes(self):
"""Test parsing duration strings - minutes format."""
assert _parse_duration("90") == 90
assert _parse_duration("120") == 120
assert _parse_duration("45m") == 45
def test_parse_duration_hours(self):
"""Test parsing duration strings - hours format."""
assert _parse_duration("1h") == 60
assert _parse_duration("2h") == 120
assert _parse_duration("1.5h") == 90
assert _parse_duration("2.25h") == 135
def test_parse_duration_hours_minutes(self):
"""Test parsing duration strings - hours and minutes format."""
assert _parse_duration("1h30m") == 90
assert _parse_duration("2h15m") == 135
assert _parse_duration("0h45m") == 45
assert _parse_duration("3h0m") == 180
def test_parse_duration_invalid(self):
"""Test parsing invalid duration strings."""
with pytest.raises(ValueError):
_parse_duration("invalid")
with pytest.raises(ValueError):
_parse_duration("1x30m")
with pytest.raises(ValueError):
_parse_duration("")
def test_format_duration_minutes_only(self):
"""Test formatting duration - minutes only."""
assert _format_duration(30) == "30m"
assert _format_duration(45) == "45m"
assert _format_duration(59) == "59m"
def test_format_duration_hours_only(self):
"""Test formatting duration - hours only."""
assert _format_duration(60) == "1h"
assert _format_duration(120) == "2h"
assert _format_duration(180) == "3h"
def test_format_duration_hours_and_minutes(self):
"""Test formatting duration - hours and minutes."""
assert _format_duration(90) == "1h30m"
assert _format_duration(135) == "2h15m"
assert _format_duration(195) == "3h15m"
@patch('markitect.finance.worktime_commands.WorktimeTracker')
def test_log_command_basic(self, mock_tracker_class):
"""Test the log command with basic parameters."""
mock_tracker = Mock()
mock_tracker_class.return_value = mock_tracker
mock_tracker.log_worktime.return_value = 1
mock_tracker.get_daily_summary.return_value = DailySummary(
work_date=date.today(),
total_minutes=90,
issue_count=1,
entries=[]
)
from click.testing import CliRunner
runner = CliRunner()
result = runner.invoke(worktime, ['log', '122', '1h30m'])
assert result.exit_code == 0
assert "✅ Logged 90min worktime for issue #122" in result.output
mock_tracker.log_worktime.assert_called_once()
@patch('markitect.finance.worktime_commands.WorktimeTracker')
def test_log_command_with_description(self, mock_tracker_class):
"""Test the log command with description."""
mock_tracker = Mock()
mock_tracker_class.return_value = mock_tracker
mock_tracker.log_worktime.return_value = 1
mock_tracker.get_daily_summary.return_value = None
from click.testing import CliRunner
runner = CliRunner()
result = runner.invoke(worktime, ['log', '122', '90', '--description', 'Testing worktime'])
assert result.exit_code == 0
assert "Testing worktime" in result.output
@patch('markitect.finance.worktime_commands.WorktimeTracker')
def test_list_command_table_format(self, mock_tracker_class):
"""Test the list command with table output format."""
mock_tracker = Mock()
mock_tracker_class.return_value = mock_tracker
mock_entries = [
WorktimeEntry(
id=1,
issue_id=122,
work_date=date.today(),
duration_minutes=90,
description="Test worktime",
entry_type="manual"
)
]
mock_tracker.get_worktime_entries.return_value = mock_entries
from click.testing import CliRunner
runner = CliRunner()
result = runner.invoke(worktime, ['list'])
assert result.exit_code == 0
assert "⏰ Worktime Entries" in result.output
assert "#122" in result.output
assert "1h30m" in result.output
@patch('markitect.finance.worktime_commands.WorktimeTracker')
def test_list_command_json_format(self, mock_tracker_class):
"""Test the list command with JSON output format."""
mock_tracker = Mock()
mock_tracker_class.return_value = mock_tracker
mock_entries = [
WorktimeEntry(
id=1,
issue_id=122,
work_date=date.today(),
duration_minutes=90,
description="Test worktime",
entry_type="manual"
)
]
mock_tracker.get_worktime_entries.return_value = mock_entries
from click.testing import CliRunner
runner = CliRunner()
result = runner.invoke(worktime, ['list', '--format', 'json'])
assert result.exit_code == 0
# Should be valid JSON
output_data = json.loads(result.output.strip())
assert len(output_data) == 1
assert output_data[0]['issue_id'] == 122
assert output_data[0]['duration_minutes'] == 90
@patch('markitect.finance.worktime_commands.WorktimeTracker')
def test_daily_command(self, mock_tracker_class):
"""Test the daily summary command."""
mock_tracker = Mock()
mock_tracker_class.return_value = mock_tracker
mock_entries = [
WorktimeEntry(id=1, issue_id=122, duration_minutes=90, entry_type="manual"),
WorktimeEntry(id=2, issue_id=123, duration_minutes=60, entry_type="manual")
]
mock_summary = DailySummary(
work_date=date.today(),
total_minutes=150,
issue_count=2,
entries=mock_entries,
cost_per_minute=Decimal('0.5'),
total_cost_allocated=Decimal('75.0')
)
mock_tracker.get_daily_summary.return_value = mock_summary
from click.testing import CliRunner
runner = CliRunner()
today = date.today().strftime('%Y-%m-%d')
result = runner.invoke(worktime, ['daily', today])
assert result.exit_code == 0
assert f"📅 Daily Summary for {date.today()}" in result.output
assert "Total Time: 2h30m" in result.output
assert "Issues Worked: 2" in result.output
assert "Cost per Minute: €0.5000" in result.output
@patch('markitect.finance.worktime_commands.WorktimeTracker')
def test_estimate_command(self, mock_tracker_class):
"""Test the estimate worktime command."""
mock_tracker = Mock()
mock_tracker_class.return_value = mock_tracker
mock_result = {
'work_date': date.today(),
'total_minutes': 480, # 8 hours
'distribution_method': 'equal',
'issue_estimates': {122: 240, 123: 240},
'issues_count': 2
}
mock_tracker.estimate_daily_worktime.return_value = mock_result
from click.testing import CliRunner
runner = CliRunner()
today = date.today().strftime('%Y-%m-%d')
result = runner.invoke(worktime, ['estimate', today, '8', '-i', '122', '-i', '123'])
assert result.exit_code == 0
assert "📊 Worktime Estimation" in result.output
assert "Total Hours: 8.0h" in result.output
assert "✅ Created 2 estimated worktime entries" in result.output
@patch('markitect.finance.worktime_commands.WorktimeTracker')
def test_distribute_command(self, mock_tracker_class):
"""Test the cost distribution command."""
mock_tracker = Mock()
mock_tracker_class.return_value = mock_tracker
mock_result = {
'work_date': date.today(),
'total_cost': 100.0,
'total_minutes': 200,
'cost_per_minute': 0.5,
'distributions': {
122: {'minutes': 120, 'percentage': 60.0, 'cost_allocated': 60.0},
123: {'minutes': 80, 'percentage': 40.0, 'cost_allocated': 40.0}
},
'issues_count': 2
}
mock_tracker.distribute_daily_costs.return_value = mock_result
from click.testing import CliRunner
runner = CliRunner()
today = date.today().strftime('%Y-%m-%d')
result = runner.invoke(worktime, ['distribute', today, '100'])
assert result.exit_code == 0
assert "💰 Cost Distribution" in result.output
assert "Total Cost: €100.00" in result.output
assert "Cost per Minute: €0.5000" in result.output
@patch('markitect.finance.worktime_commands.WorktimeTracker')
def test_delete_command(self, mock_tracker_class):
"""Test the delete command."""
mock_tracker = Mock()
mock_tracker_class.return_value = mock_tracker
mock_tracker.delete_worktime_entry.return_value = True
from click.testing import CliRunner
runner = CliRunner()
# Auto-confirm the deletion
result = runner.invoke(worktime, ['delete', '1'], input='y\n')
assert result.exit_code == 0
assert "✅ Deleted worktime entry #1" in result.output
mock_tracker.delete_worktime_entry.assert_called_once_with(1)
@patch('markitect.finance.worktime_commands.WorktimeTracker')
def test_update_command(self, mock_tracker_class):
"""Test the update command."""
mock_tracker = Mock()
mock_tracker_class.return_value = mock_tracker
mock_tracker.update_worktime_entry.return_value = True
from click.testing import CliRunner
runner = CliRunner()
result = runner.invoke(worktime, ['update', '1', '--duration', '2h', '--description', 'Updated'])
assert result.exit_code == 0
assert "✅ Updated worktime entry #1" in result.output
class TestWorktimeIntegration:
"""Integration tests for the complete worktime tracking 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_full_worktime_lifecycle(self):
"""Test the complete lifecycle of worktime tracking."""
tracker = WorktimeTracker(self.db_path)
# 1. Log worktime for multiple issues across multiple days
today = date.today()
yesterday = today - timedelta(days=1)
tracker.log_worktime(122, 120, work_date=yesterday, description="Initial development")
tracker.log_worktime(123, 90, work_date=yesterday, description="Code review")
tracker.log_worktime(122, 90, work_date=today, description="Bug fixes")
tracker.log_worktime(124, 60, work_date=today, description="Documentation")
# 2. Verify daily summaries
yesterday_summary = tracker.get_daily_summary(yesterday)
assert yesterday_summary.total_minutes == 210 # 120 + 90
assert yesterday_summary.issue_count == 2
today_summary = tracker.get_daily_summary(today)
assert today_summary.total_minutes == 150 # 90 + 60
assert today_summary.issue_count == 2
# 3. Distribute costs for a day
distribution = tracker.distribute_daily_costs(
work_date=today,
total_daily_cost=Decimal('75.00') # €75 for today's work
)
assert distribution['total_cost'] == 75.0
assert distribution['total_minutes'] == 150
assert distribution['cost_per_minute'] == 0.5
# Issue 122: 90 minutes = €45
# Issue 124: 60 minutes = €30
assert distribution['distributions'][122]['cost_allocated'] == 45.0
assert distribution['distributions'][124]['cost_allocated'] == 30.0
# 4. Generate comprehensive report
report = tracker.get_worktime_report(
start_date=yesterday,
end_date=today
)
assert report['total_entries'] == 4
assert report['total_time']['total_minutes'] == 360 # 210 + 150
assert report['unique_issues'] == 3 # Issues 122, 123, 124
assert report['unique_dates'] == 2
# 5. Test estimation functionality
tomorrow = today + timedelta(days=1)
estimation = tracker.estimate_daily_worktime(
work_date=tomorrow,
total_hours=6.0,
issues=[122, 125, 126],
distribution_method="equal"
)
assert estimation['total_minutes'] == 360
assert len(estimation['issue_estimates']) == 3
# Each issue should get 120 minutes (equal distribution)
for minutes in estimation['issue_estimates'].values():
assert minutes == 120
# 6. Verify estimated entries were created
tomorrow_entries = tracker.get_worktime_entries(work_date=tomorrow)
assert len(tomorrow_entries) == 3
assert all(e.entry_type == "estimated" for e in tomorrow_entries)
def test_cost_distribution_accuracy(self):
"""Test accurate cost distribution calculations."""
tracker = WorktimeTracker(self.db_path)
work_date = date.today()
# Log precise worktime amounts
tracker.log_worktime(122, 100, work_date=work_date) # 100 minutes
tracker.log_worktime(123, 50, work_date=work_date) # 50 minutes
tracker.log_worktime(124, 150, work_date=work_date) # 150 minutes
# Total: 300 minutes
# Distribute exactly €300
distribution = tracker.distribute_daily_costs(
work_date=work_date,
total_daily_cost=Decimal('300.00')
)
# Should be exactly €1 per minute
assert distribution['cost_per_minute'] == 1.0
# Verify exact cost allocation
assert distribution['distributions'][122]['cost_allocated'] == 100.0
assert distribution['distributions'][123]['cost_allocated'] == 50.0
assert distribution['distributions'][124]['cost_allocated'] == 150.0
# Verify percentages sum to 100%
total_percentage = sum(
dist['percentage'] for dist in distribution['distributions'].values()
)
assert abs(total_percentage - 100.0) < 0.01 # Allow for rounding
# Verify cost allocation was logged to database
with tracker.finance_models.get_connection() as conn:
cursor = conn.cursor()
cursor.execute('''
SELECT issue_id, cost_allocated
FROM worktime_cost_distributions
WHERE work_date = ?
ORDER BY issue_id
''', (work_date.isoformat() if hasattr(work_date, 'isoformat') else work_date,))
results = cursor.fetchall()
assert len(results) == 3
assert results[0] == (122, 100.0)
assert results[1] == (123, 50.0)
assert results[2] == (124, 150.0)
def test_worktime_modification_and_summary_updates(self):
"""Test that modifying worktime entries correctly updates summaries."""
tracker = WorktimeTracker(self.db_path)
work_date = date.today()
# Log initial worktime
entry_id = tracker.log_worktime(122, 60, work_date=work_date)
# Check initial summary
summary = tracker.get_daily_summary(work_date)
assert summary.total_minutes == 60
# Update the entry
tracker.update_worktime_entry(entry_id, duration_minutes=120)
# Check updated summary
summary = tracker.get_daily_summary(work_date)
assert summary.total_minutes == 120
# Delete the entry
tracker.delete_worktime_entry(entry_id)
# Check final summary
summary = tracker.get_daily_summary(work_date)
assert summary is None or summary.total_minutes == 0