Functional Weave
Code in Python

banking.fees-cap@1.0.0

impl/rust.rs

4,720 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::funejson::Value;  ← the fune runtime: the JSON value the test vectors use; fune build keeps it only where a signature takes one
use super::dates_add_days::parse_iso_date;  ← from dates.add-days ^1.0.0 · built alongside by fune
use super::money_amount::{money, money_from_value, money_to_value, Money};  ← from money.amount ^1.0.0 · built alongside by fune

/// The charging period a date falls in, as (year, month) of its start. With
/// `cycle_day` 15, 2026-03-14 belongs to the period that began on 2026-02-15.
fn period_key(iso: &str, cycle_day: i64) -> (i64, i64) {
    let d = parse_iso_date(iso);
    let (mut year, mut month) = (d.year, d.month);
    if d.day < cycle_day {
        month -= 1;
        if month == 0 {
            month = 12;
            year -= 1;
        }
    }
    (year, month)
}

/// Apply a monthly cap to a list of charges.
///
/// Within each charging period the charges are taken in date order (ties in
/// input order) until the cap is used up: the charge that crosses the cap is
/// reduced to what is left, and every later charge in that period is waived in
/// full. The result keeps the input order.
///
/// # Panics
/// Panics on a cycle day outside 1-28, a negative cap or charge, a currency
/// mismatch, or a malformed date.
pub fn apply_fee_cap(charges: &[FeeCharge], monthly_cap: &Money, cycle_day: i64) -> FeeCapResult {
    if !(1..=28).contains(&cycle_day) {
        panic!("cycleDay must be 1 to 28, received {}", cycle_day);
    }
    if monthly_cap.minor < 0 {
        panic!("monthlyCap must not be negative, received {}", monthly_cap.minor);
    }
    let currency = monthly_cap.currency.as_str();
    let mut keys: Vec<(i64, i64)> = Vec::new();
    for charge in charges {
        if charge.amount.currency != currency {
            panic!("currency mismatch: {} and {}", charge.amount.currency, currency);
        }
        if charge.amount.minor < 0 {
            panic!("charge \"{}\" must not be negative, received {}", charge.label, charge.amount.minor);
        }
        keys.push(period_key(&charge.date, cycle_day));
    }

    // sort_by is stable, so equal dates keep their input order.
    let mut order: Vec<usize> = (0..charges.len()).collect();
    order.sort_by(|a, b| charges[*a].date.cmp(&charges[*b].date));

    let mut used: Vec<((i64, i64), i64)> = Vec::new();
    let mut charged_minor = vec![0i64; charges.len()];
    for index in order {
        let slot = match used.iter().position(|(k, _)| *k == keys[index]) {
            Some(p) => p,
            None => {
                used.push((keys[index], 0));
                used.len() - 1
            }
        };
        let spent = used[slot].1;
        let take = charges[index].amount.minor.min(monthly_cap.minor - spent);
        charged_minor[index] = take;
        used[slot].1 = spent + take;
    }

    let mut out = Vec::new();
    let mut total_charged = 0i64;
    let mut total_waived = 0i64;
    for (index, charge) in charges.iter().enumerate() {
        let charged = charged_minor[index];
        let waived = charge.amount.minor - charged;
        total_charged += charged;
        total_waived += waived;
        out.push(CappedCharge {
            date: charge.date.clone(),
            label: charge.label.clone(),
            amount: money(charge.amount.minor, currency),
            charged: money(charged, currency),
            waived: money(waived, currency),
        });
    }
    FeeCapResult {
        charges: out,
        total_charged: money(total_charged, currency),
        total_waived: money(total_waived, currency),
    }
}

pub fn fee_charge_from_value(v: &Value) -> FeeCharge {
    FeeCharge {
        date: v.get("date").as_str().to_string(),
        label: v.get("label").as_str().to_string(),
        amount: money_from_value(v.get("amount")),
    }
}

pub fn fee_cap_result_to_value(result: &FeeCapResult) -> Value {
    Value::obj(vec![
        (
            "charges",
            Value::Arr(
                result
                    .charges
                    .iter()
                    .map(|c| {
                        Value::obj(vec![
                            ("date", Value::str(&c.date)),
                            ("label", Value::str(&c.label)),
                            ("amount", money_to_value(&c.amount)),
                            ("charged", money_to_value(&c.charged)),
                            ("waived", money_to_value(&c.waived)),
                        ])
                    })
                    .collect(),
            ),
        ),
        ("totalCharged", money_to_value(&result.total_charged)),
        ("totalWaived", money_to_value(&result.total_waived)),
    ])
}

pub fn fune_vector(args: &[Value]) -> Value {
    let charges: Vec<FeeCharge> = args[0].as_arr().iter().map(fee_charge_from_value).collect();
    fee_cap_result_to_value(&apply_fee_cap(&charges, &money_from_value(&args[1]), args[2].as_i64()))
}