VIP tier engine
Operator-defined tier ladder with auto-promotion based on lifetime activity. Default seed: Bronze → Silver → Gold → Platinum.
Tier shape
{
"code": "gold", // lowercase, alphanumeric_underscore
"name": "Gold",
"rank": 2, // ordering: 0 = baseline
"color": "#FFD700", // hex for badge
"glyph": "★★★", // unicode badge
"requirements": {
"lifetime_deposit_subunit": 500000, // €5000
"lifetime_wagered_subunit": 2500000, // €25000
"min_days_active": 14
},
"demotion": { "inactivity_days": 60 }, // null = sticky tier
"perks": {
"cashback_pct": 5,
"dedicated_host": true,
"priority_kyc": true
},
"is_active": true
}
Requirements + perks are operator-defined; UCRM doesn't enforce specific values, only the structural shape.
Auto-promotion
recomputeTierForPlayer(tenantId, playerId) evaluates eligibility:
- Loads active tiers ordered by rank DESC.
- For each tier, checks all requirement conditions (AND-ed):
total_deposited >=,total_wagered >=, days sincefirst_seen_at >=. - Highest matched rank wins.
All thresholds are currency-aware — lifetime_deposit_subunit is denominated in tenant base currency, and players.total_deposited is already maintained in base by the stream-processor. No FX math needed at evaluation.
Demotion
Per-tier demotion rules (demotion.inactivity_days). If the player's current tier has a demotion rule + they crossed the inactivity window, drop one tier (not all the way to baseline — concierge model).
reconcileTierDemotions runs daily as a cron-able script.
Manual override
POST /v1/players/:id/tier { to_code: "platinum", reason: "VIP host invitation" } writes a history entry with trigger=manual. Useful for concierge promotions where the operator wants someone in Platinum without meeting the auto thresholds.
The next auto-recompute respects the override: if the player STILL doesn't meet Platinum thresholds, the manual tier sticks. The auto path only changes tiers when the eligibility computation says so.
History
player_tier_history — append-only audit:
SELECT from_code, to_code, trigger, occurred_at
FROM player_tier_history
WHERE player_id = $1
ORDER BY occurred_at DESC
LIMIT 50;
trigger is auto (engine), manual (admin), or reset (one-time backfill).
Events
The engine doesn't emit events directly; tier changes show up in player_tier_history. Future enhancement: emit tier_promoted / tier_demoted canonical events for journey triggers.
Stream-processor wiring
Per-event tier recompute is deferred in MVP — we use a daily reconciliation cron + on-demand admin recompute via POST /v1/players/:id/tier. Operators who need real-time can flip on the per-event call by setting RECOMPUTE_TIER_ON_EVENT=1 in stream-processor env (adds ~5ms HTTP latency per money event).