from typing import List, Sequence from .dates_add_days import add_days from .dates_day_of_week import day_of_week from .hospitality_room_rate_types import NightlyRate, RoomNight, RoomRateQuote, StayDiscount from .money_add import subtract_money from .money_apply_rate import apply_rate from .money_sum import sum_money 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), )