use super::banking_faster_payments_limit_data::{SCHEME_LIMITS, SCHEME_LIMITS_HISTORY}; use super::funejson::Value; use super::money_amount::{money, money_from_value, money_to_value, Money}; 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(), )) }