- Introduced Pydantic schemas for profitability calculations in `schemas/calculations.py`. - Implemented service functions for profitability calculations in `services/calculations.py`. - Added new exception class `ProfitabilityValidationError` for handling validation errors. - Created repositories for managing project and scenario profitability snapshots. - Developed a utility script for verifying authenticated routes. - Added a new HTML template for the profitability calculator interface. - Implemented a script to fix user ID sequence in the database.
109 lines
3.1 KiB
Python
109 lines
3.1 KiB
Python
"""Pydantic schemas for calculation workflows."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from typing import List, Optional
|
|
|
|
from pydantic import BaseModel, Field, PositiveFloat, ValidationError, field_validator
|
|
|
|
from services.pricing import PricingResult
|
|
|
|
|
|
class ImpurityInput(BaseModel):
|
|
"""Impurity configuration row supplied by the client."""
|
|
|
|
name: str = Field(..., min_length=1)
|
|
value: float | None = Field(None, ge=0)
|
|
threshold: float | None = Field(None, ge=0)
|
|
penalty: float | None = Field(None)
|
|
|
|
@field_validator("name")
|
|
@classmethod
|
|
def _normalise_name(cls, value: str) -> str:
|
|
return value.strip()
|
|
|
|
|
|
class ProfitabilityCalculationRequest(BaseModel):
|
|
"""Request payload for profitability calculations."""
|
|
|
|
metal: str = Field(..., min_length=1)
|
|
ore_tonnage: PositiveFloat
|
|
head_grade_pct: float = Field(..., gt=0, le=100)
|
|
recovery_pct: float = Field(..., gt=0, le=100)
|
|
payable_pct: float | None = Field(None, gt=0, le=100)
|
|
reference_price: PositiveFloat
|
|
treatment_charge: float = Field(0, ge=0)
|
|
smelting_charge: float = Field(0, ge=0)
|
|
moisture_pct: float = Field(0, ge=0, le=100)
|
|
moisture_threshold_pct: float | None = Field(None, ge=0, le=100)
|
|
moisture_penalty_per_pct: float | None = None
|
|
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)
|
|
sustaining_capex: float = Field(0, ge=0)
|
|
initial_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)
|
|
|
|
@field_validator("currency_code")
|
|
@classmethod
|
|
def _uppercase_currency(cls, value: str | None) -> str | None:
|
|
if value is None:
|
|
return None
|
|
return value.strip().upper()
|
|
|
|
@field_validator("metal")
|
|
@classmethod
|
|
def _normalise_metal(cls, value: str) -> str:
|
|
return value.strip().lower()
|
|
|
|
|
|
class ProfitabilityCosts(BaseModel):
|
|
"""Aggregated cost components for profitability output."""
|
|
|
|
processing_opex_total: float
|
|
sustaining_capex_total: float
|
|
initial_capex: float
|
|
|
|
|
|
class ProfitabilityMetrics(BaseModel):
|
|
"""Financial KPIs yielded by the profitability calculation."""
|
|
|
|
npv: float | None
|
|
irr: float | None
|
|
payback_period: float | None
|
|
margin: float | None
|
|
|
|
|
|
class CashFlowEntry(BaseModel):
|
|
"""Normalized cash flow row for reporting and charting."""
|
|
|
|
period: int
|
|
revenue: float
|
|
processing_opex: float
|
|
sustaining_capex: float
|
|
net: float
|
|
|
|
|
|
class ProfitabilityCalculationResult(BaseModel):
|
|
"""Response body summarizing profitability calculation outputs."""
|
|
|
|
pricing: PricingResult
|
|
costs: ProfitabilityCosts
|
|
metrics: ProfitabilityMetrics
|
|
cash_flows: list[CashFlowEntry]
|
|
currency: str | None
|
|
|
|
|
|
__all__ = [
|
|
"ImpurityInput",
|
|
"ProfitabilityCalculationRequest",
|
|
"ProfitabilityCosts",
|
|
"ProfitabilityMetrics",
|
|
"CashFlowEntry",
|
|
"ProfitabilityCalculationResult",
|
|
"ValidationError",
|
|
]
|