Functional Weave
Code in Rust

hospitality.tourist-tax@1.0.1

impl/python.py

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