Daily Loyalty Tier Recalculation
Overview
Loyalty programmes that enable membership tiers — Silver, Gold, Platinum — need those tiers re-evaluated periodically.
The reason this has to be a cron job rather than an event handler is that the measurement window is rolling. A customer can drop a tier without doing anything at all, simply because an old purchase has aged out of the last three months. No customer-side event exists that could trigger that recalculation, so something has to walk the data on a schedule.
The job is deliberately the only path that can demote a member. Promotions happen instantly at the point of sale, through the earn logic in client-api — nobody should be demoted mid transaction in front of a cashier and a queue of other customers.
Business Flow
The job runs at 03:00 every day (0 3 * * *) on the cron-scheduler profile. That time is
chosen because it is after midnight, so "the last three months" means whole days, and because it
is a quiet period — this job touches every account on the platform.
- Load every programme in
loyalty.programthat has tiers enabled and has not been superseded, along with itsmode,tier_window_months,tier_fallbackandtier_fallback_tier_id. - Process one programme at a time. A programme that fails is logged and skipped — a platform-wide nightly sweep must not stop because a single merchant has a problem.
- Load the tier ladder: rank, conditions, each tier's own window length, and which tier is the
default.
- If tiers are enabled but no tier has been defined yet, the job does nothing. Demoting everyone because setup is incomplete would be needless damage.
- Load every account in
loyalty.accountfor that OA together with its current tier. A LEFT JOIN is used so accounts with no activity in the window come back as zero rather than disappearing — those are precisely the accounts that should be demoted. - Compute statistics per tier window, since each tier may define its own window length. One query is issued per distinct window. The metrics measured are spend, order count, points and membership months.
- Evaluate each tier's conditions — a join operator of and/or plus a list of rules specifying a metric, an operator and a value — then pick the highest tier the account qualifies for.
- Compare that result against the current tier and the
tier_fallbackpolicy to decide whether the member stays, is promoted, or is demoted, and to which level. - Write the new tier into
loyalty.accountand record the tier change in history.
Key Files & Functions
| File | Responsibility |
|---|---|
internal/cronscheduler/loyalty_tier.go | LoyaltyTierService.Run is the cron entry point, supported by runProgram, loadLadder, statsForWindow, applyChange, qualifiesForTier, matchTierRule, tierStatFor, pickTierByRules, resolveFromEarned and parseTierConditions, plus the tierRow, tierStats, tierRule and tierConditions types |
internal/cronscheduler/loyalty_tier_test.go | Unit tests for the tier selection logic |
cmd/worker/main.go | runCronScheduler — registers the job on the 0 3 * * * schedule |
There is no queue involved; this is pure cron.
Connections to Other Services
- Tables in the
loyaltyschema —program(tier_enabled,tier_window_months,tier_fallback),tier(the ladder, with jsonb conditions andwindow_months),account(thetier_idfield), and the transaction tables used to compute spend, orders and points. - PostgreSQL only — no Redis, RabbitMQ or LINE API.
- client-api-go — promotes members immediately when points are earned, working in the opposite direction from this job.
- cms-api-go — owns the screens where the tier ladder and its conditions are configured.