Files
calminer/services/unit_of_work.py
zwitschi 522b1e4105
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
feat: add scenarios list page with metrics and quick actions
- 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.
2025-11-13 16:21:36 +01:00

202 lines
7.3 KiB
Python

from __future__ import annotations
from contextlib import AbstractContextManager
from typing import Callable, Sequence
from sqlalchemy.orm import Session
from config.database import SessionLocal
from models import PricingSettings, Project, Role, Scenario
from services.pricing import PricingMetadata
from services.repositories import (
FinancialInputRepository,
PricingSettingsRepository,
PricingSettingsSeedResult,
ProjectRepository,
ProjectProfitabilityRepository,
ProjectOpexRepository,
ProjectCapexRepository,
RoleRepository,
ScenarioRepository,
ScenarioProfitabilityRepository,
ScenarioOpexRepository,
ScenarioCapexRepository,
SimulationParameterRepository,
UserRepository,
ensure_admin_user as ensure_admin_user_record,
ensure_default_pricing_settings,
ensure_default_roles,
pricing_settings_to_metadata,
NavigationRepository,
)
from services.scenario_validation import ScenarioComparisonValidator
class UnitOfWork(AbstractContextManager["UnitOfWork"]):
"""Simple unit-of-work wrapper around SQLAlchemy sessions."""
def __init__(self, session_factory: Callable[[], Session] = SessionLocal) -> None:
self._session_factory = session_factory
self.session: Session | None = None
self._scenario_validator: ScenarioComparisonValidator | None = None
self.projects: ProjectRepository | None = None
self.scenarios: ScenarioRepository | None = None
self.financial_inputs: FinancialInputRepository | None = None
self.simulation_parameters: SimulationParameterRepository | None = None
self.project_profitability: ProjectProfitabilityRepository | None = None
self.project_capex: ProjectCapexRepository | None = None
self.project_opex: ProjectOpexRepository | None = None
self.scenario_profitability: ScenarioProfitabilityRepository | None = None
self.scenario_capex: ScenarioCapexRepository | None = None
self.scenario_opex: ScenarioOpexRepository | None = None
self.users: UserRepository | None = None
self.roles: RoleRepository | None = None
self.pricing_settings: PricingSettingsRepository | None = None
self.navigation: NavigationRepository | None = None
def __enter__(self) -> "UnitOfWork":
self.session = self._session_factory()
self.projects = ProjectRepository(self.session)
self.scenarios = ScenarioRepository(self.session)
self.financial_inputs = FinancialInputRepository(self.session)
self.simulation_parameters = SimulationParameterRepository(
self.session)
self.project_profitability = ProjectProfitabilityRepository(
self.session)
self.project_capex = ProjectCapexRepository(self.session)
self.project_opex = ProjectOpexRepository(
self.session)
self.scenario_profitability = ScenarioProfitabilityRepository(
self.session
)
self.scenario_capex = ScenarioCapexRepository(self.session)
self.scenario_opex = ScenarioOpexRepository(
self.session)
self.users = UserRepository(self.session)
self.roles = RoleRepository(self.session)
self.pricing_settings = PricingSettingsRepository(self.session)
self.navigation = NavigationRepository(self.session)
self._scenario_validator = ScenarioComparisonValidator()
return self
def __exit__(self, exc_type, exc_value, traceback) -> None:
assert self.session is not None
if exc_type is None:
self.session.commit()
else:
self.session.rollback()
self.session.close()
self._scenario_validator = None
self.projects = None
self.scenarios = None
self.financial_inputs = None
self.simulation_parameters = None
self.project_profitability = None
self.project_capex = None
self.project_opex = None
self.scenario_profitability = None
self.scenario_capex = None
self.scenario_opex = None
self.users = None
self.roles = None
self.pricing_settings = None
self.navigation = None
def flush(self) -> None:
if not self.session:
raise RuntimeError("UnitOfWork session is not initialised")
self.session.flush()
def commit(self) -> None:
if not self.session:
raise RuntimeError("UnitOfWork session is not initialised")
self.session.commit()
def rollback(self) -> None:
if not self.session:
raise RuntimeError("UnitOfWork session is not initialised")
self.session.rollback()
def validate_scenarios_for_comparison(
self, scenario_ids: Sequence[int]
) -> list[Scenario]:
if not self.session or not self._scenario_validator or not self.scenarios:
raise RuntimeError("UnitOfWork session is not initialised")
scenarios = [self.scenarios.get(scenario_id)
for scenario_id in scenario_ids]
self._scenario_validator.validate(scenarios)
return scenarios
def validate_scenario_models_for_comparison(
self, scenarios: Sequence[Scenario]
) -> None:
if not self._scenario_validator:
raise RuntimeError("UnitOfWork session is not initialised")
self._scenario_validator.validate(scenarios)
def ensure_default_roles(self) -> list[Role]:
if not self.roles:
raise RuntimeError("UnitOfWork session is not initialised")
return ensure_default_roles(self.roles)
def ensure_admin_user(
self,
*,
email: str,
username: str,
password: str,
) -> None:
if not self.users or not self.roles:
raise RuntimeError("UnitOfWork session is not initialised")
ensure_default_roles(self.roles)
ensure_admin_user_record(
self.users,
self.roles,
email=email,
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)