Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.
from typing import List, Mapping, Optional, Sequence
from .insurance_rating_factors_types import AppliedFactor, FactorTable, PremiumAdjustment, RatedPremium
from .math_round_div import RoundingMode ← from math.round-div ^1.0.0 · built alongside by fune
from .money_amount import Money, assert_same_currency, money ← from money.amount ^1.0.0 · built alongside by fune
def _is_int(value: object) -> bool:
return isinstance(value, int) and not isinstance(value, bool)
def _round_quotient(numerator: int, denominator: int, mode: str) -> int:
# Both are non-negative here: a base of 0 or more times multipliers of 0 or more.
q, r = divmod(numerator, denominator)
twice = r * 2
if mode == "down":
return q
if mode == "up":
return q + 1 if twice > 0 else q
if mode == "half-up":
return q + 1 if twice >= denominator else q
if mode == "half-even":
return q + 1 if twice > denominator or (twice == denominator and q % 2 == 1) else q
raise ValueError('unknown rounding mode "%s"' % (mode,))
def rate_premium(
base_premium: Money,
tables: Sequence[FactorTable],
risk: Mapping[str, str],
adjustments: Sequence[PremiumAdjustment],
minimum_premium: Optional[Money],
maximum_premium: Optional[Money],
mode: RoundingMode,
) -> RatedPremium:
"""Rate a premium: the base times each table's multiplier for the risk,
then each loading or discount, with one rounding at the end, then held
within the minimum and maximum premiums.
Rounding after every factor drifts: 1.01 at 1.005 three times is 1.03
exactly but 1.04 rounded at each step. Python integers are exact at any
size, so there is no limit on the number of factors.
"""
if base_premium.minor < 0:
raise ValueError("basePremium must not be negative, received %d" % (base_premium.minor,))
if minimum_premium is not None:
assert_same_currency(base_premium, minimum_premium)
if maximum_premium is not None:
assert_same_currency(base_premium, maximum_premium)
if minimum_premium is not None and maximum_premium is not None and minimum_premium.minor > maximum_premium.minor:
raise ValueError("minimumPremium must not be more than maximumPremium")
factors: List[AppliedFactor] = []
names = set()
for table in tables:
if table.name in names:
raise ValueError('rating factor "%s" appears twice' % (table.name,))
names.add(table.name)
if table.name not in risk:
raise ValueError('no value for rating factor "%s"' % (table.name,))
key = risk[table.name]
found = None
for row in table.rows:
if row.key == key:
if found is not None:
raise ValueError('rating factor "%s" has two rows for "%s"' % (table.name, key))
if not _is_int(row.basis_points) or row.basis_points < 0:
raise ValueError(
"factor basisPoints must be a whole number of 0 or more, received %s" % (row.basis_points,)
)
found = row.basis_points
if found is None:
raise ValueError('rating factor "%s" has no row for "%s"' % (table.name, key))
factors.append(AppliedFactor(name=table.name, basis_points=found))
for name in risk:
if name not in names:
raise ValueError('risk has a value for "%s", which no table rates' % (name,))
for adjustment in adjustments:
bp = adjustment.basis_points
if not _is_int(bp) or bp < -10000:
raise ValueError("adjustment basisPoints must be a whole number of -10000 or more, received %s" % (bp,))
factors.append(AppliedFactor(name=adjustment.name, basis_points=10000 + bp))
numerator = base_premium.minor
denominator = 1
for f in factors:
numerator *= f.basis_points
denominator *= 10000
c = base_premium.currency
calculated = money(_round_quotient(numerator, denominator, mode), c)
premium = calculated
cap = "none"
if minimum_premium is not None and premium.minor < minimum_premium.minor:
premium = minimum_premium
cap = "minimum"
elif maximum_premium is not None and premium.minor > maximum_premium.minor:
premium = maximum_premium
cap = "maximum"
return RatedPremium(factors=factors, calculated=calculated, premium=premium, cap=cap)