Functional Weave
Code in Python

insurance.rating-factors@1.0.0

impl/python.py

4,338 bytes · the Python implementation · view raw

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)