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::money_amount::{money_from_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::money_sum::sum_money; ← from money.sum ^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 is_eligible(coupon: &Coupon, line: &CouponLine) -> bool {
let open = coupon.skus.is_empty() && coupon.categories.is_empty();
let listed = open || coupon.skus.contains(&line.sku) || coupon.categories.contains(&line.category);
let excluded = coupon.excluded_skus.contains(&line.sku)
|| coupon.excluded_categories.contains(&line.category);
listed && !excluded
}
/// Whether a coupon can be used on a basket, with every reason it cannot.
/// ISO dates compare correctly as strings.
///
/// # Panics
/// Panics on a malformed date, negative customer uses, or mixed currencies.
pub fn validate_coupon(coupon: &Coupon, lines: &[CouponLine], customer_uses: i64, on_date: &str) -> CouponCheck {
if !is_iso_date(on_date) {
panic!("onDate must be an ISO date (YYYY-MM-DD), received \"{}\"", on_date);
}
if customer_uses < 0 {
panic!("customerUses must not be negative, received {}", customer_uses);
}
let eligible_lines: Vec<i64> = lines
.iter()
.enumerate()
.filter(|(_, line)| is_eligible(coupon, line))
.map(|(i, _)| i as i64)
.collect();
let mut reasons: Vec<String> = Vec::new();
if let Some(from) = &coupon.valid_from {
if on_date < from.as_str() {
reasons.push("not-yet-valid".to_string());
}
}
if let Some(to) = &coupon.valid_to {
if on_date > to.as_str() {
reasons.push("expired".to_string());
}
}
if let Some(max) = coupon.max_uses {
if coupon.times_used >= max {
reasons.push("usage-limit-reached".to_string());
}
}
if let Some(max) = coupon.max_uses_per_customer {
if customer_uses >= max {
reasons.push("customer-limit-reached".to_string());
}
}
if eligible_lines.is_empty() {
reasons.push("no-eligible-items".to_string());
}
if let Some(minimum) = &coupon.minimum_spend {
let counted: Vec<Money> = if coupon.minimum_spend_on == "eligible" {
eligible_lines.iter().map(|i| lines[*i as usize].total.clone()).collect()
} else {
lines.iter().map(|l| l.total.clone()).collect()
};
let spend = sum_money(&counted, &minimum.currency);
if compare_money(&spend, minimum) < 0 {
reasons.push("minimum-spend-not-met".to_string());
}
}
CouponCheck {
valid: reasons.is_empty(),
reasons,
eligible_lines,
}
}
fn strings(v: &Value) -> Vec<String> {
v.as_arr().iter().map(|s| s.as_str().to_string()).collect()
}
fn opt_str(v: &Value) -> Option<String> {
if v.is_null() {
None
} else {
Some(v.as_str().to_string())
}
}
fn opt_int(v: &Value) -> Option<i64> {
if v.is_null() {
None
} else {
Some(v.as_i64())
}
}
pub fn coupon_from_value(v: &Value) -> Coupon {
Coupon {
code: v.get("code").as_str().to_string(),
valid_from: opt_str(v.get("validFrom")),
valid_to: opt_str(v.get("validTo")),
minimum_spend: if v.get("minimumSpend").is_null() {
None
} else {
Some(money_from_value(v.get("minimumSpend")))
},
minimum_spend_on: v.get("minimumSpendOn").as_str().to_string(),
max_uses: opt_int(v.get("maxUses")),
times_used: v.get("timesUsed").as_i64(),
max_uses_per_customer: opt_int(v.get("maxUsesPerCustomer")),
skus: strings(v.get("skus")),
categories: strings(v.get("categories")),
excluded_skus: strings(v.get("excludedSkus")),
excluded_categories: strings(v.get("excludedCategories")),
}
}
pub fn coupon_line_from_value(v: &Value) -> CouponLine {
CouponLine {
sku: v.get("sku").as_str().to_string(),
category: v.get("category").as_str().to_string(),
total: money_from_value(v.get("total")),
}
}
pub fn coupon_check_to_value(c: &CouponCheck) -> Value {
Value::obj(vec![
("valid", Value::Bool(c.valid)),
("reasons", Value::Arr(c.reasons.iter().map(|r| Value::str(r)).collect())),
("eligibleLines", Value::Arr(c.eligible_lines.iter().map(|i| Value::Int(*i)).collect())),
])
}
pub fn fune_vector(args: &[Value]) -> Value {
let lines: Vec<CouponLine> = args[1].as_arr().iter().map(coupon_line_from_value).collect();
coupon_check_to_value(&validate_coupon(
&coupon_from_value(&args[0]),
&lines,
args[2].as_i64(),
args[3].as_str(),
))
}