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,
)