Settings reference

Reward Points

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

KeyTypeDefaultRequiredPurpose
points_per_unitint (≥0)1yesPoints earned per $1.00 of effective amount (subtotal − discount). 0 disables earning, redemption still works.
redemption_rateint (≥0)1yesMinor units (cents) each point is worth at checkout. 0 disables redemption, earning still works.
min_redeem_pointsint (≥0)100yesFloor on the customer's balance before they can redeem any points.
max_redeem_percentageint (0–100)30yesCap on what fraction of an order total can be paid with points. Validated server-side at checkout.
expiry_daysint (≥0)365yesDays an earned/bonus point stays live before ExpireOldPoints writes it off. 0 = no expiry.
signup_bonusint (≥0)0yesPoints for creating an account, once per e-mail/phone. 0 = off.
review_bonusint (≥0)0yesPoints for a published review of a product from a completed order. 0 = off.
review_monthly_capint (≥0)0yesMost review bonuses one customer can earn per month. 0 = no limit.
referral_bonusint (≥0)0yesPoints for the customer who invited, when the friend's first order completes.
referee_bonusint (≥0)0yesPoints for the invited friend on their first completed order. Referrals are on when either is above 0.
referral_monthly_capint (≥0)0yesMost referral bonuses one customer can earn per month. 0 = no limit.
birthday_bonusint (≥0)0yesPoints on the customer's birthday (Birth Date profile field), once a year. 0 = off.

Validation enforced by UpdateConfig:

  • max_redeem_percentage must be 0 ≤ x ≤ 100 — anything else returns 400 must be between 0 and 100.
  • redemption_rate must be ≥ 0.
  • points_per_unit must be ≥ 0.
  • expiry_days is not range-checked on input — pass 0 to 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

KeyWhere setEffect when ONEffect when OFF
reward-points (master switch)Modules pagePage renders, routes mount, listener reacts, every Earn/Redeem/Bonus/Adjust call goes throughPage 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:

  1. Router middleware (RequireModule on the /rewards chi route) — denies admin HTTP traffic.
  2. Service-layer guard (Service.moduleEnabled(ctx)) — protects the synchronous code paths called from the orders flow.
  3. Listener guard (OrderListener.onOrderStatusChanged first 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:

PrivilegeDefault?Grants
view_rewardsnoRead access to admin Rewards page (Settings / User Points tabs)
manage_rewardsnoAll 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% = 30 program reads as "1% back, capped at 30% of an order". points_per_unit = 10 is 10% back — a strong promotion, costly as a permanent rate.
  • Keep min_redeem_points round. 100 is good. Don't pick odd minima (137) — customers will reverse-engineer the math and find the awkward round-down points.
  • Pick expiry_days based 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_unit retroactively expecting old earns to recompute. Each earn row 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_rates table, PointRate API) 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.* in frontend/src/lib/i18n/*.ts.
  • Email notifications on earn / redeem / expire — not in this module. Use the email-notifications module's templating to add custom notifications keyed off the same events.