Functional Weave
Code in Rust

retail.loyalty-points@1.0.0

impl/python.py

3,141 bytes · the Python 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.

from typing import Optional

from .math_round_div import round_div  ← from math.round-div ^1.0.0 · built alongside by fune
from .money_add import subtract_money  ← from money.add ^1.0.0 · built alongside by fune
from .money_amount import Money, assert_same_currency, money  ← from money.amount ^1.0.0 · built alongside by fune
from .money_compare import compare_money  ← from money.compare ^1.0.0 · built alongside by fune
from .retail_loyalty_points_types import LoyaltyScheme, LoyaltyOutcome


def _whole(value: object) -> bool:
    return isinstance(value, int) and not isinstance(value, bool)


def loyalty_points(
    scheme: LoyaltyScheme,
    spend: Money,
    qualifying_spend: Money,
    balance: int,
    redeem_points: int,
) -> LoyaltyOutcome:
    """Redeem points against a purchase, then earn on the amount actually paid,
    at the member's tier multiplier.
    """
    for other in [scheme.earn_per, scheme.block_value, qualifying_spend] + [t.threshold for t in scheme.tiers]:
        assert_same_currency(spend, other)
    if spend.minor < 0:
        raise ValueError("spend must not be negative, received %d" % spend.minor)
    if scheme.earn_per.minor <= 0:
        raise ValueError("earnPer must be more than 0, received %d" % scheme.earn_per.minor)
    if scheme.earn_points < 0:
        raise ValueError("earnPoints must not be negative, received %d" % scheme.earn_points)
    if scheme.redeem_block < 1:
        raise ValueError("redeemBlock must be 1 or more, received %d" % scheme.redeem_block)
    if not _whole(balance) or balance < 0:
        raise ValueError("balance must not be negative, received %r" % (balance,))
    if not _whole(redeem_points) or redeem_points < 0:
        raise ValueError("redeemPoints must not be negative, received %r" % (redeem_points,))
    if redeem_points > balance:
        raise ValueError("cannot redeem %d points from a balance of %d" % (redeem_points, balance))
    if redeem_points % scheme.redeem_block != 0:
        raise ValueError("points are redeemed in blocks of %d, received %d" % (scheme.redeem_block, redeem_points))

    discount = money(redeem_points // scheme.redeem_block * scheme.block_value.minor, spend.currency)
    if compare_money(discount, spend) > 0:
        raise ValueError(
            "redeeming %d points is worth %d, more than the spend of %d" % (redeem_points, discount.minor, spend.minor)
        )
    amount_payable = subtract_money(spend, discount)

    tier: Optional[str] = None
    multiplier_basis_points = 10000
    best: Optional[Money] = None
    for t in scheme.tiers:
        if compare_money(t.threshold, qualifying_spend) > 0:
            continue
        if best is None or compare_money(t.threshold, best) > 0:
            best = t.threshold
            tier = t.name
            multiplier_basis_points = t.multiplier_basis_points

    base = round_div(amount_payable.minor * scheme.earn_points, scheme.earn_per.minor, scheme.earn_rounding)
    earned_points = round_div(base * multiplier_basis_points, 10000, scheme.earn_rounding)

    return LoyaltyOutcome(
        tier=tier,
        multiplier_basis_points=multiplier_basis_points,
        redeemed_points=redeem_points,
        discount=discount,
        amount_payable=amount_payable,
        earned_points=earned_points,
        new_balance=balance - redeem_points + earned_points,
    )