Functional Weave
Code in Python

banking.faster-payments-limit@1.0.0

impl/python.py

3,161 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 typing import Optional

from .banking_faster_payments_limit_data import SCHEME_LIMITS, SCHEME_LIMITS_HISTORY  ← this capability’s own data, compiled from data/scheme-limits.json into the same file by fune build
from .banking_faster_payments_limit_types import FasterPaymentsCheck, FpsDecision
from .money_amount import Money, money  ← from money.amount ^1.0.0 · built alongside by fune

ISO_DATE = re.compile(r"^\d{4}-\d{2}-\d{2}$")


def _scheme_limit_on(on_date: str) -> int:
    """The scheme maximum per payment in force on a date, in pence."""
    best = None
    for row in SCHEME_LIMITS:
        if on_date < row.valid_from:
            continue
        if row.valid_to is not None and on_date > row.valid_to:
            continue
        if best is None or row.valid_from > best.valid_from:
            best = row
    if best is not None:
        return best.limit_minor

    earliest: Optional[str] = None
    for row in SCHEME_LIMITS:
        if earliest is None or row.valid_from < earliest:
            earliest = row.valid_from
    # A build installed with history=current only carries today's limit, and
    # answering an old date with it would be exactly the wrong answer.
    if SCHEME_LIMITS_HISTORY != "full" and earliest is not None and on_date < earliest:
        raise ValueError(
            "no Faster Payments scheme limit on %s: this build was installed with history=%s, "
            "so it only carries limits from %s. Reinstall with history=full to check older payments."
            % (on_date, SCHEME_LIMITS_HISTORY, earliest)
        )
    raise ValueError("no Faster Payments scheme limit on %s: the scheme opened on %s" % (on_date, earliest))


def faster_payments_check(
    amount: Money,
    sent_today: Money,
    per_transaction_limit: Money,
    daily_limit: Money,
    on_date: str,
) -> FasterPaymentsCheck:
    """Check a Faster Payment against three limits, in order: the scheme
    maximum on the payment date, the bank's per-payment limit, and what is
    left of the bank's daily limit. The decision names the first one broken.
    """
    if not isinstance(on_date, str) or not ISO_DATE.match(on_date):
        raise ValueError('onDate must be an ISO date (YYYY-MM-DD), received "%s"' % (on_date,))
    for m in (amount, sent_today, per_transaction_limit, daily_limit):
        if m.currency != "GBP":
            raise ValueError("Faster Payments is a sterling scheme: received %s" % (m.currency,))
    if amount.minor <= 0:
        raise ValueError("amount must be greater than zero, received %d" % (amount.minor,))
    if sent_today.minor < 0 or per_transaction_limit.minor < 0 or daily_limit.minor < 0:
        raise ValueError("sentToday and the limits must not be negative")
    scheme = _scheme_limit_on(on_date)
    remaining = max(0, daily_limit.minor - sent_today.minor)
    decision: FpsDecision = "allowed"
    if amount.minor > scheme:
        decision = "over-scheme-limit"
    elif amount.minor > per_transaction_limit.minor:
        decision = "over-transaction-limit"
    elif amount.minor > remaining:
        decision = "over-daily-limit"
    return FasterPaymentsCheck(
        decision=decision,
        allowed=decision == "allowed",
        scheme_limit=money(scheme, "GBP"),
        remaining_today=money(remaining, "GBP"),
    )