import pytest from schemas.calculations import ( CapexCalculationOptions, CapexCalculationRequest, CapexComponentInput, CapexParameters, ) from services.calculations import calculate_initial_capex from services.exceptions import CapexValidationError def _component(**kwargs) -> CapexComponentInput: defaults = { "id": None, "name": "Component", "category": "equipment", "amount": 1_000_000.0, "currency": "USD", "spend_year": 0, "notes": None, } defaults.update(kwargs) return CapexComponentInput(**defaults) def test_calculate_initial_capex_success(): request = CapexCalculationRequest( components=[ _component(name="Crusher", category="equipment", amount=1_200_000, spend_year=0), _component(name="Conveyor", category="equipment", amount=800_000, spend_year=1), _component(name="Camp", category="infrastructure", amount=600_000, spend_year=1, currency="usd"), ], parameters=CapexParameters( currency_code="USD", contingency_pct=10, discount_rate_pct=8, evaluation_horizon_years=5, ), options=CapexCalculationOptions(persist=True), ) result = calculate_initial_capex(request) assert result.currency == "USD" assert result.options.persist is True assert result.totals.overall == pytest.approx(2_600_000) assert result.totals.contingency_pct == pytest.approx(10) assert result.totals.contingency_amount == pytest.approx(260_000) assert result.totals.with_contingency == pytest.approx(2_860_000) by_category = {row.category: row for row in result.totals.by_category} assert by_category["equipment"].amount == pytest.approx(2_000_000) assert by_category["infrastructure"].amount == pytest.approx(600_000) assert by_category["equipment"].share == pytest.approx(76.923, rel=1e-3) assert by_category["infrastructure"].share == pytest.approx( 23.077, rel=1e-3) timeline = {(entry.year, entry.spend): entry.cumulative for entry in result.timeline} assert timeline[(0, 1_200_000)] == pytest.approx(1_200_000) assert timeline[(1, 1_400_000)] == pytest.approx(2_600_000) assert len(result.components) == 3 assert result.components[2].currency == "USD" def test_calculate_initial_capex_currency_mismatch_raises(): request = CapexCalculationRequest( components=[ _component(amount=500_000, currency="USD"), ], parameters=CapexParameters(currency_code="CAD"), ) with pytest.raises(CapexValidationError) as exc: calculate_initial_capex(request) assert "Component currency does not match" in exc.value.message assert exc.value.field_errors and "components[0].currency" in exc.value.field_errors[0] def test_calculate_initial_capex_requires_components(): request = CapexCalculationRequest(components=[]) with pytest.raises(CapexValidationError) as exc: calculate_initial_capex(request) assert "At least one capex component" in exc.value.message assert exc.value.field_errors and "components" in exc.value.field_errors[0]