Files
markitect-main/tests/test_issue_114_allocation_engine.py
tegwick 20e7f0f5bd feat: complete issue #114 - Issue #114
Automated issue wrap-up including:
- Implementation completion verification
- Test execution and validation
- Cost tracking and note generation
- Repository state commit

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-05 00:31:10 +02:00

809 lines
30 KiB
Python

"""
Tests for Issue #114 - Cost Allocation Engine Implementation
This module contains comprehensive tests for the cost allocation engine
that distributes operational costs across active issues according to the
algorithm defined in Issue #88.
Tests cover:
- Core allocation algorithm with equal distribution
- Edge cases (no active issues, no costs, closed periods)
- Transaction audit trail creation
- Loss carried forward handling
- Integration with existing cost and activity tracking
- CLI command functionality
- Allocation reversal capabilities
"""
import pytest
import sqlite3
import tempfile
from datetime import datetime, date, timedelta
from decimal import Decimal
from pathlib import Path
from unittest.mock import Mock, patch, MagicMock
import json
from contextlib import redirect_stdout
import io
from markitect.finance.allocation_engine import (
AllocationEngine, TransactionManager, AllocationStatus,
AllocationResult, IssueAllocation
)
from markitect.finance.models import FinanceModels
from markitect.finance.cost_manager import CostItemManager, CostItem
from markitect.finance.period_manager import PeriodManager, PeriodStatus
from markitect.issues.activity_tracker import IssueActivityTracker, ActivityType
def create_test_cost_item(cost_manager, name, category_id, cost_type, amount_eur, starting_from_date):
"""Helper function to create cost items with proper interface."""
cost_item = CostItem(
name=name,
category_id=category_id,
cost_type=cost_type,
amount_eur=amount_eur,
starting_from_date=starting_from_date
)
return cost_manager.create_cost_item(cost_item)
def create_unique_category(cost_manager, base_name, description="Test category"):
"""Helper function to create categories with unique names."""
import time
unique_name = f"{base_name}-{int(time.time()*1000000)}"
return cost_manager.create_category(unique_name, description)
class TestTransactionManager:
"""Test suite for TransactionManager class."""
@pytest.fixture
def temp_db(self):
"""Create temporary database for testing."""
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
db_path = f.name
# Initialize schema
finance_models = FinanceModels(db_path)
finance_models.initialize_finance_schema()
yield db_path
# Cleanup
Path(db_path).unlink(missing_ok=True)
@pytest.fixture
def transaction_manager(self, temp_db):
"""Create TransactionManager instance for testing."""
return TransactionManager(temp_db)
@pytest.fixture
def sample_period(self, temp_db):
"""Create a sample period for testing."""
period_manager = PeriodManager(temp_db)
period_id = period_manager.create_period(
period_start=date(2025, 10, 1),
period_end=date(2025, 10, 31)
)
return period_id
def test_transaction_manager_initialization(self, temp_db):
"""Test that TransactionManager initializes properly."""
manager = TransactionManager(temp_db)
assert manager.db_path == temp_db
assert isinstance(manager.finance_models, FinanceModels)
def test_create_allocation_transaction(self, transaction_manager, sample_period):
"""Test creating allocation transaction records."""
transaction_id = transaction_manager.create_allocation_transaction(
period_id=sample_period,
amount=Decimal('15.50'),
issue_id=123,
transaction_date=date(2025, 10, 15),
description="Test allocation transaction"
)
assert transaction_id is not None
assert isinstance(transaction_id, int)
# Verify transaction was created
with transaction_manager.finance_models.get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
'SELECT * FROM cost_transactions WHERE id = ?',
(transaction_id,)
)
row = cursor.fetchone()
assert row is not None
assert row[1] == sample_period # period_id
assert row[3] == 'cost_allocated' # transaction_type
assert float(row[4]) == 15.50 # amount_eur
assert row[5] == 123 # issue_id
def test_create_loss_forward_transaction(self, transaction_manager, sample_period):
"""Test creating loss carried forward transactions."""
# Create next period
period_manager = PeriodManager(transaction_manager.db_path)
next_period_id = period_manager.create_period(
period_start=date(2025, 11, 1),
period_end=date(2025, 11, 30)
)
transaction_id = transaction_manager.create_loss_forward_transaction(
from_period_id=sample_period,
to_period_id=next_period_id,
amount=Decimal('25.75'),
transaction_date=date(2025, 11, 1),
description="Loss carried forward"
)
assert transaction_id is not None
# Verify transaction was created
with transaction_manager.finance_models.get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
'SELECT * FROM cost_transactions WHERE id = ?',
(transaction_id,)
)
row = cursor.fetchone()
assert row is not None
assert row[1] == next_period_id # period_id
assert row[3] == 'loss_forward' # transaction_type
assert float(row[4]) == 25.75 # amount_eur
assert row[5] is None # issue_id (null for loss forward)
class TestAllocationEngine:
"""Test suite for AllocationEngine class."""
@pytest.fixture
def temp_db(self):
"""Create temporary database for testing."""
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
db_path = f.name
# Initialize schema
finance_models = FinanceModels(db_path)
finance_models.initialize_finance_schema()
yield db_path
# Cleanup
Path(db_path).unlink(missing_ok=True)
@pytest.fixture
def allocation_engine(self, temp_db):
"""Create AllocationEngine instance for testing."""
return AllocationEngine(temp_db)
@pytest.fixture
def sample_costs(self, temp_db):
"""Create sample cost items for testing."""
cost_manager = CostItemManager(temp_db)
# Create cost category
category_id = create_unique_category(cost_manager, "Test Services", "Test cost category")
# Create cost items
cost_ids = []
cost_ids.append(create_test_cost_item(
cost_manager, "Monthly Service", category_id, "monthly",
Decimal('20.00'), date(2025, 10, 1)
))
cost_ids.append(create_test_cost_item(
cost_manager, "One-time Setup", category_id, "one_time",
Decimal('30.00'), date(2025, 10, 15)
))
return cost_ids
@pytest.fixture
def sample_period_with_costs(self, temp_db, sample_costs):
"""Create a period with associated costs."""
period_manager = PeriodManager(temp_db)
period_id = period_manager.create_period(
period_start=date(2025, 10, 1),
period_end=date(2025, 10, 31)
)
return period_id
@pytest.fixture
def sample_issue_activities(self, temp_db, sample_period_with_costs):
"""Create sample issue activities for testing."""
activity_tracker = IssueActivityTracker(temp_db)
# Log activities for different issues
activity_ids = []
activity_ids.append(activity_tracker.log_activity(
issue_id=101,
activity_type=ActivityType.CREATED,
activity_date=date(2025, 10, 5),
period_id=sample_period_with_costs
))
activity_ids.append(activity_tracker.log_activity(
issue_id=102,
activity_type=ActivityType.MODIFIED,
activity_date=date(2025, 10, 10),
period_id=sample_period_with_costs
))
activity_ids.append(activity_tracker.log_activity(
issue_id=103,
activity_type=ActivityType.COMMENTED,
activity_date=date(2025, 10, 20),
period_id=sample_period_with_costs
))
return activity_ids
def test_allocation_engine_initialization(self, temp_db):
"""Test that AllocationEngine initializes with all required components."""
engine = AllocationEngine(temp_db)
assert engine.db_path == temp_db
assert isinstance(engine.finance_models, FinanceModels)
assert isinstance(engine.cost_manager, CostItemManager)
assert isinstance(engine.period_manager, PeriodManager)
assert isinstance(engine.activity_tracker, IssueActivityTracker)
assert isinstance(engine.transaction_manager, TransactionManager)
def test_successful_allocation(self, allocation_engine, sample_period_with_costs, sample_issue_activities):
"""Test successful cost allocation with active issues."""
result = allocation_engine.allocate_period_costs(sample_period_with_costs)
assert result.status == AllocationStatus.SUCCESS
assert result.period_id == sample_period_with_costs
assert result.total_costs == Decimal('50.00') # 20.00 + 30.00
assert len(result.active_issues) == 3 # Issues 101, 102, 103
assert result.cost_per_issue == Decimal('50.00') / 3
assert result.allocations_created == 3
assert result.transactions_created == 3
assert result.loss_carried_forward == Decimal('0.00')
def test_allocation_no_active_issues(self, allocation_engine, sample_period_with_costs):
"""Test allocation when no active issues exist (should carry forward loss)."""
result = allocation_engine.allocate_period_costs(sample_period_with_costs)
assert result.status == AllocationStatus.NO_ACTIVE_ISSUES
assert result.period_id == sample_period_with_costs
assert result.total_costs == Decimal('50.00')
assert len(result.active_issues) == 0
assert result.cost_per_issue == Decimal('0.00')
assert result.allocations_created == 0
assert result.transactions_created == 0
assert result.loss_carried_forward == Decimal('50.00')
def test_allocation_no_costs(self, allocation_engine):
"""Test allocation when no costs exist for period."""
# Create empty period
period_manager = PeriodManager(allocation_engine.db_path)
period_id = period_manager.create_period(
period_start=date(2025, 11, 1),
period_end=date(2025, 11, 30)
)
result = allocation_engine.allocate_period_costs(period_id)
assert result.status == AllocationStatus.NO_COSTS_TO_ALLOCATE
assert result.total_costs == Decimal('0.00')
def test_allocation_period_closed(self, allocation_engine, sample_period_with_costs):
"""Test allocation on already closed period."""
# First allocation (should succeed)
result1 = allocation_engine.allocate_period_costs(sample_period_with_costs)
assert result1.status in [AllocationStatus.SUCCESS, AllocationStatus.NO_ACTIVE_ISSUES]
# Second allocation (should fail - period closed)
result2 = allocation_engine.allocate_period_costs(sample_period_with_costs)
assert result2.status == AllocationStatus.PERIOD_CLOSED
def test_allocation_period_not_found(self, allocation_engine):
"""Test allocation with non-existent period ID."""
result = allocation_engine.allocate_period_costs(99999)
assert result.status == AllocationStatus.ERROR
assert "not found" in result.message
def test_get_issue_allocations(self, allocation_engine, sample_period_with_costs, sample_issue_activities):
"""Test retrieving allocations for a specific issue."""
# Perform allocation first
allocation_engine.allocate_period_costs(sample_period_with_costs)
# Get allocations for issue 101
allocations = allocation_engine.get_issue_allocations(101)
assert len(allocations) == 1
allocation = allocations[0]
assert allocation['issue_id'] == 101
assert allocation['period_id'] == sample_period_with_costs
assert allocation['allocated_amount'] > 0
assert allocation['transaction_id'] is not None
def test_get_period_allocations(self, allocation_engine, sample_period_with_costs, sample_issue_activities):
"""Test retrieving all allocations for a specific period."""
# Perform allocation first
allocation_engine.allocate_period_costs(sample_period_with_costs)
# Get all allocations for the period
allocations = allocation_engine.get_period_allocations(sample_period_with_costs)
assert len(allocations) == 3
issue_ids = [alloc['issue_id'] for alloc in allocations]
assert 101 in issue_ids
assert 102 in issue_ids
assert 103 in issue_ids
def test_reverse_allocation(self, allocation_engine, sample_period_with_costs, sample_issue_activities):
"""Test reversing a cost allocation."""
# Perform allocation first
allocation_engine.allocate_period_costs(sample_period_with_costs)
# Get allocation to reverse
allocations = allocation_engine.get_issue_allocations(101)
assert len(allocations) == 1
allocation_id = allocations[0]['id']
# Reverse the allocation
success = allocation_engine.reverse_allocation(allocation_id)
assert success is True
# Verify allocation is removed
allocations_after = allocation_engine.get_issue_allocations(101)
assert len(allocations_after) == 0
def test_reverse_nonexistent_allocation(self, allocation_engine):
"""Test reversing non-existent allocation."""
success = allocation_engine.reverse_allocation(99999)
assert success is False
def test_allocation_with_carried_forward_loss(self, allocation_engine, temp_db):
"""Test allocation including loss carried forward from previous period."""
# Create period with carried forward loss
period_manager = PeriodManager(temp_db)
period_id = period_manager.create_period(
period_start=date(2025, 10, 1),
period_end=date(2025, 10, 31),
loss_carried_forward=Decimal('15.00')
)
# Create some costs
cost_manager = CostItemManager(temp_db)
category_id = create_unique_category(cost_manager, "Test", "Test category")
create_test_cost_item(
cost_manager, "Test Cost", category_id, "one_time",
Decimal('10.00'), date(2025, 10, 15)
)
# Create issue activity
activity_tracker = IssueActivityTracker(temp_db)
activity_tracker.log_activity(
issue_id=201,
activity_type=ActivityType.CREATED,
activity_date=date(2025, 10, 10),
period_id=period_id
)
# Perform allocation
result = allocation_engine.allocate_period_costs(period_id)
assert result.status == AllocationStatus.SUCCESS
assert result.total_costs == Decimal('25.00') # 10.00 + 15.00 carried forward
assert len(result.active_issues) == 1
assert result.cost_per_issue == Decimal('25.00')
class TestAllocationIntegration:
"""Integration tests for allocation engine with other components."""
@pytest.fixture
def temp_db(self):
"""Create temporary database for testing."""
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
db_path = f.name
# Initialize schema
finance_models = FinanceModels(db_path)
finance_models.initialize_finance_schema()
yield db_path
# Cleanup
Path(db_path).unlink(missing_ok=True)
def test_complete_allocation_workflow(self, temp_db):
"""Test complete workflow from cost creation to allocation."""
# Step 1: Create cost categories and items
cost_manager = CostItemManager(temp_db)
category_id = create_unique_category(cost_manager, "Infrastructure", "Server and hosting costs")
monthly_cost_id = create_test_cost_item(
cost_manager, "Server Hosting", category_id, "monthly",
Decimal('25.00'), date(2025, 10, 1)
)
oneoff_cost_id = create_test_cost_item(
cost_manager, "SSL Certificate", category_id, "one_time",
Decimal('15.00'), date(2025, 10, 10)
)
# Step 2: Create period
period_manager = PeriodManager(temp_db)
period_id = period_manager.create_period(
period_start=date(2025, 10, 1),
period_end=date(2025, 10, 31)
)
# Step 3: Log issue activities
activity_tracker = IssueActivityTracker(temp_db)
activity_tracker.log_activity(
issue_id=301,
activity_type=ActivityType.CREATED,
activity_date=date(2025, 10, 5),
period_id=period_id
)
activity_tracker.log_activity(
issue_id=302,
activity_type=ActivityType.MODIFIED,
activity_date=date(2025, 10, 12),
period_id=period_id
)
# Step 4: Perform allocation
allocation_engine = AllocationEngine(temp_db)
result = allocation_engine.allocate_period_costs(period_id)
# Verify allocation success
assert result.status == AllocationStatus.SUCCESS
assert result.total_costs == Decimal('40.00')
assert len(result.active_issues) == 2
assert result.cost_per_issue == Decimal('20.00')
# Step 5: Verify database state
# Check allocations exist
allocations_301 = allocation_engine.get_issue_allocations(301)
allocations_302 = allocation_engine.get_issue_allocations(302)
assert len(allocations_301) == 1
assert len(allocations_302) == 1
assert allocations_301[0]['allocated_amount'] == 20.00
assert allocations_302[0]['allocated_amount'] == 20.00
# Check transactions exist
with allocation_engine.finance_models.get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
'SELECT COUNT(*) FROM cost_transactions WHERE transaction_type = "cost_allocated"'
)
transaction_count = cursor.fetchone()[0]
assert transaction_count == 2
# Check period is closed
period_data = period_manager.get_period_by_id(period_id)
periods = [period_data] if period_data else []
assert len(periods) == 1
assert periods[0]['status'] == PeriodStatus.CLOSED.value
assert float(periods[0]['total_costs']) == 40.00
assert periods[0]['active_issues_count'] == 2
assert float(periods[0]['cost_per_issue']) == 20.00
def test_multi_period_allocation_workflow(self, temp_db):
"""Test allocation across multiple periods with loss carry forward."""
# Create cost items
cost_manager = CostItemManager(temp_db)
category_id = create_unique_category(cost_manager, "Services", "Monthly services")
create_test_cost_item(
cost_manager, "Monthly Service", category_id, "monthly",
Decimal('30.00'), date(2025, 10, 1)
)
# Create periods
period_manager = PeriodManager(temp_db)
period1_id = period_manager.create_period(
period_start=date(2025, 10, 1),
period_end=date(2025, 10, 31)
)
period2_id = period_manager.create_period(
period_start=date(2025, 11, 1),
period_end=date(2025, 11, 30)
)
# Period 1: No activities (should carry forward loss)
allocation_engine = AllocationEngine(temp_db)
result1 = allocation_engine.allocate_period_costs(period1_id)
assert result1.status == AllocationStatus.NO_ACTIVE_ISSUES
assert result1.loss_carried_forward == Decimal('30.00')
# Period 2: Add activity and allocate
activity_tracker = IssueActivityTracker(temp_db)
activity_tracker.log_activity(
issue_id=401,
activity_type=ActivityType.CREATED,
activity_date=date(2025, 11, 15),
period_id=period2_id
)
# Manually set carried forward for period 2 (simulating automatic carry forward)
with allocation_engine.finance_models.get_connection() as conn:
cursor = conn.cursor()
cursor.execute(
'UPDATE cost_periods SET loss_carried_forward = ? WHERE id = ?',
(float(Decimal('30.00')), period2_id)
)
result2 = allocation_engine.allocate_period_costs(period2_id)
assert result2.status == AllocationStatus.SUCCESS
assert result2.total_costs == Decimal('60.00') # 30.00 current + 30.00 carried forward
assert len(result2.active_issues) == 1
assert result2.cost_per_issue == Decimal('60.00')
class TestAllocationCLI:
"""Test suite for allocation CLI commands."""
@pytest.fixture
def temp_db(self):
"""Create temporary database for testing."""
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
db_path = f.name
# Initialize schema
finance_models = FinanceModels(db_path)
finance_models.initialize_finance_schema()
yield db_path
# Cleanup
Path(db_path).unlink(missing_ok=True)
@pytest.fixture
def setup_test_data(self, temp_db):
"""Set up test data for CLI testing."""
# Create costs and period
cost_manager = CostItemManager(temp_db)
category_id = create_unique_category(cost_manager, "Test", "Test category")
create_test_cost_item(
cost_manager, "Test Cost", category_id, "one_time",
Decimal('45.00'), date(2025, 10, 15)
)
period_manager = PeriodManager(temp_db)
period_id = period_manager.create_period(
period_start=date(2025, 10, 1),
period_end=date(2025, 10, 31)
)
# Create issue activities
activity_tracker = IssueActivityTracker(temp_db)
activity_tracker.log_activity(
issue_id=501,
activity_type=ActivityType.CREATED,
activity_date=date(2025, 10, 5),
period_id=period_id
)
activity_tracker.log_activity(
issue_id=502,
activity_type=ActivityType.MODIFIED,
activity_date=date(2025, 10, 15),
period_id=period_id
)
activity_tracker.log_activity(
issue_id=503,
activity_type=ActivityType.COMMENTED,
activity_date=date(2025, 10, 25),
period_id=period_id
)
return period_id
def test_cli_allocation_period_command(self, temp_db, setup_test_data):
"""Test CLI allocation period command."""
from markitect.finance.cli import allocate_period
from click.testing import CliRunner
runner = CliRunner()
# Mock configuration manager
with patch('markitect.finance.cli.ConfigurationManager') as mock_config_manager:
mock_config = Mock()
mock_config.get_current_config.return_value = {'database_path': temp_db}
mock_config_manager.return_value = mock_config
result = runner.invoke(allocate_period, [str(setup_test_data)])
assert result.exit_code == 0
assert "Cost Allocation Complete" in result.output
assert "Total Costs Allocated: €45.00" in result.output
assert "Active Issues: 3" in result.output
assert "Cost Per Issue: €15.00" in result.output
def test_cli_show_allocations_command(self, temp_db, setup_test_data):
"""Test CLI show allocations command."""
from markitect.finance.cli import show_allocations
from click.testing import CliRunner
# First perform allocation
allocation_engine = AllocationEngine(temp_db)
allocation_engine.allocate_period_costs(setup_test_data)
runner = CliRunner()
# Mock configuration manager
with patch('markitect.finance.cli.ConfigurationManager') as mock_config_manager:
mock_config = Mock()
mock_config.get_current_config.return_value = {'database_path': temp_db}
mock_config_manager.return_value = mock_config
# Test issue allocations
result = runner.invoke(show_allocations, ['issue:501'])
assert result.exit_code == 0
assert "Cost Allocations for Issue #501" in result.output
assert "€15.00" in result.output
# Test period allocations
result = runner.invoke(show_allocations, [f'period:{setup_test_data}'])
assert result.exit_code == 0
assert f"Cost Allocations for Period {setup_test_data}" in result.output
assert "Total: 3 allocations" in result.output
class TestAllocationEdgeCases:
"""Test edge cases and error conditions."""
@pytest.fixture
def temp_db(self):
"""Create temporary database for testing."""
with tempfile.NamedTemporaryFile(suffix='.db', delete=False) as f:
db_path = f.name
# Initialize schema
finance_models = FinanceModels(db_path)
finance_models.initialize_finance_schema()
yield db_path
# Cleanup
Path(db_path).unlink(missing_ok=True)
def test_allocation_with_very_small_amounts(self, temp_db):
"""Test allocation with very small cost amounts."""
# Create tiny cost
cost_manager = CostItemManager(temp_db)
category_id = cost_manager.create_category("Test", "Test")
create_test_cost_item(
cost_manager, "Tiny Cost", category_id, "one_time",
Decimal('0.01'), date(2025, 10, 15) # 1 cent
)
# Create period and activities
period_manager = PeriodManager(temp_db)
period_id = period_manager.create_period(
period_start=date(2025, 10, 1),
period_end=date(2025, 10, 31)
)
activity_tracker = IssueActivityTracker(temp_db)
activity_tracker.log_activity(
issue_id=601,
activity_type=ActivityType.CREATED,
period_id=period_id
)
activity_tracker.log_activity(
issue_id=602,
activity_type=ActivityType.MODIFIED,
period_id=period_id
)
activity_tracker.log_activity(
issue_id=603,
activity_type=ActivityType.COMMENTED,
period_id=period_id
)
# Perform allocation
allocation_engine = AllocationEngine(temp_db)
result = allocation_engine.allocate_period_costs(period_id)
assert result.status == AllocationStatus.SUCCESS
assert result.total_costs == Decimal('0.01')
# With 3 issues, each gets 0.01/3 = 0.0033... which should be handled properly
expected_per_issue = Decimal('0.01') / 3
assert abs(result.cost_per_issue - expected_per_issue) < Decimal('0.0001')
def test_allocation_with_duplicate_activities_same_issue(self, temp_db):
"""Test that duplicate activities for same issue don't create multiple allocations."""
# Create cost and period
cost_manager = CostItemManager(temp_db)
category_id = cost_manager.create_category("Test", "Test")
create_test_cost_item(
cost_manager, "Test Cost", category_id, "one_time",
Decimal('30.00'), date(2025, 10, 15)
)
period_manager = PeriodManager(temp_db)
period_id = period_manager.create_period(
period_start=date(2025, 10, 1),
period_end=date(2025, 10, 31)
)
# Create multiple activities for same issue
activity_tracker = IssueActivityTracker(temp_db)
activity_tracker.log_activity(
issue_id=701,
activity_type=ActivityType.CREATED,
activity_date=date(2025, 10, 5),
period_id=period_id
)
activity_tracker.log_activity(
issue_id=701, # Same issue
activity_type=ActivityType.MODIFIED,
activity_date=date(2025, 10, 10),
period_id=period_id
)
activity_tracker.log_activity(
issue_id=701, # Same issue again
activity_type=ActivityType.COMMENTED,
activity_date=date(2025, 10, 15),
period_id=period_id
)
# Perform allocation
allocation_engine = AllocationEngine(temp_db)
result = allocation_engine.allocate_period_costs(period_id)
assert result.status == AllocationStatus.SUCCESS
assert len(result.active_issues) == 1 # Only one unique issue
assert result.active_issues[0] == 701
assert result.cost_per_issue == Decimal('30.00') # Full amount to single issue
assert result.allocations_created == 1 # Only one allocation
def test_database_constraint_violations(self, temp_db):
"""Test handling of database constraint violations."""
allocation_engine = AllocationEngine(temp_db)
# Try to create duplicate allocation manually
with allocation_engine.finance_models.get_connection() as conn:
cursor = conn.cursor()
# Create period first
cursor.execute('''
INSERT INTO cost_periods (period_start, period_end)
VALUES ('2025-10-01', '2025-10-31')
''')
period_id = cursor.lastrowid
# Create first allocation
cursor.execute('''
INSERT INTO issue_cost_allocations
(issue_id, period_id, allocated_amount, allocation_date)
VALUES (801, ?, 10.00, '2025-10-15')
''', (period_id,))
# Try to create duplicate (should fail due to unique constraint)
with pytest.raises(sqlite3.IntegrityError):
cursor.execute('''
INSERT INTO issue_cost_allocations
(issue_id, period_id, allocated_amount, allocation_date)
VALUES (801, ?, 20.00, '2025-10-16')
''', (period_id,))
if __name__ == '__main__':
pytest.main([__file__])