44 lines
1.3 KiB
Python
44 lines
1.3 KiB
Python
"""Utilities for currency normalization within pricing and financial workflows."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from dataclasses import dataclass
|
|
|
|
VALID_CURRENCY_PATTERN = re.compile(r"^[A-Z]{3}$")
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class CurrencyValidationError(ValueError):
|
|
"""Raised when a currency code fails validation."""
|
|
|
|
code: str
|
|
|
|
def __str__(self) -> str: # pragma: no cover - dataclass repr not required in tests
|
|
return f"Invalid currency code: {self.code!r}"
|
|
|
|
|
|
def normalise_currency(code: str | None) -> str | None:
|
|
"""Normalise currency codes to uppercase ISO-4217 values."""
|
|
|
|
if code is None:
|
|
return None
|
|
candidate = code.strip().upper()
|
|
if not VALID_CURRENCY_PATTERN.match(candidate):
|
|
raise CurrencyValidationError(candidate)
|
|
return candidate
|
|
|
|
|
|
def require_currency(code: str | None, default: str | None = None) -> str:
|
|
"""Return normalised currency code, falling back to default when missing."""
|
|
|
|
normalised = normalise_currency(code)
|
|
if normalised is not None:
|
|
return normalised
|
|
if default is None:
|
|
raise CurrencyValidationError("<missing currency>")
|
|
fallback = normalise_currency(default)
|
|
if fallback is None:
|
|
raise CurrencyValidationError("<invalid default currency>")
|
|
return fallback
|