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

@@ -40,9 +40,9 @@ class ProfitabilityCalculationRequest(BaseModel):
premiums: float = Field(0)
fx_rate: PositiveFloat = Field(1)
currency_code: str | None = Field(None, min_length=3, max_length=3)
processing_opex: float = Field(0, ge=0)
opex: float = Field(0, ge=0)
sustaining_capex: float = Field(0, ge=0)
initial_capex: float = Field(0, ge=0)
capex: float = Field(0, ge=0)
discount_rate: float | None = Field(None, ge=0, le=100)
periods: int = Field(10, ge=1, le=120)
impurities: List[ImpurityInput] = Field(default_factory=list)
@@ -63,9 +63,9 @@ class ProfitabilityCalculationRequest(BaseModel):
class ProfitabilityCosts(BaseModel):
"""Aggregated cost components for profitability output."""
processing_opex_total: float
opex_total: float
sustaining_capex_total: float
initial_capex: float
capex: float
class ProfitabilityMetrics(BaseModel):
@@ -82,7 +82,7 @@ class CashFlowEntry(BaseModel):
period: int
revenue: float
processing_opex: float
opex: float
sustaining_capex: float
net: float
@@ -197,8 +197,8 @@ class CapexCalculationResult(BaseModel):
currency: str | None
class ProcessingOpexComponentInput(BaseModel):
"""Processing opex component entry supplied by the UI."""
class OpexComponentInput(BaseModel):
"""opex component entry supplied by the UI."""
id: int | None = Field(default=None, ge=1)
name: str = Field(..., min_length=1)
@@ -234,8 +234,8 @@ class ProcessingOpexComponentInput(BaseModel):
return value.strip()
class ProcessingOpexParameters(BaseModel):
"""Global parameters applied to processing opex calculations."""
class OpexParameters(BaseModel):
"""Global parameters applied to opex calculations."""
currency_code: str | None = Field(None, min_length=3, max_length=3)
escalation_pct: float | None = Field(None, ge=0, le=100)
@@ -251,35 +251,35 @@ class ProcessingOpexParameters(BaseModel):
return value.strip().upper()
class ProcessingOpexOptions(BaseModel):
class OpexOptions(BaseModel):
"""Optional behaviour flags for opex calculations."""
persist: bool = False
snapshot_notes: str | None = Field(None, max_length=500)
class ProcessingOpexCalculationRequest(BaseModel):
"""Request payload for processing opex aggregation."""
class OpexCalculationRequest(BaseModel):
"""Request payload for opex aggregation."""
components: List[ProcessingOpexComponentInput] = Field(
components: List[OpexComponentInput] = Field(
default_factory=list)
parameters: ProcessingOpexParameters = Field(
default_factory=ProcessingOpexParameters, # type: ignore[arg-type]
parameters: OpexParameters = Field(
default_factory=OpexParameters, # type: ignore[arg-type]
)
options: ProcessingOpexOptions = Field(
default_factory=ProcessingOpexOptions, # type: ignore[arg-type]
options: OpexOptions = Field(
default_factory=OpexOptions, # type: ignore[arg-type]
)
class ProcessingOpexCategoryBreakdown(BaseModel):
"""Category breakdown for processing opex totals."""
class OpexCategoryBreakdown(BaseModel):
"""Category breakdown for opex totals."""
category: str
annual_cost: float = Field(..., ge=0)
share: float | None = Field(None, ge=0, le=100)
class ProcessingOpexTimelineEntry(BaseModel):
class OpexTimelineEntry(BaseModel):
"""Timeline entry representing cost over evaluation periods."""
period: int
@@ -287,34 +287,34 @@ class ProcessingOpexTimelineEntry(BaseModel):
escalated_cost: float | None = Field(None, ge=0)
class ProcessingOpexMetrics(BaseModel):
"""Derived KPIs for processing opex outputs."""
class OpexMetrics(BaseModel):
"""Derived KPIs for opex outputs."""
annual_average: float | None
cost_per_ton: float | None
class ProcessingOpexTotals(BaseModel):
"""Aggregated totals for processing opex."""
class OpexTotals(BaseModel):
"""Aggregated totals for opex."""
overall_annual: float = Field(..., ge=0)
escalated_total: float | None = Field(None, ge=0)
escalation_pct: float | None = Field(None, ge=0, le=100)
by_category: List[ProcessingOpexCategoryBreakdown] = Field(
by_category: List[OpexCategoryBreakdown] = Field(
default_factory=list
)
class ProcessingOpexCalculationResult(BaseModel):
"""Response body summarising processing opex calculations."""
class OpexCalculationResult(BaseModel):
"""Response body summarising opex calculations."""
totals: ProcessingOpexTotals
timeline: List[ProcessingOpexTimelineEntry] = Field(default_factory=list)
metrics: ProcessingOpexMetrics
components: List[ProcessingOpexComponentInput] = Field(
totals: OpexTotals
timeline: List[OpexTimelineEntry] = Field(default_factory=list)
metrics: OpexMetrics
components: List[OpexComponentInput] = Field(
default_factory=list)
parameters: ProcessingOpexParameters
options: ProcessingOpexOptions
parameters: OpexParameters
options: OpexOptions
currency: str | None
@@ -333,14 +333,14 @@ __all__ = [
"CapexTotals",
"CapexTimelineEntry",
"CapexCalculationResult",
"ProcessingOpexComponentInput",
"ProcessingOpexParameters",
"ProcessingOpexOptions",
"ProcessingOpexCalculationRequest",
"ProcessingOpexCategoryBreakdown",
"ProcessingOpexTimelineEntry",
"ProcessingOpexMetrics",
"ProcessingOpexTotals",
"ProcessingOpexCalculationResult",
"OpexComponentInput",
"OpexParameters",
"OpexOptions",
"OpexCalculationRequest",
"OpexCategoryBreakdown",
"OpexTimelineEntry",
"OpexMetrics",
"OpexTotals",
"OpexCalculationResult",
"ValidationError",
]