Skip to main content

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.

  1. Load every programme in loyalty.program that has tiers enabled and has not been superseded, along with its mode, tier_window_months, tier_fallback and tier_fallback_tier_id.
  2. 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.
  3. 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.
  4. Load every account in loyalty.account for 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.
  5. 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.
  6. 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.
  7. Compare that result against the current tier and the tier_fallback policy to decide whether the member stays, is promoted, or is demoted, and to which level.
  8. Write the new tier into loyalty.account and record the tier change in history.

Key Files & Functions

FileResponsibility
internal/cronscheduler/loyalty_tier.goLoyaltyTierService.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.goUnit tests for the tier selection logic
cmd/worker/main.gorunCronScheduler — registers the job on the 0 3 * * * schedule

There is no queue involved; this is pure cron.

Connections to Other Services

  • Tables in the loyalty schemaprogram (tier_enabled, tier_window_months, tier_fallback), tier (the ladder, with jsonb conditions and window_months), account (the tier_id field), 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.