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::add_days; ← from dates.add-days ^1.0.0 · built alongside by fune
use super::dates_add_months::add_months; ← from dates.add-months ^1.0.0 · built alongside by fune
use super::dates_month_boundaries::month_boundaries; ← from dates.month-boundaries ^1.0.0 · built alongside by fune
fn due_date_only(invoice_date: &str, terms: &PaymentTerms) -> String {
let n = terms.n;
match terms.kind.as_str() {
"net" | "end-of-month" | "days-after-month-end" => {
if n < 0 {
panic!("payment terms n must be 0 or more, received {}", n);
}
match terms.kind.as_str() {
"net" => add_days(invoice_date, n),
// Move to the target month first, then take its end: the day of
// the month is irrelevant, so add_months' clamping cannot shift
// the answer.
"end-of-month" => month_boundaries(&add_months(invoice_date, n)).end,
_ => add_days(&month_boundaries(invoice_date).end, n),
}
}
"day-of-next-month" => {
if !(1..=31).contains(&n) {
panic!("day-of-next-month needs a day from 1 to 31, received {}", n);
}
let next = month_boundaries(&add_months(&month_boundaries(invoice_date).start, 1));
add_days(&next.start, n.min(next.days) - 1)
}
other => panic!("unknown payment terms kind \"{}\"", other),
}
}
/// The date an invoice falls due under its payment terms.
///
/// Net N is N calendar days, never "one month": 31 January net 30 is 2 March
/// in 2026. The discount fields do not move the due date, but they are checked
/// here so that terms whose discount outlasts the due date never get further.
///
/// # Panics
/// Panics on a malformed date, an unknown kind, or out-of-range terms.
pub fn payment_due_date(invoice_date: &str, terms: &PaymentTerms) -> String {
let due = due_date_only(invoice_date, terms);
let bp = terms.discount_basis_points;
if !(0..=10000).contains(&bp) {
panic!("discount must be 0 to 10000 basis points, received {}", bp);
}
let days = terms.discount_days;
if days < 0 {
panic!("discount days must be 0 or more, received {}", days);
}
if bp > 0 && add_days(invoice_date, days) > due {
panic!("the discount period ends after the due date {}", due);
}
due
}
pub fn payment_terms_from_value(v: &Value) -> PaymentTerms {
PaymentTerms {
kind: v.get("kind").as_str().to_string(),
n: v.get("n").as_i64(),
discount_basis_points: v.get("discountBasisPoints").as_i64(),
discount_days: v.get("discountDays").as_i64(),
}
}
pub fn fune_vector(args: &[Value]) -> Value {
Value::str(&payment_due_date(args[0].as_str(), &payment_terms_from_value(&args[1])))
}