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.
This commit is contained in:
@@ -6,20 +6,25 @@ from decimal import Decimal
|
||||
from typing import Any, Sequence
|
||||
|
||||
from fastapi import APIRouter, Depends, Query, Request, status
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, Response
|
||||
from fastapi.templating import Jinja2Templates
|
||||
from fastapi.responses import HTMLResponse, JSONResponse, Response, RedirectResponse
|
||||
from pydantic import ValidationError
|
||||
from starlette.datastructures import FormData
|
||||
from starlette.routing import NoMatchFound
|
||||
|
||||
from dependencies import get_pricing_metadata, get_unit_of_work, require_authenticated_user
|
||||
from dependencies import (
|
||||
get_pricing_metadata,
|
||||
get_unit_of_work,
|
||||
require_authenticated_user,
|
||||
require_authenticated_user_html,
|
||||
)
|
||||
from models import (
|
||||
Project,
|
||||
ProjectCapexSnapshot,
|
||||
ProjectProcessingOpexSnapshot,
|
||||
ProjectOpexSnapshot,
|
||||
ProjectProfitability,
|
||||
Scenario,
|
||||
ScenarioCapexSnapshot,
|
||||
ScenarioProcessingOpexSnapshot,
|
||||
ScenarioOpexSnapshot,
|
||||
ScenarioProfitability,
|
||||
User,
|
||||
)
|
||||
@@ -29,17 +34,17 @@ from schemas.calculations import (
|
||||
CapexCalculationResult,
|
||||
CapexComponentInput,
|
||||
CapexParameters,
|
||||
ProcessingOpexCalculationRequest,
|
||||
ProcessingOpexCalculationResult,
|
||||
ProcessingOpexComponentInput,
|
||||
ProcessingOpexOptions,
|
||||
ProcessingOpexParameters,
|
||||
OpexCalculationRequest,
|
||||
OpexCalculationResult,
|
||||
OpexComponentInput,
|
||||
OpexOptions,
|
||||
OpexParameters,
|
||||
ProfitabilityCalculationRequest,
|
||||
ProfitabilityCalculationResult,
|
||||
)
|
||||
from services.calculations import (
|
||||
calculate_initial_capex,
|
||||
calculate_processing_opex,
|
||||
calculate_opex,
|
||||
calculate_profitability,
|
||||
)
|
||||
from services.exceptions import (
|
||||
@@ -50,11 +55,10 @@ from services.exceptions import (
|
||||
)
|
||||
from services.pricing import PricingMetadata
|
||||
from services.unit_of_work import UnitOfWork
|
||||
from routes.template_filters import register_common_filters
|
||||
from routes.template_filters import create_templates
|
||||
|
||||
router = APIRouter(prefix="/calculations", tags=["Calculations"])
|
||||
templates = Jinja2Templates(directory="templates")
|
||||
register_common_filters(templates)
|
||||
templates = create_templates()
|
||||
|
||||
_SUPPORTED_METALS: tuple[dict[str, str], ...] = (
|
||||
{"value": "copper", "label": "Copper"},
|
||||
@@ -90,7 +94,7 @@ _OPEX_FREQUENCY_OPTIONS: tuple[dict[str, str], ...] = (
|
||||
|
||||
_DEFAULT_OPEX_HORIZON_YEARS = 5
|
||||
|
||||
_PROCESSING_OPEX_TEMPLATE = "scenarios/opex.html"
|
||||
_opex_TEMPLATE = "scenarios/opex.html"
|
||||
|
||||
|
||||
def _combine_impurity_metadata(metadata: PricingMetadata) -> list[dict[str, object]]:
|
||||
@@ -196,7 +200,7 @@ def _build_default_form_data(
|
||||
"reference_price": "",
|
||||
"treatment_charge": "",
|
||||
"smelting_charge": "",
|
||||
"processing_opex": "",
|
||||
"opex": "",
|
||||
"moisture_pct": "",
|
||||
"moisture_threshold_pct": moisture_threshold_default,
|
||||
"moisture_penalty_per_pct": moisture_penalty_default,
|
||||
@@ -204,7 +208,7 @@ def _build_default_form_data(
|
||||
"fx_rate": 1.0,
|
||||
"currency_code": currency,
|
||||
"impurities": None,
|
||||
"initial_capex": "",
|
||||
"capex": "",
|
||||
"sustaining_capex": "",
|
||||
"discount_rate": discount_rate,
|
||||
"periods": _DEFAULT_EVALUATION_PERIODS,
|
||||
@@ -380,6 +384,12 @@ def _prepare_capex_context(
|
||||
currency_code = parameters.get(
|
||||
"currency_code") or defaults["currency_code"]
|
||||
|
||||
navigation = _resolve_navigation_links(
|
||||
request,
|
||||
project=project,
|
||||
scenario=scenario,
|
||||
)
|
||||
|
||||
return {
|
||||
"request": request,
|
||||
"project": project,
|
||||
@@ -396,14 +406,14 @@ def _prepare_capex_context(
|
||||
"notices": notices or [],
|
||||
"component_errors": component_errors or [],
|
||||
"component_notices": component_notices or [],
|
||||
"cancel_url": request.headers.get("Referer"),
|
||||
"form_action": request.url.path,
|
||||
"form_action": str(request.url),
|
||||
"csrf_token": None,
|
||||
**navigation,
|
||||
}
|
||||
|
||||
|
||||
def _serialise_opex_component_entry(component: Any) -> dict[str, Any]:
|
||||
if isinstance(component, ProcessingOpexComponentInput):
|
||||
if isinstance(component, OpexComponentInput):
|
||||
raw = component.model_dump()
|
||||
elif isinstance(component, dict):
|
||||
raw = dict(component)
|
||||
@@ -436,7 +446,7 @@ def _serialise_opex_component_entry(component: Any) -> dict[str, Any]:
|
||||
|
||||
|
||||
def _serialise_opex_parameters(parameters: Any) -> dict[str, Any]:
|
||||
if isinstance(parameters, ProcessingOpexParameters):
|
||||
if isinstance(parameters, OpexParameters):
|
||||
raw = parameters.model_dump()
|
||||
elif isinstance(parameters, dict):
|
||||
raw = dict(parameters)
|
||||
@@ -455,7 +465,7 @@ def _serialise_opex_parameters(parameters: Any) -> dict[str, Any]:
|
||||
|
||||
|
||||
def _serialise_opex_options(options: Any) -> dict[str, Any]:
|
||||
if isinstance(options, ProcessingOpexOptions):
|
||||
if isinstance(options, OpexOptions):
|
||||
raw = options.model_dump()
|
||||
elif isinstance(options, dict):
|
||||
raw = dict(options)
|
||||
@@ -511,7 +521,7 @@ def _prepare_opex_context(
|
||||
project: Project | None,
|
||||
scenario: Scenario | None,
|
||||
form_data: dict[str, Any] | None = None,
|
||||
result: ProcessingOpexCalculationResult | None = None,
|
||||
result: OpexCalculationResult | None = None,
|
||||
errors: list[str] | None = None,
|
||||
notices: list[str] | None = None,
|
||||
component_errors: list[str] | None = None,
|
||||
@@ -544,6 +554,12 @@ def _prepare_opex_context(
|
||||
currency_code = parameters.get(
|
||||
"currency_code") or defaults["currency_code"]
|
||||
|
||||
navigation = _resolve_navigation_links(
|
||||
request,
|
||||
project=project,
|
||||
scenario=scenario,
|
||||
)
|
||||
|
||||
return {
|
||||
"request": request,
|
||||
"project": project,
|
||||
@@ -561,9 +577,9 @@ def _prepare_opex_context(
|
||||
"notices": notices or [],
|
||||
"component_errors": component_errors or [],
|
||||
"component_notices": component_notices or [],
|
||||
"cancel_url": request.headers.get("Referer"),
|
||||
"form_action": request.url.path,
|
||||
"form_action": str(request.url),
|
||||
"csrf_token": None,
|
||||
**navigation,
|
||||
}
|
||||
|
||||
|
||||
@@ -758,6 +774,76 @@ async def _extract_capex_payload(request: Request) -> dict[str, Any]:
|
||||
return _capex_form_to_payload(form)
|
||||
|
||||
|
||||
def _resolve_navigation_links(
|
||||
request: Request,
|
||||
*,
|
||||
project: Project | None,
|
||||
scenario: Scenario | None,
|
||||
) -> dict[str, str | None]:
|
||||
project_url: str | None = None
|
||||
scenario_url: str | None = None
|
||||
scenario_portfolio_url: str | None = None
|
||||
|
||||
candidate_project = project
|
||||
if scenario is not None and getattr(scenario, "id", None) is not None:
|
||||
try:
|
||||
scenario_url = str(
|
||||
request.url_for(
|
||||
"scenarios.view_scenario", scenario_id=scenario.id
|
||||
)
|
||||
)
|
||||
except NoMatchFound:
|
||||
scenario_url = None
|
||||
|
||||
try:
|
||||
scenario_portfolio_url = str(
|
||||
request.url_for(
|
||||
"scenarios.project_scenario_list",
|
||||
project_id=scenario.project_id,
|
||||
)
|
||||
)
|
||||
except NoMatchFound:
|
||||
scenario_portfolio_url = None
|
||||
|
||||
if candidate_project is None:
|
||||
candidate_project = getattr(scenario, "project", None)
|
||||
|
||||
if candidate_project is not None and getattr(candidate_project, "id", None) is not None:
|
||||
try:
|
||||
project_url = str(
|
||||
request.url_for(
|
||||
"projects.view_project", project_id=candidate_project.id
|
||||
)
|
||||
)
|
||||
except NoMatchFound:
|
||||
project_url = None
|
||||
|
||||
if scenario_portfolio_url is None:
|
||||
try:
|
||||
scenario_portfolio_url = str(
|
||||
request.url_for(
|
||||
"scenarios.project_scenario_list",
|
||||
project_id=candidate_project.id,
|
||||
)
|
||||
)
|
||||
except NoMatchFound:
|
||||
scenario_portfolio_url = None
|
||||
|
||||
cancel_url = scenario_url or project_url or request.headers.get("Referer")
|
||||
if cancel_url is None:
|
||||
try:
|
||||
cancel_url = str(request.url_for("projects.project_list_page"))
|
||||
except NoMatchFound:
|
||||
cancel_url = "/"
|
||||
|
||||
return {
|
||||
"project_url": project_url,
|
||||
"scenario_url": scenario_url,
|
||||
"scenario_portfolio_url": scenario_portfolio_url,
|
||||
"cancel_url": cancel_url,
|
||||
}
|
||||
|
||||
|
||||
def _prepare_default_context(
|
||||
request: Request,
|
||||
*,
|
||||
@@ -781,6 +867,12 @@ def _prepare_default_context(
|
||||
allow_empty_override=allow_empty_override,
|
||||
)
|
||||
|
||||
navigation = _resolve_navigation_links(
|
||||
request,
|
||||
project=project,
|
||||
scenario=scenario,
|
||||
)
|
||||
|
||||
return {
|
||||
"request": request,
|
||||
"project": project,
|
||||
@@ -792,10 +884,10 @@ def _prepare_default_context(
|
||||
"result": result,
|
||||
"errors": [],
|
||||
"notices": [],
|
||||
"cancel_url": request.headers.get("Referer"),
|
||||
"form_action": request.url.path,
|
||||
"form_action": str(request.url),
|
||||
"csrf_token": None,
|
||||
"default_periods": _DEFAULT_EVALUATION_PERIODS,
|
||||
**navigation,
|
||||
}
|
||||
|
||||
|
||||
@@ -920,11 +1012,11 @@ def _persist_profitability_snapshots(
|
||||
created_by_id = getattr(user, "id", None)
|
||||
|
||||
revenue_total = float(result.pricing.net_revenue)
|
||||
processing_total = float(result.costs.processing_opex_total)
|
||||
processing_total = float(result.costs.opex_total)
|
||||
sustaining_total = float(result.costs.sustaining_capex_total)
|
||||
initial_capex = float(result.costs.initial_capex)
|
||||
capex = float(result.costs.capex)
|
||||
net_cash_flow_total = revenue_total - (
|
||||
processing_total + sustaining_total + initial_capex
|
||||
processing_total + sustaining_total + capex
|
||||
)
|
||||
|
||||
npv_value = (
|
||||
@@ -964,9 +1056,9 @@ def _persist_profitability_snapshots(
|
||||
payback_period_years=payback_value,
|
||||
margin_pct=margin_value,
|
||||
revenue_total=revenue_total,
|
||||
processing_opex_total=processing_total,
|
||||
opex_total=processing_total,
|
||||
sustaining_capex_total=sustaining_total,
|
||||
initial_capex=initial_capex,
|
||||
capex=capex,
|
||||
net_cash_flow_total=net_cash_flow_total,
|
||||
payload=payload,
|
||||
)
|
||||
@@ -983,9 +1075,9 @@ def _persist_profitability_snapshots(
|
||||
payback_period_years=payback_value,
|
||||
margin_pct=margin_value,
|
||||
revenue_total=revenue_total,
|
||||
processing_opex_total=processing_total,
|
||||
opex_total=processing_total,
|
||||
sustaining_capex_total=sustaining_total,
|
||||
initial_capex=initial_capex,
|
||||
capex=capex,
|
||||
net_cash_flow_total=net_cash_flow_total,
|
||||
payload=payload,
|
||||
)
|
||||
@@ -1067,7 +1159,7 @@ def _should_persist_opex(
|
||||
*,
|
||||
project: Project | None,
|
||||
scenario: Scenario | None,
|
||||
request_model: ProcessingOpexCalculationRequest,
|
||||
request_model: OpexCalculationRequest,
|
||||
) -> bool:
|
||||
persist_requested = bool(
|
||||
getattr(request_model, "options", None)
|
||||
@@ -1082,8 +1174,8 @@ def _persist_opex_snapshots(
|
||||
project: Project | None,
|
||||
scenario: Scenario | None,
|
||||
user: User | None,
|
||||
request_model: ProcessingOpexCalculationRequest,
|
||||
result: ProcessingOpexCalculationResult,
|
||||
request_model: OpexCalculationRequest,
|
||||
result: OpexCalculationResult,
|
||||
) -> None:
|
||||
if not _should_persist_opex(
|
||||
project=project,
|
||||
@@ -1130,11 +1222,11 @@ def _persist_opex_snapshots(
|
||||
"result": result.model_dump(),
|
||||
}
|
||||
|
||||
if scenario and uow.scenario_processing_opex:
|
||||
scenario_snapshot = ScenarioProcessingOpexSnapshot(
|
||||
if scenario and uow.scenario_opex:
|
||||
scenario_snapshot = ScenarioOpexSnapshot(
|
||||
scenario_id=scenario.id,
|
||||
created_by_id=created_by_id,
|
||||
calculation_source="calculations.processing_opex",
|
||||
calculation_source="calculations.opex",
|
||||
currency_code=result.currency,
|
||||
overall_annual=overall_annual,
|
||||
escalated_total=escalated_total,
|
||||
@@ -1145,13 +1237,13 @@ def _persist_opex_snapshots(
|
||||
component_count=component_count,
|
||||
payload=payload,
|
||||
)
|
||||
uow.scenario_processing_opex.create(scenario_snapshot)
|
||||
uow.scenario_opex.create(scenario_snapshot)
|
||||
|
||||
if project and uow.project_processing_opex:
|
||||
project_snapshot = ProjectProcessingOpexSnapshot(
|
||||
if project and uow.project_opex:
|
||||
project_snapshot = ProjectOpexSnapshot(
|
||||
project_id=project.id,
|
||||
created_by_id=created_by_id,
|
||||
calculation_source="calculations.processing_opex",
|
||||
calculation_source="calculations.opex",
|
||||
currency_code=result.currency,
|
||||
overall_annual=overall_annual,
|
||||
escalated_total=escalated_total,
|
||||
@@ -1162,24 +1254,24 @@ def _persist_opex_snapshots(
|
||||
component_count=component_count,
|
||||
payload=payload,
|
||||
)
|
||||
uow.project_processing_opex.create(project_snapshot)
|
||||
uow.project_opex.create(project_snapshot)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/processing-opex",
|
||||
"/opex",
|
||||
response_class=HTMLResponse,
|
||||
name="calculations.processing_opex_form",
|
||||
name="calculations.opex_form",
|
||||
)
|
||||
def processing_opex_form(
|
||||
def opex_form(
|
||||
request: Request,
|
||||
_: User = Depends(require_authenticated_user),
|
||||
_: User = Depends(require_authenticated_user_html),
|
||||
uow: UnitOfWork = Depends(get_unit_of_work),
|
||||
project_id: int | None = Query(
|
||||
None, description="Optional project identifier"),
|
||||
scenario_id: int | None = Query(
|
||||
None, description="Optional scenario identifier"),
|
||||
) -> HTMLResponse:
|
||||
"""Render the processing opex planner with default context."""
|
||||
"""Render the opex planner with default context."""
|
||||
|
||||
project, scenario = _load_project_and_scenario(
|
||||
uow=uow, project_id=project_id, scenario_id=scenario_id
|
||||
@@ -1189,14 +1281,14 @@ def processing_opex_form(
|
||||
project=project,
|
||||
scenario=scenario,
|
||||
)
|
||||
return templates.TemplateResponse(_PROCESSING_OPEX_TEMPLATE, context)
|
||||
return templates.TemplateResponse(_opex_TEMPLATE, context)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/processing-opex",
|
||||
name="calculations.processing_opex_submit",
|
||||
"/opex",
|
||||
name="calculations.opex_submit",
|
||||
)
|
||||
async def processing_opex_submit(
|
||||
async def opex_submit(
|
||||
request: Request,
|
||||
current_user: User = Depends(require_authenticated_user),
|
||||
uow: UnitOfWork = Depends(get_unit_of_work),
|
||||
@@ -1205,16 +1297,16 @@ async def processing_opex_submit(
|
||||
scenario_id: int | None = Query(
|
||||
None, description="Optional scenario identifier"),
|
||||
) -> Response:
|
||||
"""Handle processing opex submissions and respond with HTML or JSON."""
|
||||
"""Handle opex submissions and respond with HTML or JSON."""
|
||||
|
||||
wants_json = _is_json_request(request)
|
||||
payload_data = await _extract_opex_payload(request)
|
||||
|
||||
try:
|
||||
request_model = ProcessingOpexCalculationRequest.model_validate(
|
||||
request_model = OpexCalculationRequest.model_validate(
|
||||
payload_data
|
||||
)
|
||||
result = calculate_processing_opex(request_model)
|
||||
result = calculate_opex(request_model)
|
||||
except ValidationError as exc:
|
||||
if wants_json:
|
||||
return JSONResponse(
|
||||
@@ -1237,7 +1329,7 @@ async def processing_opex_submit(
|
||||
component_errors=component_errors,
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
_PROCESSING_OPEX_TEMPLATE,
|
||||
_opex_TEMPLATE,
|
||||
context,
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
)
|
||||
@@ -1263,7 +1355,7 @@ async def processing_opex_submit(
|
||||
errors=errors,
|
||||
)
|
||||
return templates.TemplateResponse(
|
||||
_PROCESSING_OPEX_TEMPLATE,
|
||||
_opex_TEMPLATE,
|
||||
context,
|
||||
status_code=status.HTTP_422_UNPROCESSABLE_ENTITY,
|
||||
)
|
||||
@@ -1295,10 +1387,10 @@ async def processing_opex_submit(
|
||||
result=result,
|
||||
)
|
||||
notices = _list_from_context(context, "notices")
|
||||
notices.append("Processing opex calculation completed successfully.")
|
||||
notices.append("Opex calculation completed successfully.")
|
||||
|
||||
return templates.TemplateResponse(
|
||||
_PROCESSING_OPEX_TEMPLATE,
|
||||
_opex_TEMPLATE,
|
||||
context,
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
@@ -1311,14 +1403,14 @@ async def processing_opex_submit(
|
||||
)
|
||||
def capex_form(
|
||||
request: Request,
|
||||
_: User = Depends(require_authenticated_user),
|
||||
_: User = Depends(require_authenticated_user_html),
|
||||
uow: UnitOfWork = Depends(get_unit_of_work),
|
||||
project_id: int | None = Query(
|
||||
None, description="Optional project identifier"),
|
||||
scenario_id: int | None = Query(
|
||||
None, description="Optional scenario identifier"),
|
||||
) -> HTMLResponse:
|
||||
"""Render the initial capex planner template with defaults."""
|
||||
"""Render the capex planner template with defaults."""
|
||||
|
||||
project, scenario = _load_project_and_scenario(
|
||||
uow=uow, project_id=project_id, scenario_id=scenario_id
|
||||
@@ -1432,7 +1524,7 @@ async def capex_submit(
|
||||
result=result,
|
||||
)
|
||||
notices = _list_from_context(context, "notices")
|
||||
notices.append("Initial capex calculation completed successfully.")
|
||||
notices.append("Capex calculation completed successfully.")
|
||||
|
||||
return templates.TemplateResponse(
|
||||
"scenarios/capex.html",
|
||||
@@ -1441,26 +1533,35 @@ async def capex_submit(
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/profitability",
|
||||
response_class=HTMLResponse,
|
||||
name="calculations.profitability_form",
|
||||
)
|
||||
def profitability_form(
|
||||
def _render_profitability_form(
|
||||
request: Request,
|
||||
_: User = Depends(require_authenticated_user),
|
||||
metadata: PricingMetadata = Depends(get_pricing_metadata),
|
||||
uow: UnitOfWork = Depends(get_unit_of_work),
|
||||
project_id: int | None = Query(
|
||||
None, description="Optional project identifier"),
|
||||
scenario_id: int | None = Query(
|
||||
None, description="Optional scenario identifier"),
|
||||
) -> HTMLResponse:
|
||||
"""Render the profitability calculation form with default metadata."""
|
||||
|
||||
*,
|
||||
metadata: PricingMetadata,
|
||||
uow: UnitOfWork,
|
||||
project_id: int | None,
|
||||
scenario_id: int | None,
|
||||
allow_redirect: bool,
|
||||
) -> Response:
|
||||
project, scenario = _load_project_and_scenario(
|
||||
uow=uow, project_id=project_id, scenario_id=scenario_id
|
||||
)
|
||||
|
||||
if allow_redirect and scenario is not None and getattr(scenario, "id", None):
|
||||
target_project_id = project_id or getattr(scenario, "project_id", None)
|
||||
if target_project_id is None and getattr(scenario, "project", None) is not None:
|
||||
target_project_id = getattr(scenario.project, "id", None)
|
||||
|
||||
if target_project_id is not None:
|
||||
redirect_url = request.url_for(
|
||||
"calculations.profitability_form",
|
||||
project_id=target_project_id,
|
||||
scenario_id=scenario.id,
|
||||
)
|
||||
if redirect_url != str(request.url):
|
||||
return RedirectResponse(
|
||||
redirect_url, status_code=status.HTTP_307_TEMPORARY_REDIRECT
|
||||
)
|
||||
|
||||
context = _prepare_default_context(
|
||||
request,
|
||||
project=project,
|
||||
@@ -1471,28 +1572,74 @@ def profitability_form(
|
||||
return templates.TemplateResponse("scenarios/profitability.html", context)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/profitability",
|
||||
name="calculations.profitability_submit",
|
||||
@router.get(
|
||||
"/projects/{project_id}/scenarios/{scenario_id}/profitability",
|
||||
response_class=HTMLResponse,
|
||||
include_in_schema=False,
|
||||
name="calculations.profitability_form",
|
||||
)
|
||||
async def profitability_submit(
|
||||
def profitability_form_for_scenario(
|
||||
request: Request,
|
||||
current_user: User = Depends(require_authenticated_user),
|
||||
project_id: int,
|
||||
scenario_id: int,
|
||||
_: User = Depends(require_authenticated_user_html),
|
||||
metadata: PricingMetadata = Depends(get_pricing_metadata),
|
||||
uow: UnitOfWork = Depends(get_unit_of_work),
|
||||
) -> Response:
|
||||
return _render_profitability_form(
|
||||
request,
|
||||
metadata=metadata,
|
||||
uow=uow,
|
||||
project_id=project_id,
|
||||
scenario_id=scenario_id,
|
||||
allow_redirect=False,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/profitability",
|
||||
response_class=HTMLResponse,
|
||||
)
|
||||
def profitability_form(
|
||||
request: Request,
|
||||
_: User = Depends(require_authenticated_user_html),
|
||||
metadata: PricingMetadata = Depends(get_pricing_metadata),
|
||||
uow: UnitOfWork = Depends(get_unit_of_work),
|
||||
project_id: int | None = Query(
|
||||
None, description="Optional project identifier"),
|
||||
None, description="Optional project identifier"
|
||||
),
|
||||
scenario_id: int | None = Query(
|
||||
None, description="Optional scenario identifier"),
|
||||
None, description="Optional scenario identifier"
|
||||
),
|
||||
) -> Response:
|
||||
"""Handle profitability calculations and return HTML or JSON."""
|
||||
"""Render the profitability calculation form with default metadata."""
|
||||
|
||||
return _render_profitability_form(
|
||||
request,
|
||||
metadata=metadata,
|
||||
uow=uow,
|
||||
project_id=project_id,
|
||||
scenario_id=scenario_id,
|
||||
allow_redirect=True,
|
||||
)
|
||||
|
||||
|
||||
async def _handle_profitability_submission(
|
||||
request: Request,
|
||||
*,
|
||||
current_user: User,
|
||||
metadata: PricingMetadata,
|
||||
uow: UnitOfWork,
|
||||
project_id: int | None,
|
||||
scenario_id: int | None,
|
||||
) -> Response:
|
||||
wants_json = _is_json_request(request)
|
||||
payload_data = await _extract_payload(request)
|
||||
|
||||
try:
|
||||
request_model = ProfitabilityCalculationRequest.model_validate(
|
||||
payload_data)
|
||||
payload_data
|
||||
)
|
||||
result = calculate_profitability(request_model, metadata=metadata)
|
||||
except ValidationError as exc:
|
||||
if wants_json:
|
||||
@@ -1586,3 +1733,53 @@ async def profitability_submit(
|
||||
context,
|
||||
status_code=status.HTTP_200_OK,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/projects/{project_id}/scenarios/{scenario_id}/profitability",
|
||||
include_in_schema=False,
|
||||
name="calculations.profitability_submit",
|
||||
)
|
||||
async def profitability_submit_for_scenario(
|
||||
request: Request,
|
||||
project_id: int,
|
||||
scenario_id: int,
|
||||
current_user: User = Depends(require_authenticated_user),
|
||||
metadata: PricingMetadata = Depends(get_pricing_metadata),
|
||||
uow: UnitOfWork = Depends(get_unit_of_work),
|
||||
) -> Response:
|
||||
return await _handle_profitability_submission(
|
||||
request,
|
||||
current_user=current_user,
|
||||
metadata=metadata,
|
||||
uow=uow,
|
||||
project_id=project_id,
|
||||
scenario_id=scenario_id,
|
||||
)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/profitability",
|
||||
)
|
||||
async def profitability_submit(
|
||||
request: Request,
|
||||
current_user: User = Depends(require_authenticated_user),
|
||||
metadata: PricingMetadata = Depends(get_pricing_metadata),
|
||||
uow: UnitOfWork = Depends(get_unit_of_work),
|
||||
project_id: int | None = Query(
|
||||
None, description="Optional project identifier"
|
||||
),
|
||||
scenario_id: int | None = Query(
|
||||
None, description="Optional scenario identifier"
|
||||
),
|
||||
) -> Response:
|
||||
"""Handle profitability calculations and return HTML or JSON."""
|
||||
|
||||
return await _handle_profitability_submission(
|
||||
request,
|
||||
current_user=current_user,
|
||||
metadata=metadata,
|
||||
uow=uow,
|
||||
project_id=project_id,
|
||||
scenario_id=scenario_id,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user