Functional Weave
Code in TypeScript

insurance.premium-proration@1.0.1

impl/python.py

3,989 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 Optional, Sequence

from .dates_add_months import add_months  ← from dates.add-months ^1.0.0 · built alongside by fune
from .dates_days_between import days_between  ← from dates.days-between ^1.0.0 · built alongside by fune
from .finance_proration import prorate  ← from finance.proration ^1.0.0 · built alongside by fune
from .insurance_premium_proration_types import CancellationBasis, CancellationRefund, ShortPeriodBand
from .money_amount import Money, assert_same_currency, money  ← from money.amount ^1.0.0 · built alongside by fune
from .money_apply_rate import apply_rate  ← from money.apply-rate ^1.0.0 · built alongside by fune


def _is_int(value: object) -> bool:
    return isinstance(value, int) and not isinstance(value, bool)


def cancellation_return_premium(
    premium: Money,
    inception_date: str,
    expiry_date: str,
    cancellation_date: str,
    basis: CancellationBasis,
    short_period_scale: Sequence[ShortPeriodBand],
    minimum_retained: Optional[Money],
) -> CancellationRefund:
    """The return premium when a policy is cancelled mid-term.

    Pro rata keeps the premium for the days on cover. Short period keeps the
    share the insurer's scale gives for how long cover ran ("not exceeding one
    month: 20%"), and a period longer than every band keeps the whole premium.
    A month on the scale is a calendar month from inception, not 30 days.
    """
    if premium.minor < 0:
        raise ValueError("premium must not be negative, received %d" % (premium.minor,))
    total_days = days_between(inception_date, expiry_date)
    if total_days <= 0:
        raise ValueError(
            "expiryDate must be after inceptionDate, received %s to %s" % (inception_date, expiry_date)
        )
    days_in_force = days_between(inception_date, cancellation_date)
    if days_in_force < 0 or days_in_force > total_days:
        raise ValueError(
            "cancellationDate must be from inceptionDate to expiryDate, received %s" % (cancellation_date,)
        )

    if basis == "pro-rata":
        retained = prorate(premium, total_days, days_in_force).used
    elif basis == "short-period":
        if len(short_period_scale) == 0:
            raise ValueError("a short-period cancellation needs a scale")
        share = 10000
        found = False
        previous = 0
        for band in short_period_scale:
            if not _is_int(band.up_to) or band.up_to < 1:
                raise ValueError("upTo must be a whole number of at least 1, received %s" % (band.up_to,))
            if band.unit not in ("days", "months"):
                raise ValueError('unit must be days or months, received "%s"' % (band.unit,))
            bp = band.retained_basis_points
            if not _is_int(bp) or bp < 0 or bp > 10000:
                raise ValueError("retainedBasisPoints must be from 0 to 10000, received %s" % (bp,))
            if bp < previous:
                raise ValueError("a short-period scale must not keep less for a longer period")
            previous = bp
            if found:
                continue
            if band.unit == "days":
                within = days_in_force <= band.up_to
            else:
                within = cancellation_date <= add_months(inception_date, band.up_to)
            if within:
                share = bp
                found = True
        retained = apply_rate(premium, share, "half-up")
    else:
        raise ValueError('unknown basis "%s": use pro-rata or short-period' % (basis,))

    minimum_applied = False
    if minimum_retained is not None:
        assert_same_currency(premium, minimum_retained)
        if minimum_retained.minor < 0:
            raise ValueError("minimumRetained must not be negative, received %d" % (minimum_retained.minor,))
        # The insurer can never keep more than it was paid.
        floor = min(minimum_retained.minor, premium.minor)
        if floor > retained.minor:
            retained = money(floor, premium.currency)
            minimum_applied = True
    return CancellationRefund(
        days_in_force=days_in_force,
        total_days=total_days,
        retained=retained,
        return_premium=money(premium.minor - retained.minor, premium.currency),
        minimum_applied=minimum_applied,
    )