feat: Implement initial capex calculation feature
- Added CapexComponentInput, CapexParameters, CapexCalculationRequest, CapexCalculationResult, and related schemas for capex calculations. - Introduced calculate_initial_capex function to aggregate capex components and compute totals and timelines. - Created ProjectCapexRepository and ScenarioCapexRepository for managing capex snapshots in the database. - Developed capex.html template for capturing and displaying initial capex data. - Registered common Jinja2 filters for formatting currency and percentages. - Implemented unit and integration tests for capex calculation functionality. - Updated unit of work to include new repositories for capex management.
This commit is contained in:
93
tests/services/test_calculations_capex.py
Normal file
93
tests/services/test_calculations_capex.py
Normal file
@@ -0,0 +1,93 @@
|
||||
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]
|
||||
Reference in New Issue
Block a user