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"),
};
}