use super::funejson::Value; use super::money_amount::{money_from_value, Money}; use super::money_compare::compare_money; use super::money_sum::sum_money; 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 = lines .iter() .enumerate() .filter(|(_, line)| is_eligible(coupon, line)) .map(|(i, _)| i as i64) .collect(); let mut reasons: Vec = 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 = 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 { v.as_arr().iter().map(|s| s.as_str().to_string()).collect() } fn opt_str(v: &Value) -> Option { if v.is_null() { None } else { Some(v.as_str().to_string()) } } fn opt_int(v: &Value) -> Option { 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 = 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(), )) }