Functional Weave
Code in Python

hospitality.room-rate@1.0.0

impl/python.py

3,663 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, Sequence

from .dates_add_days import add_days  ← from dates.add-days ^1.0.0 · built alongside by fune
from .dates_day_of_week import day_of_week  ← from dates.day-of-week ^1.0.0 · built alongside by fune
from .hospitality_room_rate_types import NightlyRate, RoomNight, RoomRateQuote, StayDiscount
from .money_add import subtract_money  ← from money.add ^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 room_rate(
    rates: Sequence[NightlyRate],
    stay_discounts: Sequence[StayDiscount],
    arrival: str,
    nights: int,
) -> RoomRateQuote:
    """Price a stay night by night from a rate table, then take off the
    length-of-stay discount the stay qualifies for.

    A night is named by the date it starts, so a Friday night is the night of
    a Friday. Exactly one row must match each night: two matching rows are
    an error rather than a guess about which one the hotel meant.
    """
    if not _whole(nights) or nights < 1:
        raise ValueError("nights must be at least 1, received %s" % (nights,))
    if len(rates) == 0:
        raise ValueError("the rate table is empty")
    for row in rates:
        # add_days parses both ends strictly, so a malformed date fails here.
        add_days(row.first_night, 0)
        add_days(row.last_night, 0)
        if row.last_night < row.first_night:
            raise ValueError('rate "%s" ends before it starts' % (row.season,))
        for d in row.days_of_week:
            if not _whole(d) or d < 1 or d > 7:
                raise ValueError(
                    'daysOfWeek must be 1 (Monday) to 7 (Sunday), received %s in rate "%s"' % (d, row.season)
                )
    seen = set()
    for discount in stay_discounts:
        if not _whole(discount.min_nights) or discount.min_nights < 1:
            raise ValueError("minNights must be at least 1, received %s" % (discount.min_nights,))
        if discount.min_nights in seen:
            raise ValueError("two stay discounts start at %d nights" % (discount.min_nights,))
        seen.add(discount.min_nights)
        if not _whole(discount.basis_points) or discount.basis_points < 0 or discount.basis_points > 10000:
            raise ValueError(
                "basisPoints must be a whole number from 0 to 10000, received %s" % (discount.basis_points,)
            )

    stay: List[RoomNight] = []
    for i in range(nights):
        date = add_days(arrival, i)
        weekday = day_of_week(date)
        matches = [
            row
            for row in rates
            if row.first_night <= date <= row.last_night and (len(row.days_of_week) == 0 or weekday in row.days_of_week)
        ]
        if len(matches) == 0:
            raise ValueError("no rate for the night of %s" % (date,))
        if len(matches) > 1:
            raise ValueError(
                'more than one rate for the night of %s: "%s" and "%s"' % (date, matches[0].season, matches[1].season)
            )
        stay.append(RoomNight(date=date, season=matches[0].season, rate=matches[0].rate))

    subtotal = sum_money([night.rate for night in stay], stay[0].rate.currency)
    discount_basis_points = 0
    best = 0
    for discount in stay_discounts:
        if discount.min_nights <= nights and discount.min_nights > best:
            best = discount.min_nights
            discount_basis_points = discount.basis_points
    discount_amount = apply_rate(subtotal, discount_basis_points, "half-up")
    return RoomRateQuote(
        nights=stay,
        subtotal=subtotal,
        discount_basis_points=discount_basis_points,
        discount=discount_amount,
        total=subtract_money(subtotal, discount_amount),
    )