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