Functional Weave
Code in Rust

retail.loyalty-points@1.0.0

README.md

1,902 bytes · view raw

# retail.loyalty-points

One purchase through a points scheme: the member spends some points against
the bill, pays the rest, and earns points on what they paid, at their tier's
rate.

## Order of operations

1. **Redeem.** `redeemPoints` must be a multiple of `redeemBlock` (schemes
   such as "150 points = £1.50" redeem in blocks) and no more than the
   balance. The discount is `redeemPoints / redeemBlock x blockValue`, and it
   may not exceed the spend.
2. **Pay.** `amountPayable = spend - discount`.
3. **Earn on what was paid.** Points are not earned on the part of the bill
   paid with points, which is how most UK schemes work and stops points
   earning points.
   - base = `amountPayable x earnPoints / earnPer`, rounded with
     `earnRounding` (`down` gives "1 point per whole pound").
   - earned = `base x multiplierBasisPoints / 10000`, rounded the same way.
4. **Balance.** `balance - redeemedPoints + earnedPoints`.

Rounding twice is deliberate. A "double points for Gold" scheme doubles the
points you would have earned, so £9.99 at 1 point per whole pound is 9 points,
doubled to 18. Rounding once (9.99 x 2 = 19.98, so 19) gives the customer a
point the scheme's own terms do not.

## Tiers

The tier is the one with the highest `threshold` at or below
`qualifyingSpend` (the first listed wins a tie). With no tier reached, `tier`
is null and the multiplier is 1x. What counts as qualifying spend (this year,
the last 12 months, including this purchase or not) is the scheme's rule, so
the caller passes it in.

## Errors

Redeeming more points than the balance, a number that is not a whole number of
blocks, or more value than the spend is an error, not a silent cap: a till
that quietly redeems fewer points than the customer asked for is a complaint.
All amounts must be in one currency. Refunds are not handled here; reverse
the original transaction's points instead.