Functional Weave
Code in Rust

hospitality.cancellation-charge@1.0.0

impl/python.py

3,041 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_days_between import days_between  ← from dates.days-between ^1.0.0 · built alongside by fune
from .hospitality_cancellation_charge_types import CancellationBand, CancellationCharge
from .money_amount import 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
from .money_sum import sum_money  ← from money.sum ^1.0.0 · built alongside by fune


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


def cancellation_charge(
    policy: Sequence[CancellationBand],
    nightly_rates: Sequence[Money],
    arrival: str,
    cancelled_on: str,
) -> CancellationCharge:
    """The cancellation fee a policy sets for the notice given.

    Notice is counted in calendar days from the day of cancelling to the day of
    arrival, so cancelling on 4 December for an 18 December arrival is 14 days.
    The band with the most notice that the cancellation still meets applies. A
    cancellation on or after the arrival day is 0 days' notice, the no-show band.
    """
    if len(policy) == 0:
        raise ValueError("policy must have at least one band")
    if len(nightly_rates) == 0:
        raise ValueError("nightlyRates must have at least one night")
    seen = set()
    for band in policy:
        if not _whole(band.min_days_notice) or band.min_days_notice < 0:
            raise ValueError("minDaysNotice must not be negative, received %s" % (band.min_days_notice,))
        if band.min_days_notice in seen:
            raise ValueError("policy has two bands for %d days' notice" % (band.min_days_notice,))
        seen.add(band.min_days_notice)
        if band.nights is not None and (not _whole(band.nights) or band.nights < 1):
            raise ValueError("nights must be at least 1 or null for the whole stay, received %s" % (band.nights,))
        if not _whole(band.basis_points) or band.basis_points < 0 or band.basis_points > 10000:
            raise ValueError(
                "basisPoints must be a whole number from 0 to 10000, received %s" % (band.basis_points,)
            )

    days_notice = days_between(cancelled_on, arrival)
    effective = max(days_notice, 0)
    chosen: Optional[CancellationBand] = None
    for band in policy:
        if band.min_days_notice <= effective and (chosen is None or band.min_days_notice > chosen.min_days_notice):
            chosen = band
    if chosen is None:
        raise ValueError("no band in the policy covers %d days' notice" % (effective,))

    charged_nights = len(nightly_rates) if chosen.nights is None else min(chosen.nights, len(nightly_rates))
    currency = nightly_rates[0].currency
    charge_base = sum_money(list(nightly_rates[:charged_nights]), currency)
    # The whole stay is checked for one currency, not only the nights charged.
    sum_money(nightly_rates, currency)
    return CancellationCharge(
        days_notice=days_notice,
        min_days_notice=chosen.min_days_notice,
        charged_nights=charged_nights,
        charge_base=charge_base,
        basis_points=chosen.basis_points,
        charge=apply_rate(charge_base, chosen.basis_points, "half-up"),
    )