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
from .dates_add_days import add_days ← from dates.add-days ^1.0.0 · built alongside by fune
from .hospitality_tourist_tax_data import VISITOR_LEVIES, VISITOR_LEVIES_HISTORY, VisitorLevy ← this capability’s own data, compiled from data/visitor-levies.json into the same file by fune build
from .hospitality_tourist_tax_types import LevyRule, Stay, TouristTax
from .money_amount import assert_same_currency, 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 _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)