import re from typing import Optional from .banking_faster_payments_limit_data import SCHEME_LIMITS, SCHEME_LIMITS_HISTORY from .banking_faster_payments_limit_types import FasterPaymentsCheck, FpsDecision from .money_amount import Money, money ISO_DATE = re.compile(r"[0-9]{4}-[0-9]{2}-[0-9]{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.fullmatch(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"), )