Functional Weave
Code in Rust

retail.loyalty-points@1.0.0

impl/rust.rs

5,007 bytes · the Rust implementation · view raw

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_add::subtract_money;  ← from money.add ^1.0.0 · built alongside by fune
use super::money_amount::{assert_same_currency, 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

/// 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<String> = None;
    let mut multiplier_basis_points = 10000;
    let mut best: Option<Money> = 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(),
    ))
}