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::finance_invoice_calculate::{calculate_invoice, invoice_line_from_value, invoice_to_value, Invoice, InvoiceLine}; ← from finance.invoice.calculate ^1.0.0 · built alongside by fune
use super::finance_invoice_line_total::line_total; ← from finance.invoice.line-total ^1.0.0 · built alongside by fune
use super::money_allocate::allocate; ← from money.allocate ^1.0.0 · built alongside by fune
use super::money_amount::{assert_same_currency, money, money_from_value}; ← from money.amount ^1.0.0 · built alongside by fune
use super::money_apply_rate::apply_rate; ← from money.apply-rate ^1.0.0 · built alongside by fune
fn check_discount(d: &Discount, currency: &str) {
if d.basis_points.is_none() == d.amount_off.is_none() {
panic!(
"a discount needs exactly one of basisPoints and amountOff: \"{}\"",
d.description
);
}
if let Some(bp) = d.basis_points {
if !(0..=10000).contains(&bp) {
panic!("discount basis points must be between 0 and 10000, received {}", bp);
}
}
if let Some(off) = &d.amount_off {
assert_same_currency(&money(0, currency), off);
if off.minor < 0 {
panic!("discount amount must not be negative, received {}", off.minor);
}
}
if d.start_period < 1 {
panic!("discount start period must be 1 or more, received {}", d.start_period);
}
match d.duration.as_str() {
"repeating" => {
if d.duration_periods.map_or(true, |n| n < 1) {
panic!(
"a repeating discount needs durationPeriods of 1 or more, received {:?}",
d.duration_periods
);
}
}
"once" | "forever" => {
if d.duration_periods.is_some() {
panic!(
"durationPeriods applies only to a repeating discount: \"{}\"",
d.description
);
}
}
other => panic!(
"unknown discount duration \"{}\": expected once, repeating or forever",
other
),
}
}
fn applies(d: &Discount, period: i64) -> bool {
match d.duration.as_str() {
"once" => period == d.start_period,
"repeating" => period >= d.start_period && period < d.start_period + d.duration_periods.unwrap_or(0),
_ => period >= d.start_period,
}
}
/// The renewal invoice for one billing period: plan, add-ons and usage, less
/// the coupons in force in that period, taxed per line by
/// finance::invoice::calculate. Each coupon becomes negative lines, one per
/// VAT category, so the reduction is taxed at the rate of what it reduces.
///
/// # Panics
/// Panics on a period below 1, a malformed coupon, or anything
/// `calculate_invoice` refuses.
pub fn renewal_invoice(
plan: &InvoiceLine,
add_ons: &[InvoiceLine],
usage: &[InvoiceLine],
discounts: &[Discount],
period_number: i64,
jurisdiction: &str,
invoice_date: &str,
) -> Invoice {
if period_number < 1 {
panic!("period number must be 1 or more, received {}", period_number);
}
let currency = plan.unit_price.currency.clone();
let mut items: Vec<InvoiceLine> = vec![plan.clone()];
items.extend(add_ons.iter().cloned());
items.extend(usage.iter().cloned());
let mut categories: Vec<String> = Vec::new();
let mut category_net: Vec<i64> = Vec::new();
let mut running: i64 = 0;
for item in &items {
let net = line_total(&item.unit_price, item.quantity, item.discount_basis_points);
assert_same_currency(&money(0, ¤cy), &net);
let k = match categories.iter().position(|c| *c == item.tax_category) {
Some(k) => k,
None => {
categories.push(item.tax_category.clone());
category_net.push(0);
categories.len() - 1
}
};
category_net[k] += net.minor;
running += net.minor;
}
// Credit-only categories take no share of a discount.
let weights: Vec<i64> = category_net.iter().map(|&n| n.max(0)).collect();
let mut discount_lines: Vec<InvoiceLine> = Vec::new();
for d in discounts {
check_discount(d, ¤cy);
if !applies(d, period_number) || running <= 0 {
continue;
}
let amount = match (d.basis_points, &d.amount_off) {
(Some(bp), _) => apply_rate(&money(running, ¤cy), bp, "half-up").minor,
(None, Some(off)) => off.minor.min(running),
(None, None) => 0,
};
if amount == 0 {
continue;
}
running -= amount;
for (k, share) in allocate(&money(amount, ¤cy), &weights).iter().enumerate() {
if share.minor != 0 {
discount_lines.push(InvoiceLine {
description: d.description.clone(),
unit_price: money(-share.minor, ¤cy),
quantity: 1,
discount_basis_points: 0,
tax_category: categories[k].clone(),
});
}
}
}
items.extend(discount_lines);
calculate_invoice(&items, jurisdiction, invoice_date)
}
pub fn discount_from_value(v: &Value) -> Discount {
Discount {
description: v.get("description").as_str().to_string(),
basis_points: if v.get("basisPoints").is_null() { None } else { Some(v.get("basisPoints").as_i64()) },
amount_off: if v.get("amountOff").is_null() { None } else { Some(money_from_value(v.get("amountOff"))) },
duration: v.get("duration").as_str().to_string(),
duration_periods: if v.get("durationPeriods").is_null() { None } else { Some(v.get("durationPeriods").as_i64()) },
start_period: v.get("startPeriod").as_i64(),
}
}
pub fn fune_vector(args: &[Value]) -> Value {
let add_ons: Vec<InvoiceLine> = args[1].as_arr().iter().map(invoice_line_from_value).collect();
let usage: Vec<InvoiceLine> = args[2].as_arr().iter().map(invoice_line_from_value).collect();
let discounts: Vec<Discount> = args[3].as_arr().iter().map(discount_from_value).collect();
invoice_to_value(&renewal_invoice(
&invoice_line_from_value(&args[0]),
&add_ons,
&usage,
&discounts,
args[4].as_i64(),
args[5].as_str(),
args[6].as_str(),
))
}