Functional Weave
Code in Rust

banking.faster-payments-limit@1.0.1

impl/rust.rs

4,308 bytes · the Rust 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.

use super::banking_faster_payments_limit_data::{SCHEME_LIMITS, SCHEME_LIMITS_HISTORY};  ← this capability’s own data, compiled from data/scheme-limits.json into the same file by fune build
use super::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::money_amount::{money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune

fn is_iso_date(value: &str) -> bool {
    let bytes = value.as_bytes();
    bytes.len() == 10
        && bytes[4] == b'-'
        && bytes[7] == b'-'
        && bytes
            .iter()
            .enumerate()
            .all(|(i, b)| i == 4 || i == 7 || b.is_ascii_digit())
}

/// The scheme maximum per payment in force on a date, in pence.
fn scheme_limit_on(on_date: &str) -> i64 {
    let mut best: Option<(i64, &'static str)> = None;
    for row in SCHEME_LIMITS {
        if on_date < row.valid_from {
            continue;
        }
        if let Some(valid_to) = row.valid_to {
            if on_date > valid_to {
                continue;
            }
        }
        if best.map_or(true, |(_, from)| row.valid_from > from) {
            best = Some((row.limit_minor, row.valid_from));
        }
    }
    if let Some((limit, _)) = best {
        return limit;
    }
    let mut earliest: Option<&'static str> = None;
    for row in SCHEME_LIMITS {
        if earliest.map_or(true, |e| row.valid_from < e) {
            earliest = Some(row.valid_from);
        }
    }
    let earliest = earliest.unwrap_or("");
    // 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.is_empty() && on_date < earliest {
        panic!(
            "no Faster Payments scheme limit on {}: this build was installed with history={}, so it only carries limits from {}. Reinstall with history=full to check older payments.",
            on_date, SCHEME_LIMITS_HISTORY, earliest
        );
    }
    panic!("no Faster Payments scheme limit on {}: the scheme opened on {}", on_date, 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.
///
/// # Panics
/// Panics on a malformed date, a date before the scheme opened, a currency
/// other than GBP, an amount of zero or less, or a negative limit.
pub fn faster_payments_check(
    amount: &Money,
    sent_today: &Money,
    per_transaction_limit: &Money,
    daily_limit: &Money,
    on_date: &str,
) -> FasterPaymentsCheck {
    if !is_iso_date(on_date) {
        panic!("onDate must be an ISO date (YYYY-MM-DD), received \"{}\"", on_date);
    }
    for m in [amount, sent_today, per_transaction_limit, daily_limit] {
        if m.currency != "GBP" {
            panic!("Faster Payments is a sterling scheme: received {}", m.currency);
        }
    }
    if amount.minor <= 0 {
        panic!("amount must be greater than zero, received {}", amount.minor);
    }
    if sent_today.minor < 0 || per_transaction_limit.minor < 0 || daily_limit.minor < 0 {
        panic!("sentToday and the limits must not be negative");
    }
    let scheme = scheme_limit_on(on_date);
    let remaining = (daily_limit.minor - sent_today.minor).max(0);
    let decision = if amount.minor > scheme {
        "over-scheme-limit"
    } else if amount.minor > per_transaction_limit.minor {
        "over-transaction-limit"
    } else if amount.minor > remaining {
        "over-daily-limit"
    } else {
        "allowed"
    };
    FasterPaymentsCheck {
        decision: decision.to_string(),
        allowed: decision == "allowed",
        scheme_limit: money(scheme, "GBP"),
        remaining_today: money(remaining, "GBP"),
    }
}

pub fn faster_payments_check_to_value(check: &FasterPaymentsCheck) -> Value {
    Value::obj(vec![
        ("decision", Value::str(&check.decision)),
        ("allowed", Value::Bool(check.allowed)),
        ("schemeLimit", money_to_value(&check.scheme_limit)),
        ("remainingToday", money_to_value(&check.remaining_today)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    faster_payments_check_to_value(&faster_payments_check(
        &money_from_value(&args[0]),
        &money_from_value(&args[1]),
        &money_from_value(&args[2]),
        &money_from_value(&args[3]),
        args[4].as_str(),
    ))
}