@zenuinebit/loyalty-points
v0.1.0
Published
Loyalty points: earn rules, tiers, redemption limits and an expiring-points ledger.
Readme
@zenuinebit/loyalty-points
Loyalty programme maths: earning, tiers, redemption limits, and a points ledger where points expire. Money is in minor units (see @zenuinebit/money); dates are "YYYY-MM-DD" strings. No dependencies.
import { earn, tierFor, maxRedeemable, pointsValue, earnedEntry, balance, expiringSoon } from "@zenuinebit/loyalty-points";
// 1 point per 100.00 spent, with a Gold bonus of 1.5x
earn(250_000, { points: 1, per: 10_000, multiplier: 1.5 }); // 37
const tiers = [{ name: "Silver", min: 0 }, { name: "Gold", min: 1000, multiplier: 1.5 }];
tierFor(1200, tiers)?.name; // "Gold"
// How many points can pay for this bill? (1 point = 1.00, at most half the bill, in steps of 10)
maxRedeemable(480, 20_000, { minorPerPoint: 100, maxPercentOfBill: 50, step: 10 }); // 100
// A ledger: earned points expire after 90 days; redeemed points are negative entries
const entries = [
earnedEntry("2025-01-01", 100, 90),
earnedEntry("2025-02-01", 200, 365),
{ date: "2025-03-01", points: -150 },
];
balance(entries, "2025-03-15"); // 150
expiringSoon(entries, "2025-03-15", 30); // points expiring in the next 30 daysHow it works
earnrounds down and is safe against floating-point noise (3.3000000000000003 is 3, not 4).expiresOnis the first day the points are no longer valid.- Redemptions use up the soonest-expiring points first, so customers lose as little as possible. A redemption bigger than the points valid that day throws, which usually means a bug or a double spend in your data.
- The ledger is derived from entries each time. Store the entries, not the balance.
Not included: storage, fraud checks, refunds (add a negative entry for earned points taken back), and a points-to-currency tax treatment.
