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()))
}