Some checks failed
Test Suite / unit-tests (3.11) (push) Has been cancelled
Test Suite / unit-tests (3.12) (push) Has been cancelled
Test Suite / integration-tests (push) Has been cancelled
Test Suite / e2e-tests (push) Has been cancelled
Test Suite / performance-tests (push) Has been cancelled
Test Suite / code-quality (push) Has been cancelled
Test Suite / security-scan (push) Has been cancelled
Test Suite / test-summary (push) Has been cancelled
Comprehensive cost tracking system implementation including: - Cost report generator with multiple formats (summary, detailed, audit) - Full CLI integration with cost management commands - Claude session cost tracking and estimation - Professional markdown reports with frontmatter/contentmatter - Automatic cost note generation for issue implementations - Complete test coverage (33 test cases) - Database integration with finance schema initialization 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
398 lines
15 KiB
Python
398 lines
15 KiB
Python
"""
|
|
Tests for MarkiTect cost item management system.
|
|
|
|
This module tests the complete cost item management functionality including:
|
|
- Cost item lifecycle (create, update, deactivate)
|
|
- Category management
|
|
- Business rule validation
|
|
- Period-based cost calculations
|
|
- Integration with database models
|
|
"""
|
|
|
|
import pytest
|
|
import tempfile
|
|
import os
|
|
from datetime import date, datetime
|
|
from decimal import Decimal
|
|
|
|
from markitect.finance.cost_manager import CostItemManager, CostItem, CostCategory
|
|
from markitect.finance.models import FinanceModels
|
|
|
|
|
|
class TestCostItemManager:
|
|
"""Test suite for cost item management system."""
|
|
|
|
@pytest.fixture
|
|
def temp_db(self):
|
|
"""Create temporary database for testing."""
|
|
fd, path = tempfile.mkstemp(suffix='.db')
|
|
os.close(fd)
|
|
yield path
|
|
os.unlink(path)
|
|
|
|
@pytest.fixture
|
|
def cost_manager(self, temp_db):
|
|
"""Create CostItemManager instance with initialized database."""
|
|
finance_models = FinanceModels(temp_db)
|
|
finance_models.initialize_finance_schema()
|
|
return CostItemManager(temp_db)
|
|
|
|
@pytest.fixture
|
|
def sample_category_id(self, cost_manager):
|
|
"""Create a sample category for testing."""
|
|
return cost_manager.create_category("Test Category", "For testing purposes")
|
|
|
|
def test_create_cost_item_valid(self, cost_manager, sample_category_id):
|
|
"""Test creating a valid cost item."""
|
|
cost_item = CostItem(
|
|
category_id=sample_category_id,
|
|
name="Test Server",
|
|
description="Monthly hosting",
|
|
cost_type="monthly",
|
|
amount_eur=Decimal('25.50'),
|
|
starting_from_date=date(2025, 1, 1)
|
|
)
|
|
|
|
cost_item_id = cost_manager.create_cost_item(cost_item)
|
|
assert cost_item_id is not None
|
|
|
|
# Verify item was created
|
|
retrieved = cost_manager.get_cost_item(cost_item_id)
|
|
assert retrieved['name'] == "Test Server"
|
|
assert float(retrieved['amount_eur']) == 25.50
|
|
assert retrieved['cost_type'] == "monthly"
|
|
assert retrieved['is_active'] == 1 # SQLite stores booleans as integers
|
|
|
|
def test_create_cost_item_validation_errors(self, cost_manager, sample_category_id):
|
|
"""Test cost item validation errors."""
|
|
# Missing name
|
|
with pytest.raises(ValueError, match="name is required"):
|
|
cost_item = CostItem(
|
|
category_id=sample_category_id,
|
|
name="",
|
|
cost_type="monthly",
|
|
amount_eur=Decimal('10.00'),
|
|
starting_from_date=date(2025, 1, 1)
|
|
)
|
|
cost_manager.create_cost_item(cost_item)
|
|
|
|
# Invalid cost type
|
|
with pytest.raises(ValueError, match="must be 'monthly' or 'one_time'"):
|
|
cost_item = CostItem(
|
|
category_id=sample_category_id,
|
|
name="Test Item",
|
|
cost_type="invalid",
|
|
amount_eur=Decimal('10.00'),
|
|
starting_from_date=date(2025, 1, 1)
|
|
)
|
|
cost_manager.create_cost_item(cost_item)
|
|
|
|
# Negative amount
|
|
with pytest.raises(ValueError, match="must be non-negative"):
|
|
cost_item = CostItem(
|
|
category_id=sample_category_id,
|
|
name="Test Item",
|
|
cost_type="monthly",
|
|
amount_eur=Decimal('-10.00'),
|
|
starting_from_date=date(2025, 1, 1)
|
|
)
|
|
cost_manager.create_cost_item(cost_item)
|
|
|
|
# Invalid date range
|
|
with pytest.raises(ValueError, match="must be after starting date"):
|
|
cost_item = CostItem(
|
|
category_id=sample_category_id,
|
|
name="Test Item",
|
|
cost_type="monthly",
|
|
amount_eur=Decimal('10.00'),
|
|
starting_from_date=date(2025, 1, 15),
|
|
ending_date=date(2025, 1, 10)
|
|
)
|
|
cost_manager.create_cost_item(cost_item)
|
|
|
|
# Inactive without ending date
|
|
with pytest.raises(ValueError, match="must have an ending date"):
|
|
cost_item = CostItem(
|
|
category_id=sample_category_id,
|
|
name="Test Item",
|
|
cost_type="monthly",
|
|
amount_eur=Decimal('10.00'),
|
|
starting_from_date=date(2025, 1, 1),
|
|
is_active=False
|
|
)
|
|
cost_manager.create_cost_item(cost_item)
|
|
|
|
def test_update_cost_item(self, cost_manager, sample_category_id):
|
|
"""Test updating cost item."""
|
|
# Create initial cost item
|
|
cost_item = CostItem(
|
|
category_id=sample_category_id,
|
|
name="Original Name",
|
|
cost_type="monthly",
|
|
amount_eur=Decimal('10.00'),
|
|
starting_from_date=date(2025, 1, 1)
|
|
)
|
|
cost_item_id = cost_manager.create_cost_item(cost_item)
|
|
|
|
# Update the cost item
|
|
updates = {
|
|
'name': 'Updated Name',
|
|
'amount_eur': Decimal('15.50'),
|
|
'description': 'Updated description'
|
|
}
|
|
success = cost_manager.update_cost_item(cost_item_id, updates)
|
|
assert success is True
|
|
|
|
# Verify updates
|
|
updated = cost_manager.get_cost_item(cost_item_id)
|
|
assert updated['name'] == 'Updated Name'
|
|
assert float(updated['amount_eur']) == 15.50
|
|
assert updated['description'] == 'Updated description'
|
|
|
|
def test_update_nonexistent_cost_item(self, cost_manager):
|
|
"""Test updating non-existent cost item."""
|
|
with pytest.raises(ValueError, match="not found"):
|
|
cost_manager.update_cost_item(99999, {'name': 'New Name'})
|
|
|
|
def test_deactivate_cost_item(self, cost_manager, sample_category_id):
|
|
"""Test deactivating cost item."""
|
|
# Create cost item
|
|
cost_item = CostItem(
|
|
category_id=sample_category_id,
|
|
name="Test Item",
|
|
cost_type="monthly",
|
|
amount_eur=Decimal('10.00'),
|
|
starting_from_date=date(2025, 1, 1)
|
|
)
|
|
cost_item_id = cost_manager.create_cost_item(cost_item)
|
|
|
|
# Deactivate with specific ending date
|
|
ending_date = date(2025, 6, 30)
|
|
success = cost_manager.deactivate_cost_item(cost_item_id, ending_date)
|
|
assert success is True
|
|
|
|
# Verify deactivation
|
|
updated = cost_manager.get_cost_item(cost_item_id)
|
|
assert updated['is_active'] == 0 # SQLite stores False as 0
|
|
assert updated['ending_date'] == ending_date.isoformat()
|
|
|
|
def test_list_cost_items_filtering(self, cost_manager, sample_category_id):
|
|
"""Test listing cost items with filtering."""
|
|
# Create multiple cost items
|
|
items = [
|
|
CostItem(
|
|
category_id=sample_category_id,
|
|
name="Monthly Item 1",
|
|
cost_type="monthly",
|
|
amount_eur=Decimal('10.00'),
|
|
starting_from_date=date(2025, 1, 1)
|
|
),
|
|
CostItem(
|
|
category_id=sample_category_id,
|
|
name="One-time Item",
|
|
cost_type="one_time",
|
|
amount_eur=Decimal('50.00'),
|
|
starting_from_date=date(2025, 1, 1)
|
|
),
|
|
CostItem(
|
|
category_id=sample_category_id,
|
|
name="Inactive Item",
|
|
cost_type="monthly",
|
|
amount_eur=Decimal('5.00'),
|
|
starting_from_date=date(2025, 1, 1),
|
|
ending_date=date(2025, 1, 31),
|
|
is_active=False
|
|
)
|
|
]
|
|
|
|
for item in items:
|
|
cost_manager.create_cost_item(item)
|
|
|
|
# Test filtering by active only
|
|
active_items = cost_manager.list_cost_items(active_only=True)
|
|
assert len(active_items) == 2
|
|
assert all(item['is_active'] == 1 for item in active_items)
|
|
|
|
# Test filtering by cost type
|
|
monthly_items = cost_manager.list_cost_items(cost_type="monthly")
|
|
assert len(monthly_items) == 1 # Only active monthly items
|
|
assert monthly_items[0]['cost_type'] == "monthly"
|
|
|
|
# Test including inactive items
|
|
all_items = cost_manager.list_cost_items(active_only=False)
|
|
assert len(all_items) == 3
|
|
|
|
def test_get_active_costs_for_period(self, cost_manager, sample_category_id):
|
|
"""Test retrieving active costs for specific period."""
|
|
# Create cost items with different date ranges
|
|
items = [
|
|
CostItem(
|
|
category_id=sample_category_id,
|
|
name="Active Throughout",
|
|
cost_type="monthly",
|
|
amount_eur=Decimal('10.00'),
|
|
starting_from_date=date(2024, 12, 1)
|
|
),
|
|
CostItem(
|
|
category_id=sample_category_id,
|
|
name="Starts Mid-Period",
|
|
cost_type="monthly",
|
|
amount_eur=Decimal('15.00'),
|
|
starting_from_date=date(2025, 1, 15)
|
|
),
|
|
CostItem(
|
|
category_id=sample_category_id,
|
|
name="Ends Mid-Period",
|
|
cost_type="monthly",
|
|
amount_eur=Decimal('20.00'),
|
|
starting_from_date=date(2024, 12, 1),
|
|
ending_date=date(2025, 1, 15)
|
|
),
|
|
CostItem(
|
|
category_id=sample_category_id,
|
|
name="Outside Period",
|
|
cost_type="monthly",
|
|
amount_eur=Decimal('25.00'),
|
|
starting_from_date=date(2025, 2, 1)
|
|
)
|
|
]
|
|
|
|
for item in items:
|
|
cost_manager.create_cost_item(item)
|
|
|
|
# Get active costs for January 2025
|
|
period_start = date(2025, 1, 1)
|
|
period_end = date(2025, 1, 31)
|
|
active_costs = cost_manager.get_active_costs_for_period(period_start, period_end)
|
|
|
|
# Should include first 3 items but not the fourth
|
|
assert len(active_costs) == 3
|
|
names = [item['name'] for item in active_costs]
|
|
assert "Active Throughout" in names
|
|
assert "Starts Mid-Period" in names
|
|
assert "Ends Mid-Period" in names
|
|
assert "Outside Period" not in names
|
|
|
|
def test_calculate_period_costs(self, cost_manager, sample_category_id):
|
|
"""Test period cost calculations."""
|
|
# Create another category
|
|
other_category_id = cost_manager.create_category("Other Category")
|
|
|
|
# Create cost items in different categories
|
|
items = [
|
|
CostItem(
|
|
category_id=sample_category_id,
|
|
name="Monthly Cost 1",
|
|
cost_type="monthly",
|
|
amount_eur=Decimal('10.00'),
|
|
starting_from_date=date(2025, 1, 1)
|
|
),
|
|
CostItem(
|
|
category_id=sample_category_id,
|
|
name="Monthly Cost 2",
|
|
cost_type="monthly",
|
|
amount_eur=Decimal('15.00'),
|
|
starting_from_date=date(2025, 1, 1)
|
|
),
|
|
CostItem(
|
|
category_id=other_category_id,
|
|
name="One-time Cost",
|
|
cost_type="one_time",
|
|
amount_eur=Decimal('100.00'),
|
|
starting_from_date=date(2025, 1, 1)
|
|
)
|
|
]
|
|
|
|
for item in items:
|
|
cost_manager.create_cost_item(item)
|
|
|
|
# Calculate costs for January 2025
|
|
period_start = date(2025, 1, 1)
|
|
period_end = date(2025, 1, 31)
|
|
calculations = cost_manager.calculate_period_costs(period_start, period_end)
|
|
|
|
assert calculations['total_monthly'] == 25.00
|
|
assert calculations['total_one_time'] == 100.00
|
|
assert calculations['total_period'] == 125.00
|
|
assert calculations['active_cost_items'] == 3
|
|
|
|
# Check category breakdown
|
|
assert 'Test Category' in calculations['category_breakdown']
|
|
assert 'Other Category' in calculations['category_breakdown']
|
|
assert calculations['category_breakdown']['Test Category']['monthly'] == 25.00
|
|
assert calculations['category_breakdown']['Other Category']['one_time'] == 100.00
|
|
|
|
def test_category_management(self, cost_manager):
|
|
"""Test category creation and management."""
|
|
# Create category with unique name
|
|
category_id = cost_manager.create_category("Custom Infrastructure", "Custom server costs")
|
|
assert category_id is not None
|
|
|
|
# Retrieve category
|
|
category = cost_manager.get_category(category_id)
|
|
assert category['name'] == "Custom Infrastructure"
|
|
assert category['description'] == "Custom server costs"
|
|
|
|
# Test duplicate category
|
|
with pytest.raises(ValueError, match="already exists"):
|
|
cost_manager.create_category("Custom Infrastructure")
|
|
|
|
# List categories
|
|
categories = cost_manager.list_categories()
|
|
category_names = [cat['name'] for cat in categories]
|
|
assert "Custom Infrastructure" in category_names
|
|
# Should also include default categories from schema initialization
|
|
assert len(categories) >= 9 # 8 default + 1 created
|
|
|
|
# Get category by name
|
|
found_category = cost_manager.get_category_by_name("Custom Infrastructure")
|
|
assert found_category['id'] == category_id
|
|
|
|
def test_cost_item_with_category_validation(self, cost_manager):
|
|
"""Test cost item creation with category validation."""
|
|
# Try to create cost item with non-existent category
|
|
with pytest.raises(ValueError, match="does not exist"):
|
|
cost_item = CostItem(
|
|
category_id=99999,
|
|
name="Test Item",
|
|
cost_type="monthly",
|
|
amount_eur=Decimal('10.00'),
|
|
starting_from_date=date(2025, 1, 1)
|
|
)
|
|
cost_manager.create_cost_item(cost_item)
|
|
|
|
def test_precision_handling(self, cost_manager, sample_category_id):
|
|
"""Test decimal precision in cost calculations."""
|
|
# Create cost item with precise decimal
|
|
cost_item = CostItem(
|
|
category_id=sample_category_id,
|
|
name="Precise Cost",
|
|
cost_type="monthly",
|
|
amount_eur=Decimal('10.99'),
|
|
starting_from_date=date(2025, 1, 1)
|
|
)
|
|
cost_item_id = cost_manager.create_cost_item(cost_item)
|
|
|
|
# Verify precision is maintained
|
|
retrieved = cost_manager.get_cost_item(cost_item_id)
|
|
assert float(retrieved['amount_eur']) == 10.99
|
|
|
|
# Test in period calculations
|
|
calculations = cost_manager.calculate_period_costs(date(2025, 1, 1), date(2025, 1, 31))
|
|
assert calculations['total_monthly'] == 10.99
|
|
|
|
def test_empty_database_operations(self, cost_manager):
|
|
"""Test operations on empty database."""
|
|
# List items in empty database
|
|
items = cost_manager.list_cost_items()
|
|
assert len(items) == 0
|
|
|
|
# Get non-existent item
|
|
item = cost_manager.get_cost_item(99999)
|
|
assert item is None
|
|
|
|
# Calculate costs for empty period
|
|
calculations = cost_manager.calculate_period_costs(date(2025, 1, 1), date(2025, 1, 31))
|
|
assert calculations['total_monthly'] == 0.00
|
|
assert calculations['total_one_time'] == 0.00
|
|
assert calculations['active_cost_items'] == 0 |