feat: Enhance currency handling and validation across scenarios

- 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.
This commit is contained in:
2025-11-11 18:29:59 +01:00
parent 032e6d2681
commit 795a9f99f4
50 changed files with 5110 additions and 81 deletions

View File

@@ -6,16 +6,21 @@ from typing import Callable, Sequence
from sqlalchemy.orm import Session
from config.database import SessionLocal
from models import Role, Scenario
from models import PricingSettings, Project, Role, Scenario
from services.pricing import PricingMetadata
from services.repositories import (
FinancialInputRepository,
PricingSettingsRepository,
PricingSettingsSeedResult,
ProjectRepository,
RoleRepository,
ScenarioRepository,
SimulationParameterRepository,
UserRepository,
ensure_admin_user as ensure_admin_user_record,
ensure_default_pricing_settings,
ensure_default_roles,
pricing_settings_to_metadata,
)
from services.scenario_validation import ScenarioComparisonValidator
@@ -33,6 +38,7 @@ class UnitOfWork(AbstractContextManager["UnitOfWork"]):
self.simulation_parameters: SimulationParameterRepository | None = None
self.users: UserRepository | None = None
self.roles: RoleRepository | None = None
self.pricing_settings: PricingSettingsRepository | None = None
def __enter__(self) -> "UnitOfWork":
self.session = self._session_factory()
@@ -43,6 +49,7 @@ class UnitOfWork(AbstractContextManager["UnitOfWork"]):
self.session)
self.users = UserRepository(self.session)
self.roles = RoleRepository(self.session)
self.pricing_settings = PricingSettingsRepository(self.session)
self._scenario_validator = ScenarioComparisonValidator()
return self
@@ -60,6 +67,7 @@ class UnitOfWork(AbstractContextManager["UnitOfWork"]):
self.simulation_parameters = None
self.users = None
self.roles = None
self.pricing_settings = None
def flush(self) -> None:
if not self.session:
@@ -116,3 +124,45 @@ class UnitOfWork(AbstractContextManager["UnitOfWork"]):
username=username,
password=password,
)
def ensure_default_pricing_settings(
self,
*,
metadata: PricingMetadata,
slug: str = "default",
name: str | None = None,
description: str | None = None,
) -> PricingSettingsSeedResult:
if not self.pricing_settings:
raise RuntimeError("UnitOfWork session is not initialised")
return ensure_default_pricing_settings(
self.pricing_settings,
metadata=metadata,
slug=slug,
name=name,
description=description,
)
def get_pricing_metadata(
self,
*,
slug: str = "default",
) -> PricingMetadata | None:
if not self.pricing_settings:
raise RuntimeError("UnitOfWork session is not initialised")
settings = self.pricing_settings.find_by_slug(
slug,
include_children=True,
)
if settings is None:
return None
return pricing_settings_to_metadata(settings)
def set_project_pricing_settings(
self,
project: Project,
pricing_settings: PricingSettings | None,
) -> Project:
if not self.projects:
raise RuntimeError("UnitOfWork session is not initialised")
return self.projects.set_pricing_settings(project, pricing_settings)