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"),
)