Troubleshooting + FAQ

Reward Points

Troubleshooting + FAQ

Customer says they completed an order but no points showed up.

Check the order's subtotal and discount_total. Points accrue on the effective amount (subtotal − discount_total); if a 100% coupon zeroed the order, no points are due. Then check the order's status — points fire on transitions INTO paid or completed. If the order skipped straight to completed without firing the event, the event bus subscription may have failed at startup. Backend logs → search reward order listener registered for order.statusChanged.

The Rewards page in admin returns "module disabled".

Toggle reward-points ON under Modules. The page short-circuits to null when useModuleEnabled resolves false — there is no error, just a blank page. All /admin/rewards/* routes also 403 with module_disabled while the master switch is off.

Customer redeemed at checkout, order completed, but their balance still shows the old number.

Force-reload /account/rewards. The page reads GET /storefront/rewards/balance once on mount; there's no live-update channel. If the data really is wrong, check the orders activity log for an error entry like rewards redemption commit failed — the order may have committed without the matching redeem row. Use Adjust to manually correct.

Insufficient-points error at checkout when the customer says they have enough.

The check uses available balance, which is the running sum across all transaction types. If the customer has earn=500 + adjust=−100 + bonus=200, available = 600 — even though total_earned reads 700. The error message reflects what the customer can actually redeem.

Order listener registers, but no points are credited on order paid.

Three usual suspects:

  1. The module is disabled for this tenant — listener short-circuits silently. Logs show reward-points module disabled for tenant, skipping rewards accrual.
  2. The event payload's OldStatus is already in the completed-set (e.g., paid → completed doesn't re-earn). This is by design — points fire on the FIRST entry into completed-or-paid.
  3. Idempotency hit — an earn row already exists for this order_id. Logs show points already earned for order, skipping.

Points were credited twice for one order.

This should be impossible thanks to the idempotency check (pointsAlreadyEarned). If it happens, the most likely cause is two distinct order_id rows referring to the same logical purchase (e.g., a duplicate-order bug elsewhere). Inspect reward_points for the duplicate earn rows — their order_ids will tell you the upstream issue.

Refund happened but points weren't revoked.

Check the order status transition. Revocation fires on transitions INTO cancelled or refunded. If your refund flow updates payment_status instead of status, the order listener won't see it. Either standardise on orders.status (full-word values per the codebase convention) or wire a separate listener.

Expiry never runs.

ExpireOldPoints is a service method — it is NOT auto-scheduled in this codebase. Wire it into your cron (one call per tenant per day at off-peak hours). Until then, no expire rows are written; points live forever in practice.

Expiry job ran, no rows written.

Check reward_config.expiry_days. If 0, the job returns immediately. Also verify there ARE earn/bonus rows older than the cutoff — a brand-new program won't have any.

Customer can't redeem despite balance ≥ minimum.

The max_redeem_percentage cap is calculated against the order total the storefront sends. A small order will have a small max-points-allowed even with a huge balance. Increase max_redeem_percentage if you want the cap less restrictive — or have the customer add more items.

Customer history shows a "−800 adjust" they didn't authorise.

That's a refund-or-cancel revocation. Cross-reference the order_id on that adjust row with the matching order. The description should read Order #N cancelled or Order #N refunded. Never delete those rows — they're the audit trail proving you didn't double-credit on a refund.

AdjustPoints 500s with "module disabled".

The handler returns 500 when the service returns (nil, nil). Re-enable the module before adjusting. (The service deliberately returns (nil, nil) rather than an error to keep the order flow simple — the handler's behaviour is a known sharp edge.)

Balance shows 0 / total_earned 800 / total_redeemed 800 / a −800 adjust.

Customer earned, spent, and got refunded. Available is correctly 0 — they can't spend what was clawed back. Their next earn rebuilds.

reward_point_rates doesn't exist in my DB.

Correct — the table is not yet provisioned by any migration. The corresponding handlers (AdminListPointRates, etc.) exist in code but are not mounted in the router. Per-product / per-category multipliers are roadmap, not live.

Storefront shows the wrong "earn this many points" preview at cart.

The preview reads GET /storefront/rewards/earnings?total=N where N is the cart subtotal in minor units. If the cart aggregator sends the wrong subtotal (with shipping + tax baked in), the preview is wrong. The actual earning at completion uses the real orders.subtotal − discount_total, which is correct — it's only the preview that's affected.

Customer asks "why didn't I earn 1 more point?"

Integer division truncates. $0.99 with points_per_unit = 10 earns 9 points, not 10. Document this in your customer-facing FAQ.

How to disable the program fast.

Toggle reward-points OFF under Modules. Earning halts, redemption halts, the listener no-ops, the admin page returns null, the storefront page disappears from nav. All ledger rows persist; flipping back ON resumes from where you left off.

How to wind the program down without the abrupt off-switch.

  1. Set redemption_rate = 0 — points become unspendable but still earn.
  2. Wait one expiry cycle — expiry_days worth of time.
  3. Set points_per_unit = 0 — earning halts; redemption is already off.
  4. Wait until Available drops to near-zero across the user base.
  5. Toggle the master switch off and announce.