- Updated form template to prefill currency input with default value and added help text for clarity. - Modified integration tests to assert more descriptive error messages for invalid currency codes. - Introduced new tests for currency normalization and validation in various scenarios, including imports and exports. - Added comprehensive tests for pricing calculations, ensuring defaults are respected and overrides function correctly. - Implemented unit tests for pricing settings repository, ensuring CRUD operations and default settings are handled properly. - Enhanced scenario pricing evaluation tests to validate currency handling and metadata defaults. - Added simulation tests to ensure Monte Carlo runs are accurate and handle various distribution scenarios.
122 lines
3.8 KiB
Python
122 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from datetime import date, datetime
|
|
from typing import Iterable
|
|
|
|
from models import MiningOperationType, ResourceType, ScenarioStatus
|
|
from services.currency import CurrencyValidationError, normalise_currency
|
|
|
|
|
|
def _normalise_lower_strings(values: Iterable[str]) -> tuple[str, ...]:
|
|
unique: set[str] = set()
|
|
for value in values:
|
|
if not value:
|
|
continue
|
|
trimmed = value.strip().lower()
|
|
if not trimmed:
|
|
continue
|
|
unique.add(trimmed)
|
|
return tuple(sorted(unique))
|
|
|
|
|
|
def _normalise_upper_strings(values: Iterable[str | None]) -> tuple[str, ...]:
|
|
unique: set[str] = set()
|
|
for value in values:
|
|
if value is None:
|
|
continue
|
|
candidate = value if isinstance(value, str) else str(value)
|
|
candidate = candidate.strip()
|
|
if not candidate:
|
|
continue
|
|
try:
|
|
normalised = normalise_currency(candidate)
|
|
except CurrencyValidationError as exc:
|
|
raise ValueError(str(exc)) from exc
|
|
if normalised is None:
|
|
continue
|
|
unique.add(normalised)
|
|
return tuple(sorted(unique))
|
|
|
|
|
|
@dataclass(slots=True, frozen=True)
|
|
class ProjectExportFilters:
|
|
"""Filter parameters for project export queries."""
|
|
|
|
ids: tuple[int, ...] = ()
|
|
names: tuple[str, ...] = ()
|
|
name_contains: str | None = None
|
|
locations: tuple[str, ...] = ()
|
|
operation_types: tuple[MiningOperationType, ...] = ()
|
|
created_from: datetime | None = None
|
|
created_to: datetime | None = None
|
|
updated_from: datetime | None = None
|
|
updated_to: datetime | None = None
|
|
|
|
def normalised_ids(self) -> tuple[int, ...]:
|
|
unique = {identifier for identifier in self.ids if identifier > 0}
|
|
return tuple(sorted(unique))
|
|
|
|
def normalised_names(self) -> tuple[str, ...]:
|
|
return _normalise_lower_strings(self.names)
|
|
|
|
def normalised_locations(self) -> tuple[str, ...]:
|
|
return _normalise_lower_strings(self.locations)
|
|
|
|
def name_search_pattern(self) -> str | None:
|
|
if not self.name_contains:
|
|
return None
|
|
pattern = self.name_contains.strip()
|
|
if not pattern:
|
|
return None
|
|
return f"%{pattern}%"
|
|
|
|
|
|
@dataclass(slots=True, frozen=True)
|
|
class ScenarioExportFilters:
|
|
"""Filter parameters for scenario export queries."""
|
|
|
|
ids: tuple[int, ...] = ()
|
|
project_ids: tuple[int, ...] = ()
|
|
project_names: tuple[str, ...] = ()
|
|
name_contains: str | None = None
|
|
statuses: tuple[ScenarioStatus, ...] = ()
|
|
start_date_from: date | None = None
|
|
start_date_to: date | None = None
|
|
end_date_from: date | None = None
|
|
end_date_to: date | None = None
|
|
created_from: datetime | None = None
|
|
created_to: datetime | None = None
|
|
updated_from: datetime | None = None
|
|
updated_to: datetime | None = None
|
|
currencies: tuple[str, ...] = ()
|
|
primary_resources: tuple[ResourceType, ...] = ()
|
|
|
|
def normalised_ids(self) -> tuple[int, ...]:
|
|
unique = {identifier for identifier in self.ids if identifier > 0}
|
|
return tuple(sorted(unique))
|
|
|
|
def normalised_project_ids(self) -> tuple[int, ...]:
|
|
unique = {identifier for identifier in self.project_ids if identifier > 0}
|
|
return tuple(sorted(unique))
|
|
|
|
def normalised_project_names(self) -> tuple[str, ...]:
|
|
return _normalise_lower_strings(self.project_names)
|
|
|
|
def name_search_pattern(self) -> str | None:
|
|
if not self.name_contains:
|
|
return None
|
|
pattern = self.name_contains.strip()
|
|
if not pattern:
|
|
return None
|
|
return f"%{pattern}%"
|
|
|
|
def normalised_currencies(self) -> tuple[str, ...]:
|
|
return _normalise_upper_strings(self.currencies)
|
|
|
|
|
|
__all__ = (
|
|
"ProjectExportFilters",
|
|
"ScenarioExportFilters",
|
|
)
|