from typing import Optional, Sequence from .dates_add_months import add_months from .dates_days_between import days_between from .finance_proration import prorate from .insurance_premium_proration_types import CancellationBasis, CancellationRefund, ShortPeriodBand from .money_amount import Money, assert_same_currency, money from .money_apply_rate import apply_rate 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, )