use super::funejson::Value; use super::math_round_div::round_div; use super::money_add::subtract_money; use super::money_amount::{assert_same_currency, money, money_from_value, money_to_value, Money}; use super::money_compare::compare_money; /// Redeem points against a purchase, then earn on the amount actually paid, /// at the member's tier multiplier. /// /// # Panics /// Panics on mixed currencies, negative amounts, or a redemption that is not /// whole blocks, exceeds the balance or exceeds the spend. pub fn loyalty_points( scheme: &LoyaltyScheme, spend: &Money, qualifying_spend: &Money, balance: i64, redeem_points: i64, ) -> LoyaltyOutcome { assert_same_currency(spend, &scheme.earn_per); assert_same_currency(spend, &scheme.block_value); assert_same_currency(spend, qualifying_spend); for t in &scheme.tiers { assert_same_currency(spend, &t.threshold); } if spend.minor < 0 { panic!("spend must not be negative, received {}", spend.minor); } if scheme.earn_per.minor <= 0 { panic!("earnPer must be more than 0, received {}", scheme.earn_per.minor); } if scheme.earn_points < 0 { panic!("earnPoints must not be negative, received {}", scheme.earn_points); } if scheme.redeem_block < 1 { panic!("redeemBlock must be 1 or more, received {}", scheme.redeem_block); } if balance < 0 { panic!("balance must not be negative, received {}", balance); } if redeem_points < 0 { panic!("redeemPoints must not be negative, received {}", redeem_points); } if redeem_points > balance { panic!("cannot redeem {} points from a balance of {}", redeem_points, balance); } if redeem_points % scheme.redeem_block != 0 { panic!( "points are redeemed in blocks of {}, received {}", scheme.redeem_block, redeem_points ); } let discount = money( redeem_points / scheme.redeem_block * scheme.block_value.minor, &spend.currency, ); if compare_money(&discount, spend) > 0 { panic!( "redeeming {} points is worth {}, more than the spend of {}", redeem_points, discount.minor, spend.minor ); } let amount_payable = subtract_money(spend, &discount); let mut tier: Option = None; let mut multiplier_basis_points = 10000; let mut best: Option = None; for t in &scheme.tiers { if compare_money(&t.threshold, qualifying_spend) > 0 { continue; } let better = match &best { None => true, Some(b) => compare_money(&t.threshold, b) > 0, }; if better { best = Some(t.threshold.clone()); tier = Some(t.name.clone()); multiplier_basis_points = t.multiplier_basis_points; } } let base = round_div( amount_payable.minor * scheme.earn_points, scheme.earn_per.minor, &scheme.earn_rounding, ); let earned_points = round_div(base * multiplier_basis_points, 10000, &scheme.earn_rounding); LoyaltyOutcome { tier, multiplier_basis_points, redeemed_points: redeem_points, discount, amount_payable, earned_points, new_balance: balance - redeem_points + earned_points, } } pub fn loyalty_scheme_from_value(v: &Value) -> LoyaltyScheme { LoyaltyScheme { earn_points: v.get("earnPoints").as_i64(), earn_per: money_from_value(v.get("earnPer")), earn_rounding: v.get("earnRounding").as_str().to_string(), redeem_block: v.get("redeemBlock").as_i64(), block_value: money_from_value(v.get("blockValue")), tiers: v .get("tiers") .as_arr() .iter() .map(|t| LoyaltyTier { name: t.get("name").as_str().to_string(), threshold: money_from_value(t.get("threshold")), multiplier_basis_points: t.get("multiplierBasisPoints").as_i64(), }) .collect(), } } pub fn loyalty_outcome_to_value(o: &LoyaltyOutcome) -> Value { Value::obj(vec![ ( "tier", match &o.tier { Some(name) => Value::str(name), None => Value::Null, }, ), ("multiplierBasisPoints", Value::Int(o.multiplier_basis_points)), ("redeemedPoints", Value::Int(o.redeemed_points)), ("discount", money_to_value(&o.discount)), ("amountPayable", money_to_value(&o.amount_payable)), ("earnedPoints", Value::Int(o.earned_points)), ("newBalance", Value::Int(o.new_balance)), ]) } pub fn fune_vector(args: &[Value]) -> Value { loyalty_outcome_to_value(&loyalty_points( &loyalty_scheme_from_value(&args[0]), &money_from_value(&args[1]), &money_from_value(&args[2]), args[3].as_i64(), args[4].as_i64(), )) }