feat: add scenarios list page with metrics and quick actions
Some checks failed
CI / test (push) Has been skipped
CI / build (push) Has been skipped
CI / lint (push) Failing after 15s
CI / deploy (push) Has been skipped

- Introduced a new template for listing scenarios associated with a project.
- Added metrics for total, active, draft, and archived scenarios.
- Implemented quick actions for creating new scenarios and reviewing project overview.
- Enhanced navigation with breadcrumbs for better user experience.

refactor: update Opex and Profitability templates for consistency

- Changed titles and button labels for clarity in Opex and Profitability templates.
- Updated form IDs and action URLs for better alignment with new naming conventions.
- Improved navigation links to include scenario and project overviews.

test: add integration tests for Opex calculations

- Created new tests for Opex calculation HTML and JSON flows.
- Validated successful calculations and ensured correct data persistence.
- Implemented tests for currency mismatch and unsupported frequency scenarios.

test: enhance project and scenario route tests

- Added tests to verify scenario list rendering and calculator shortcuts.
- Ensured scenario detail pages link back to the portfolio correctly.
- Validated project detail pages show associated scenarios accurately.
This commit is contained in:
2025-11-13 16:21:36 +01:00
parent 4f00bf0d3c
commit 522b1e4105
54 changed files with 3419 additions and 700 deletions

View File

@@ -1,16 +1,16 @@
import pytest
from schemas.calculations import (
ProcessingOpexCalculationRequest,
ProcessingOpexComponentInput,
ProcessingOpexOptions,
ProcessingOpexParameters,
OpexCalculationRequest,
OpexComponentInput,
OpexOptions,
OpexParameters,
)
from services.calculations import calculate_processing_opex
from services.calculations import calculate_opex
from services.exceptions import OpexValidationError
def _component(**overrides) -> ProcessingOpexComponentInput:
def _component(**overrides) -> OpexComponentInput:
defaults = {
"id": None,
"name": "Component",
@@ -24,11 +24,11 @@ def _component(**overrides) -> ProcessingOpexComponentInput:
"notes": None,
}
defaults.update(overrides)
return ProcessingOpexComponentInput(**defaults)
return OpexComponentInput(**defaults)
def test_calculate_processing_opex_success():
request = ProcessingOpexCalculationRequest(
def test_calculate_opex_success():
request = OpexCalculationRequest(
components=[
_component(
name="Power",
@@ -49,17 +49,17 @@ def test_calculate_processing_opex_success():
period_end=2,
),
],
parameters=ProcessingOpexParameters(
parameters=OpexParameters(
currency_code="USD",
escalation_pct=5,
discount_rate_pct=None,
evaluation_horizon_years=2,
apply_escalation=True,
),
options=ProcessingOpexOptions(persist=True, snapshot_notes=None),
options=OpexOptions(persist=True, snapshot_notes=None),
)
result = calculate_processing_opex(request)
result = calculate_opex(request)
assert result.currency == "USD"
assert result.options.persist is True
@@ -89,10 +89,10 @@ def test_calculate_processing_opex_success():
assert result.components[1].frequency == "quarterly"
def test_calculate_processing_opex_currency_mismatch():
request = ProcessingOpexCalculationRequest(
def test_calculate_opex_currency_mismatch():
request = OpexCalculationRequest(
components=[_component(currency="USD")],
parameters=ProcessingOpexParameters(
parameters=OpexParameters(
currency_code="CAD",
escalation_pct=None,
discount_rate_pct=None,
@@ -101,16 +101,16 @@ def test_calculate_processing_opex_currency_mismatch():
)
with pytest.raises(OpexValidationError) as exc:
calculate_processing_opex(request)
calculate_opex(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_processing_opex_unsupported_frequency():
request = ProcessingOpexCalculationRequest(
def test_calculate_opex_unsupported_frequency():
request = OpexCalculationRequest(
components=[_component(frequency="biweekly")],
parameters=ProcessingOpexParameters(
parameters=OpexParameters(
currency_code="USD",
escalation_pct=None,
discount_rate_pct=None,
@@ -119,28 +119,28 @@ def test_calculate_processing_opex_unsupported_frequency():
)
with pytest.raises(OpexValidationError) as exc:
calculate_processing_opex(request)
calculate_opex(request)
assert "Unsupported frequency" in exc.value.message
assert exc.value.field_errors and "components[0].frequency" in exc.value.field_errors[0]
def test_calculate_processing_opex_requires_components():
request = ProcessingOpexCalculationRequest(components=[])
def test_calculate_opex_requires_components():
request = OpexCalculationRequest(components=[])
with pytest.raises(OpexValidationError) as exc:
calculate_processing_opex(request)
calculate_opex(request)
assert "At least one processing opex component" in exc.value.message
assert "At least one opex component" in exc.value.message
assert exc.value.field_errors and "components" in exc.value.field_errors[0]
def test_calculate_processing_opex_extends_evaluation_horizon():
request = ProcessingOpexCalculationRequest(
def test_calculate_opex_extends_evaluation_horizon():
request = OpexCalculationRequest(
components=[
_component(period_start=1, period_end=4),
],
parameters=ProcessingOpexParameters(
parameters=OpexParameters(
currency_code="USD",
discount_rate_pct=0,
escalation_pct=0,
@@ -149,7 +149,7 @@ def test_calculate_processing_opex_extends_evaluation_horizon():
),
)
result = calculate_processing_opex(request)
result = calculate_opex(request)
assert len(result.timeline) == 4
assert result.timeline[-1].period == 4