Functional Weave
Code in Rust

banking.faster-payments-limit@1.0.1

impl/typescript.ts

3,044 bytes · the TypeScript 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 { type Money, money } from "./money_amount.ts";  ← from money.amount ^1.0.0 · built alongside by fune
import { SCHEME_LIMITS, SCHEME_LIMITS_HISTORY } from "./banking_faster_payments_limit_data.ts";  ← this capability’s own data, compiled from data/scheme-limits.json into the same file by fune build
import { type FasterPaymentsCheck, type FpsDecision } from "./banking_faster_payments_limit_types.ts";

const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;

/** The scheme maximum per payment in force on a date, in pence. */
function schemeLimitOn(onDate: string): number {
  let best: { limitMinor: number; validFrom: string } | null = null;
  for (const row of SCHEME_LIMITS) {
    if (onDate < row.validFrom) continue;
    if (row.validTo !== null && onDate > row.validTo) continue;
    if (best === null || row.validFrom > best.validFrom) best = row;
  }
  if (best !== null) return best.limitMinor;

  let earliest: string | null = null;
  for (const row of SCHEME_LIMITS) {
    if (earliest === null || row.validFrom < earliest) earliest = row.validFrom;
  }
  // 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" && earliest !== null && onDate < earliest) {
    throw new RangeError(
      `no Faster Payments scheme limit on ${onDate}: this build was installed with history=${SCHEME_LIMITS_HISTORY}, ` +
        `so it only carries limits from ${earliest}. Reinstall with history=full to check older payments.`
    );
  }
  throw new RangeError(`no Faster Payments scheme limit on ${onDate}: the scheme opened on ${earliest}`);
}

/**
 * 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.
 */
export function fasterPaymentsCheck(
  amount: Money,
  sentToday: Money,
  perTransactionLimit: Money,
  dailyLimit: Money,
  onDate: string,
): FasterPaymentsCheck {
  if (!ISO_DATE.test(onDate)) {
    throw new RangeError(`onDate must be an ISO date (YYYY-MM-DD), received "${onDate}"`);
  }
  for (const m of [amount, sentToday, perTransactionLimit, dailyLimit]) {
    if (m.currency !== "GBP") {
      throw new RangeError(`Faster Payments is a sterling scheme: received ${m.currency}`);
    }
  }
  if (amount.minor <= 0) {
    throw new RangeError(`amount must be greater than zero, received ${amount.minor}`);
  }
  if (sentToday.minor < 0 || perTransactionLimit.minor < 0 || dailyLimit.minor < 0) {
    throw new RangeError("sentToday and the limits must not be negative");
  }
  const scheme = schemeLimitOn(onDate);
  const remaining = Math.max(0, dailyLimit.minor - sentToday.minor);
  let decision: FpsDecision = "allowed";
  if (amount.minor > scheme) decision = "over-scheme-limit";
  else if (amount.minor > perTransactionLimit.minor) decision = "over-transaction-limit";
  else if (amount.minor > remaining) decision = "over-daily-limit";
  return {
    decision,
    allowed: decision === "allowed",
    schemeLimit: money(scheme, "GBP"),
    remainingToday: money(remaining, "GBP"),
  };
}