Functional Weave
Code in Rust

construction.vat-reverse-charge@1.0.2

impl/python.py

3,359 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.

import re

from .construction_vat_reverse_charge_data import REVERSE_CHARGE_RULES, REVERSE_CHARGE_RULES_HISTORY, REVERSE_CHARGE_RULES_HORIZON  ← this capability’s own data, compiled from data/reverse-charge.json into the same file by fune build
from .construction_vat_reverse_charge_types import ReverseChargeDecision, ReverseChargeReason, ReverseChargeSupply

ISO_DATE = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{2}")


def _not_applied(reason: ReverseChargeReason) -> ReverseChargeDecision:
    return ReverseChargeDecision(applies=False, reason=reason, invoice_wording=None)


def domestic_reverse_charge(supply: ReverseChargeSupply) -> ReverseChargeDecision:
    """Whether the UK VAT domestic reverse charge for building and construction
    services (VATA 1994 s.55A) applies to one supply.

    The conditions are checked in a fixed order and the first that fails is the
    reason, so the answer is the same in every language and explains itself.
    """
    if not isinstance(supply.supply_date, str) or not ISO_DATE.fullmatch(supply.supply_date):
        raise ValueError('supplyDate must be an ISO date (YYYY-MM-DD), received "%s"' % (supply.supply_date,))
    share = supply.reverse_charge_share_basis_points
    if isinstance(share, bool) or not isinstance(share, int):
        raise TypeError("reverseChargeShareBasisPoints must be an integer, received %r" % (share,))
    if share < 0 or share > 10000:
        raise ValueError("reverseChargeShareBasisPoints must be between 0 and 10000, received %d" % (share,))
    if supply.vat_category not in ("standard", "reduced", "zero", "exempt"):
        raise ValueError(
            'unknown vatCategory "%s": expected standard, reduced, zero or exempt' % (supply.vat_category,)
        )

    date = supply.supply_date
    rule = None
    for row in REVERSE_CHARGE_RULES:
        if row.jurisdiction != "GB" or date < row.valid_from:
            continue
        if row.valid_to is not None and date > row.valid_to:
            continue
        if rule is None or row.valid_from > rule.valid_from:
            rule = row
    if rule is None:
        # A pruned build must not answer "before the reverse charge started" for
        # a date it simply no longer carries rules for.
        if REVERSE_CHARGE_RULES_HORIZON is not None and date < REVERSE_CHARGE_RULES_HORIZON:
            raise ValueError(
                "no reverse charge rule for %s: this build was installed with history=%s, "
                "so it only carries rules from %s. Reinstall with history=full."
                % (date, REVERSE_CHARGE_RULES_HISTORY, REVERSE_CHARGE_RULES_HORIZON)
            )
        return _not_applied("before-start")

    if not supply.supplier_vat_registered:
        return _not_applied("supplier-not-vat-registered")
    if not supply.customer_vat_registered:
        return _not_applied("customer-not-vat-registered")
    if not supply.reported_under_cis:
        return _not_applied("not-reported-under-cis")
    if supply.vat_category not in ("standard", "reduced"):
        return _not_applied("not-standard-or-reduced")
    if supply.employment_business:
        return _not_applied("employment-business")
    if supply.end_user_notified:
        return _not_applied("end-user-notified")
    if share <= rule.disregard_basis_points:
        return _not_applied("five-percent-disregard")
    return ReverseChargeDecision(applies=True, reason="reverse-charge", invoice_wording=rule.invoice_wording)