from typing import Optional from .dates_add_days import add_days from .hospitality_tourist_tax_data import VISITOR_LEVIES, VISITOR_LEVIES_HISTORY, VisitorLevy from .hospitality_tourist_tax_types import LevyRule, Stay, TouristTax from .money_amount import assert_same_currency, 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 _check_rule(rule: LevyRule) -> None: if rule.method not in ("percent", "per-person-night"): raise ValueError('unknown levy method "%s"' % (rule.method,)) if not _whole(rule.basis_points) or rule.basis_points < 0 or rule.basis_points > 10000: raise ValueError("basisPoints must be a whole number from 0 to 10000, received %s" % (rule.basis_points,)) if rule.amount.minor < 0: raise ValueError("amount must not be negative, received %d" % (rule.amount.minor,)) if rule.max_nights is not None and (not _whole(rule.max_nights) or rule.max_nights < 1): raise ValueError("maxNights must be at least 1 or null, received %s" % (rule.max_nights,)) if rule.exempt_over_nights is not None and (not _whole(rule.exempt_over_nights) or rule.exempt_over_nights < 1): raise ValueError("exemptOverNights must be at least 1 or null, received %s" % (rule.exempt_over_nights,)) def _to_rule(row: VisitorLevy) -> LevyRule: return LevyRule( method=row.method, # type: ignore[arg-type] basis_points=row.basis_points, amount=money(row.amount_minor, row.currency), max_nights=row.max_nights, exempt_over_nights=row.exempt_over_nights, under18_exempt=row.under18_exempt, ) def tourist_tax(locality: str, custom_rule: Optional[LevyRule], stay: Stay) -> TouristTax: """The visitor levy on one stay. The rule is the one in force on the arrival date: Edinburgh does not levy a stay that began before its scheme started, even for the nights after. A percentage levy is taken of the accommodation charge of the levied nights and rounded half-up once; a flat levy is the amount times the liable people times the levied nights. """ charges = list(stay.nightly_charges) if len(charges) == 0: raise ValueError("a stay needs at least one night") currency = charges[0].currency sum_money(charges, currency) for c in charges: if c.minor < 0: raise ValueError("nightly charges must not be negative, received %d" % (c.minor,)) if stay.accommodation not in ("standard", "shared"): raise ValueError('unknown accommodation type "%s"' % (stay.accommodation,)) if not _whole(stay.adults) or stay.adults < 0: raise ValueError("adults must not be negative, received %s" % (stay.adults,)) if not _whole(stay.children) or stay.children < 0: raise ValueError("children must not be negative, received %s" % (stay.children,)) add_days(stay.arrival, 0) add_days(stay.booked_on, 0) if stay.first_paid_on is not None: add_days(stay.first_paid_on, 0) def none(exemption: str, rule: Optional[LevyRule]) -> TouristTax: return TouristTax(locality=locality, levy=money(0, currency), leviable_nights=0, exemption=exemption, rule=rule) if locality == "custom": if custom_rule is None: raise ValueError("locality custom needs a customRule") _check_rule(custom_rule) rule = custom_rule else: if custom_rule is not None: raise ValueError("customRule is only used with locality custom") rows = [r for r in VISITOR_LEVIES if r.locality == locality] if not rows: raise ValueError('no visitor levy data for locality "%s"' % (locality,)) row = next( ( r for r in rows if r.accommodation in ("any", stay.accommodation) and r.valid_from <= stay.arrival and (r.valid_to is None or stay.arrival <= r.valid_to) ), None, ) if row is None: if VISITOR_LEVIES_HISTORY != "full": earliest = min(r.valid_from for r in rows) if stay.arrival < earliest: raise ValueError( "no visitor levy rule for %s on %s: this build was installed with history=%s, so it only carries rules from %s" % (locality, stay.arrival, VISITOR_LEVIES_HISTORY, earliest) ) return none("not-in-force", None) rule = _to_rule(row) cutoff = row.paid_bookings_exempt_before if ( cutoff is not None and stay.booked_on < cutoff and stay.first_paid_on is not None and stay.first_paid_on < cutoff ): return none("booked-and-paid-before-scheme", rule) nights = len(charges) if rule.exempt_over_nights is not None and nights > rule.exempt_over_nights: return none("long-stay", rule) leviable = nights if rule.max_nights is None else min(nights, rule.max_nights) if rule.method == "percent": levy = apply_rate(sum_money(charges[:leviable], currency), rule.basis_points, "half-up") else: assert_same_currency(money(0, currency), rule.amount) people = stay.adults + (0 if rule.under18_exempt else stay.children) levy = money(rule.amount.minor * people * leviable, currency) return TouristTax(locality=locality, levy=levy, leviable_nights=leviable, exemption=None, rule=rule)