Settings reference
Every config knob in /admin/rewards → Settings, in plain language. The values live in the reward_config row with id=1 (one row per tenant — the table is per-tenant DB).
Earning + redemption
| Key | Type | Default | Required | Purpose |
|---|---|---|---|---|
points_per_unit | int (≥0) | 1 | yes | Points earned per $1.00 of effective amount (subtotal − discount). 0 disables earning, redemption still works. |
redemption_rate | int (≥0) | 1 | yes | Minor units (cents) each point is worth at checkout. 0 disables redemption, earning still works. |
min_redeem_points | int (≥0) | 100 | yes | Floor on the customer's balance before they can redeem any points. |
max_redeem_percentage | int (0–100) | 30 | yes | Cap on what fraction of an order total can be paid with points. Validated server-side at checkout. |
expiry_days | int (≥0) | 365 | yes | Days an earned/bonus point stays live before ExpireOldPoints writes it off. 0 = no expiry. |
signup_bonus | int (≥0) | 0 | yes | Points for creating an account, once per e-mail/phone. 0 = off. |
review_bonus | int (≥0) | 0 | yes | Points for a published review of a product from a completed order. 0 = off. |
review_monthly_cap | int (≥0) | 0 | yes | Most review bonuses one customer can earn per month. 0 = no limit. |
referral_bonus | int (≥0) | 0 | yes | Points for the customer who invited, when the friend's first order completes. |
referee_bonus | int (≥0) | 0 | yes | Points for the invited friend on their first completed order. Referrals are on when either is above 0. |
referral_monthly_cap | int (≥0) | 0 | yes | Most referral bonuses one customer can earn per month. 0 = no limit. |
birthday_bonus | int (≥0) | 0 | yes | Points on the customer's birthday (Birth Date profile field), once a year. 0 = off. |
Validation enforced by UpdateConfig:
max_redeem_percentagemust be0 ≤ x ≤ 100— anything else returns 400must be between 0 and 100.redemption_ratemust be≥ 0.points_per_unitmust be≥ 0.expiry_daysis not range-checked on input — pass0to disable, any positive int to age out at that day count.
Reward Rules (legacy, hidden)
The old reward_rules table and its API still exist for compatibility, but nothing reads them and the admin Rules tab was removed. Bonus points are configured with the *_bonus settings above.
Module-level
| Key | Where set | Effect when ON | Effect when OFF |
|---|---|---|---|
reward-points (master switch) | Modules page | Page renders, routes mount, listener reacts, every Earn/Redeem/Bonus/Adjust call goes through | Page returns null, all admin routes return module_disabled, listener returns immediately, service calls return (nil, nil) silently |
The module gate is checked in three places and that's by design:
- Router middleware (
RequireModuleon the/rewardschi route) — denies admin HTTP traffic. - Service-layer guard (
Service.moduleEnabled(ctx)) — protects the synchronous code paths called from the orders flow. - Listener guard (
OrderListener.onOrderStatusChangedfirst lines) — protects the async event-bus path.
All three close the loop independently — see feedback_module_gating_4_layers.md for why.
Privileges
Granted on Users → Usergroups → Privileges under section marketing, group rewards:
| Privilege | Default? | Grants |
|---|---|---|
view_rewards | no | Read access to admin Rewards page (Settings / User Points tabs) |
manage_rewards | no | All write actions (settings incl. bonuses, balance adjustments). Wave 11 lets manage_promotions substitute. |
The page wrapper today is RequirePrivilege privilege="manage_promotions" (umbrella marketing privilege). Either privilege unlocks the page; the route-level rules-write/adjust gates accept either one.
Recommendations
- Start modest, raise for promotions. The default
points_per_unit = 1,redemption_rate = 1,max% = 30program reads as "1% back, capped at 30% of an order".points_per_unit = 10is 10% back — a strong promotion, costly as a permanent rate. - Keep
min_redeem_pointsround. 100 is good. Don't pick odd minima (137) — customers will reverse-engineer the math and find the awkward round-down points. - Pick
expiry_daysbased on average repeat-purchase cadence. If your repeat customers buy quarterly, 365 days is fine. If they buy annually, 730 or 0 is friendlier. Aggressive expiry (90 days) feels punitive. - Disable redemption (
redemption_rate = 0) before disabling earning if you're winding the program down. Customers can't earn points they can't spend without realising — redemption-off is a clearer signal. - Don't change
points_per_unitretroactively expecting old earns to recompute. Eachearnrow is a flat int that was correct at the time of the order. Changing the rate only affects future earns.
Things NOT in this module's settings (and where they live instead)
- Per-product / per-category / per-vendor multipliers — code skeleton exists (
reward_point_ratestable,PointRateAPI) but is not yet shipped (no migration, no router mount). See Earning rules. - Tiered loyalty (Bronze/Silver/Gold) — not implemented. Build with rule rows that fire on cumulative-spend conditions when listeners land.
- Customer-facing FAQ / help text — i18n keys at
account.rewards.*infrontend/src/lib/i18n/*.ts. - Email notifications on earn / redeem / expire — not in this module. Use the
email-notificationsmodule's templating to add custom notifications keyed off the same events.
