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::math_round_div::round_div; ← from math.round-div ^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
use super::money_compare::compare_money; ← from money.compare ^1.0.0 · built alongside by fune
use super::retail_volumetric_weight::chargeable_weight; ← from retail.volumetric-weight ^1.0.0 · built alongside by fune
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())
}
fn chargeable_in(band: &ShippingBand, parcel: &Parcel) -> i64 {
match band.volumetric_divisor {
None => round_div(parcel.actual_grams, band.round_up_to_grams, "up") * band.round_up_to_grams,
Some(divisor) => {
chargeable_weight(
parcel.length_mm,
parcel.width_mm,
parcel.height_mm,
parcel.actual_grams,
divisor,
band.round_up_to_grams,
)
.chargeable_grams
}
}
}
/// The delivery charge for a parcel: the smallest band on the rate card in
/// force on the order date that takes it, free when the basket reaches the
/// band's threshold.
///
/// # Panics
/// Panics on a malformed date or parcel, mixed currencies, or when no band
/// takes the parcel.
pub fn shipping_rate(
bands: &[ShippingBand],
service: &str,
zone: &str,
parcel: &Parcel,
basket_total: &Money,
on_date: &str,
) -> ShippingQuote {
if !is_iso_date(on_date) {
panic!("onDate must be an ISO date (YYYY-MM-DD), received \"{}\"", on_date);
}
for d in [parcel.length_mm, parcel.width_mm, parcel.height_mm] {
if d < 1 {
panic!("dimensions must be 1 mm or more, received {}", d);
}
}
if parcel.actual_grams < 0 {
panic!("actualGrams must not be negative, received {}", parcel.actual_grams);
}
let longest = parcel.length_mm.max(parcel.width_mm).max(parcel.height_mm);
let mut best: Option<&ShippingBand> = None;
let mut best_grams = 0;
for band in bands {
if band.service != service || band.zone != zone {
continue;
}
if on_date < band.valid_from.as_str() {
continue;
}
if let Some(to) = &band.valid_to {
if on_date > to.as_str() {
continue;
}
}
if let Some(max_length) = band.max_length_mm {
if longest > max_length {
continue;
}
}
let grams = chargeable_in(band, parcel);
if grams > band.max_grams {
continue;
}
let better = match best {
None => true,
Some(b) => {
band.max_grams < b.max_grams
|| (band.max_grams == b.max_grams && compare_money(&band.price, &b.price) < 0)
}
};
if better {
best = Some(band);
best_grams = grams;
}
}
let best = match best {
Some(b) => b,
None => panic!(
"no shipping band for service \"{}\" to zone \"{}\" on {} fits this parcel",
service, zone, on_date
),
};
let free = match &best.free_over {
Some(threshold) => compare_money(basket_total, threshold) >= 0,
None => false,
};
ShippingQuote {
service: service.to_string(),
zone: zone.to_string(),
chargeable_grams: best_grams,
band_max_grams: best.max_grams,
standard_price: best.price.clone(),
price: if free { money(0, &best.price.currency) } else { best.price.clone() },
free,
}
}
fn opt_int(v: &Value) -> Option<i64> {
if v.is_null() {
None
} else {
Some(v.as_i64())
}
}
pub fn shipping_band_from_value(v: &Value) -> ShippingBand {
ShippingBand {
service: v.get("service").as_str().to_string(),
zone: v.get("zone").as_str().to_string(),
max_grams: v.get("maxGrams").as_i64(),
max_length_mm: opt_int(v.get("maxLengthMm")),
volumetric_divisor: opt_int(v.get("volumetricDivisor")),
round_up_to_grams: v.get("roundUpToGrams").as_i64(),
price: money_from_value(v.get("price")),
free_over: if v.get("freeOver").is_null() {
None
} else {
Some(money_from_value(v.get("freeOver")))
},
valid_from: v.get("validFrom").as_str().to_string(),
valid_to: if v.get("validTo").is_null() {
None
} else {
Some(v.get("validTo").as_str().to_string())
},
}
}
pub fn parcel_from_value(v: &Value) -> Parcel {
Parcel {
length_mm: v.get("lengthMm").as_i64(),
width_mm: v.get("widthMm").as_i64(),
height_mm: v.get("heightMm").as_i64(),
actual_grams: v.get("actualGrams").as_i64(),
}
}
pub fn shipping_quote_to_value(q: &ShippingQuote) -> Value {
Value::obj(vec![
("service", Value::str(&q.service)),
("zone", Value::str(&q.zone)),
("chargeableGrams", Value::Int(q.chargeable_grams)),
("bandMaxGrams", Value::Int(q.band_max_grams)),
("standardPrice", money_to_value(&q.standard_price)),
("price", money_to_value(&q.price)),
("free", Value::Bool(q.free)),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let bands: Vec<ShippingBand> = args[0].as_arr().iter().map(shipping_band_from_value).collect();
shipping_quote_to_value(&shipping_rate(
&bands,
args[1].as_str(),
args[2].as_str(),
&parcel_from_value(&args[3]),
&money_from_value(&args[4]),
args[5].as_str(),
))
}