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:
155
tests/test_pricing.py
Normal file
155
tests/test_pricing.py
Normal file
@@ -0,0 +1,155 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
|
||||
import pytest
|
||||
|
||||
from services.pricing import (
|
||||
PricingInput,
|
||||
PricingMetadata,
|
||||
PricingResult,
|
||||
calculate_pricing,
|
||||
)
|
||||
|
||||
|
||||
def test_calculate_pricing_with_explicit_penalties() -> None:
|
||||
pricing_input = PricingInput(
|
||||
metal="copper",
|
||||
ore_tonnage=100_000,
|
||||
head_grade_pct=1.2,
|
||||
recovery_pct=90,
|
||||
payable_pct=96,
|
||||
reference_price=8_500,
|
||||
treatment_charge=100_000,
|
||||
smelting_charge=0,
|
||||
moisture_pct=10,
|
||||
moisture_threshold_pct=8,
|
||||
moisture_penalty_per_pct=3_000,
|
||||
impurity_ppm={"As": 100},
|
||||
impurity_thresholds={"As": 0},
|
||||
impurity_penalty_per_ppm={"As": 2},
|
||||
premiums=50_000,
|
||||
fx_rate=1.0,
|
||||
currency_code="usd",
|
||||
)
|
||||
|
||||
result = calculate_pricing(pricing_input)
|
||||
|
||||
assert isinstance(result, PricingResult)
|
||||
assert math.isclose(result.payable_metal_tonnes, 1_036.8, rel_tol=1e-6)
|
||||
assert math.isclose(result.gross_revenue, 1_036.8 * 8_500, rel_tol=1e-6)
|
||||
assert math.isclose(result.moisture_penalty, 6_000, rel_tol=1e-6)
|
||||
assert math.isclose(result.impurity_penalty, 200, rel_tol=1e-6)
|
||||
assert math.isclose(result.net_revenue, 8_756_600, rel_tol=1e-6)
|
||||
assert result.treatment_smelt_charges == pytest.approx(100_000)
|
||||
assert result.currency == "USD"
|
||||
|
||||
|
||||
def test_calculate_pricing_with_metadata_defaults() -> None:
|
||||
metadata = PricingMetadata(
|
||||
default_payable_pct=95,
|
||||
default_currency="EUR",
|
||||
moisture_threshold_pct=7,
|
||||
moisture_penalty_per_pct=2_000,
|
||||
impurity_thresholds={"Pb": 50},
|
||||
impurity_penalty_per_ppm={"Pb": 1.5},
|
||||
)
|
||||
|
||||
pricing_input = PricingInput(
|
||||
metal="lead",
|
||||
ore_tonnage=50_000,
|
||||
head_grade_pct=5,
|
||||
recovery_pct=85,
|
||||
payable_pct=None,
|
||||
reference_price=2_000,
|
||||
treatment_charge=30_000,
|
||||
smelting_charge=20_000,
|
||||
moisture_pct=9,
|
||||
moisture_threshold_pct=None,
|
||||
moisture_penalty_per_pct=None,
|
||||
impurity_ppm={"Pb": 120},
|
||||
premiums=12_000,
|
||||
fx_rate=1.2,
|
||||
currency_code=None,
|
||||
)
|
||||
|
||||
result = calculate_pricing(pricing_input, metadata=metadata)
|
||||
|
||||
expected_payable = 50_000 * 0.05 * 0.85 * 0.95
|
||||
assert math.isclose(result.payable_metal_tonnes,
|
||||
expected_payable, rel_tol=1e-6)
|
||||
assert result.moisture_penalty == pytest.approx((9 - 7) * 2_000)
|
||||
assert result.impurity_penalty == pytest.approx((120 - 50) * 1.5)
|
||||
assert result.treatment_smelt_charges == pytest.approx(50_000)
|
||||
assert result.currency == "EUR"
|
||||
assert result.net_revenue > 0
|
||||
|
||||
|
||||
def test_calculate_pricing_currency_override() -> None:
|
||||
pricing_input = PricingInput(
|
||||
metal="gold",
|
||||
ore_tonnage=10_000,
|
||||
head_grade_pct=2.5,
|
||||
recovery_pct=92,
|
||||
payable_pct=98,
|
||||
reference_price=60_000,
|
||||
treatment_charge=40_000,
|
||||
smelting_charge=10_000,
|
||||
moisture_pct=5,
|
||||
moisture_threshold_pct=7,
|
||||
moisture_penalty_per_pct=1_000,
|
||||
premiums=25_000,
|
||||
fx_rate=1.0,
|
||||
currency_code="cad",
|
||||
)
|
||||
|
||||
metadata = PricingMetadata(default_currency="USD")
|
||||
|
||||
result = calculate_pricing(
|
||||
pricing_input, metadata=metadata, currency="CAD")
|
||||
|
||||
assert result.currency == "CAD"
|
||||
assert result.net_revenue > 0
|
||||
|
||||
|
||||
def test_calculate_pricing_multiple_inputs_aggregate() -> None:
|
||||
metadata = PricingMetadata(default_currency="USD")
|
||||
inputs = [
|
||||
PricingInput(
|
||||
metal="copper",
|
||||
ore_tonnage=10_000,
|
||||
head_grade_pct=1.5,
|
||||
recovery_pct=88,
|
||||
payable_pct=95,
|
||||
reference_price=8_000,
|
||||
treatment_charge=20_000,
|
||||
smelting_charge=5_000,
|
||||
moisture_pct=7,
|
||||
moisture_threshold_pct=8,
|
||||
moisture_penalty_per_pct=1_000,
|
||||
premiums=0,
|
||||
fx_rate=1.0,
|
||||
currency_code=None,
|
||||
),
|
||||
PricingInput(
|
||||
metal="copper",
|
||||
ore_tonnage=8_000,
|
||||
head_grade_pct=1.1,
|
||||
recovery_pct=90,
|
||||
payable_pct=96,
|
||||
reference_price=8_000,
|
||||
treatment_charge=18_000,
|
||||
smelting_charge=4_000,
|
||||
moisture_pct=9,
|
||||
moisture_threshold_pct=8,
|
||||
moisture_penalty_per_pct=1_000,
|
||||
premiums=0,
|
||||
fx_rate=1.0,
|
||||
currency_code="usd",
|
||||
),
|
||||
]
|
||||
|
||||
results = [calculate_pricing(i, metadata=metadata) for i in inputs]
|
||||
|
||||
assert all(result.currency == "USD" for result in results)
|
||||
assert sum(result.net_revenue for result in results) > 0
|
||||
Reference in New Issue
Block a user