Loyalty Points & Rewards

Earning at the till, redeeming for a discount, and keeping the ledger honest

What v2.1 added

v1 customers could earn points and have them manually adjusted. v2.1 completes the program: customers now redeem points for a discount during checkout, earning respects each customer's tier multiplier and a configurable rounding mode, refunds prorate points instead of all-or-nothing, and points can expire. Every rule is set per tenant and recorded onto each transaction so history never re-interprets itself.

The Program Settings

One LoyaltyProgram per tenant holds the rules. ADMINs edit them via PATCH /api/v1/customers/loyalty/program (permission loyalty:manage); the change is audited as LOYALTY_PROGRAM_UPDATED.

SettingMeaningDefault
isActiveWhether the program runs at allfalse
earnRatePoints earned per unit of spend1
redeemRateCurrency value of one point when redeemed0.5
minRedeemPointsSmallest redeemable balance100
redemptionEnabledAllow redeeming points at checkoutfalse
maxRedeemPointsHard cap on points per redemption— (none)
maxRedeemPercentCap redemption to a share of the bill— (none)
eligibleStatusesWhich sale statuses earn points[COMPLETED]
includeTaxInEarningCount tax in the earning basetrue
earnOnRedeemedAmountEarn on the part paid with points toofalse
roundingModeFLOOR · ROUND · CEILFLOOR
pointsExpiryDaysDays until earned points expire— (never)
isPausedTemporarily stop earning & redeemingfalse
The defaults reproduce v1 behavior

Out of the box, only COMPLETED sales earn, redemption is off, and points never expire — exactly how loyalty behaved before v2.1. You opt into each new capability by changing the relevant setting.

Earning Points

When an eligible sale completes, POSVelo computes the points earned from the spend, the customer's tier multiplier (resolved from their prior committed spend), the tax / redeemed-amount toggles, and the rounding mode. Each earning is written as an EARNED transaction.

Every transaction snapshots the rules that made it

Each EARNED / REDEEMED row stores a ruleSnapshot of the settings in force at the time, plus the resulting balanceAfter. So if you later change the earn rate or rounding, historical transactions are never recomputed — and a refund knows exactly how many points the original sale granted.

Redeeming at Checkout

With redemptionEnabled, a cashier can apply a customer's points as a discount during checkout by passing pointsToRedeem on the checkout request.

http
json
The server computes the discount — never the client

POSVelo recomputes the discount from redeemRate server-side and validates the customer's balance and the min / max / percent caps. A client-supplied discount amount is never trusted. The points are deducted inside the same atomic checkout transaction (a conditional decrement, the same guard used for stock), so a checkout that fails never loses points — there is no separate hold to leak, because in-store checkout is synchronous and single-transaction.

The sale records what happened in three fields: pointsEarned, pointsRedeemed, and pointsDiscount.

Refunds Prorate Points

Returns and voids no longer treat points as all-or-nothing. A partial return adjusts points in proportion to the value of the items actually returned.

ActionEffect on points
Partial returnProrates by returned value: reverses earned points (REVERSED) and restores redeemed points (RESTORED) for the returned portion only
Repeated partial returnsCumulative reversal / restoration is capped so you can never reverse more than was originally earned or restore more than was spent
Balance already spentThe balance floors at zero rather than going negative
VoidReverses 100% — closing a v1 gap where a void left points untouched

Manual Adjustments

Staff with customer:write can credit or debit a customer's points directly — for a goodwill gesture or a correction.

http
Adjustments are safe and accountable

An adjustment applies as an atomic increment / decrement (not a racy absolute-balance write), records who performed it and the resulting balance, and writes a LOYALTY_POINTS_ADJUSTED audit row. A notes reason is required — every manual change to a balance has a stated reason.

Expiry

If pointsExpiryDays is set, each EARNED transaction carries an expiresAt. A daily background job (01:00 UTC) sweeps expired points and offsets them with an EXPIRED transaction per tenant, audit-logged. Leave pointsExpiryDays unset and points never expire.

Transaction Types

TypeWhen it's written
EARNEDAn eligible sale grants points
REDEEMEDPoints applied as a discount at checkout
RESTOREDRedeemed points returned by a refund/void
REVERSEDEarned points clawed back by a refund/void
ADJUSTEDA manual credit / debit by staff
EXPIREDThe expiry sweep retires aged points

In the App

TaskWhere
See a customer's point history/customers/[id] → Loyalty tab
Adjust a customer's pointsCustomer detail → adjust points
Configure the programLoyalty program settings (loyalty:manage)
Redeem at the tillPOS checkout, when redemption is enabled
Related

Loyalty rides on the customer record and the sale lifecycle — see the customer, sales, and POS modules in the API Reference for the surrounding endpoints.