from typing import Optional from .math_round_div import round_div from .money_amount import Money from .professional_utilisation_types import UtilisationRates def _ratio(numerator: int, denominator: int) -> Optional[int]: # A ratio over nothing is not 0% or 100%: None says there was nothing to realise. return None if denominator == 0 else round_div(numerator * 10000, denominator, "half-up") def _check_minutes(name: str, value: int) -> None: if isinstance(value, bool) or not isinstance(value, int) or value < 0: raise ValueError("%s must be a non-negative integer, received %s" % (name, value)) def utilisation_rates( billable_minutes: int, available_minutes: int, standard_value: Money, billed_value: Money, collected_value: Money, ) -> UtilisationRates: """Utilisation and billing, collection and overall realisation, in basis points.""" _check_minutes("billableMinutes", billable_minutes) _check_minutes("availableMinutes", available_minutes) if available_minutes == 0: raise ValueError("availableMinutes must be greater than zero") for name, amount in (("standardValue", standard_value), ("billedValue", billed_value), ("collectedValue", collected_value)): if amount.currency != standard_value.currency: raise ValueError("currency mismatch: %s and %s" % (standard_value.currency, amount.currency)) if amount.minor < 0: raise ValueError("%s must not be negative, received %s" % (name, amount.minor)) return UtilisationRates( utilisation_basis_points=round_div(billable_minutes * 10000, available_minutes, "half-up"), billing_realisation_basis_points=_ratio(billed_value.minor, standard_value.minor), collection_realisation_basis_points=_ratio(collected_value.minor, billed_value.minor), overall_realisation_basis_points=_ratio(collected_value.minor, standard_value.minor), )