Skip to content

Developer notes

Table Contents
?:kv_lt_tiers tier_id, usergroup_id (unique), position, status, min_spend, min_orders, qualify_mode (all/any), points_multiplier, cashback_percent, birthday_type (none/points/wallet/coupon), birthday_value, birthday_coupon, birthday_expiry_days, early_access, color
?:kv_lt_tier_descriptions tier_id, lang_code, perks (customer-facing text). The tier name is the user group name (?:usergroup_descriptions).
?:kv_lt_members user_id, tier_id, qualified_tier_id, spend, orders (window), lifetime_spend, lifetime_orders, since, grace_until, grace_tier_id, override_tier_id, override_until, override_note, last_evaluated
?:kv_lt_history upgrade, downgrade, grace_start, grace_end, override, override_end, removed, tier_deleted events with the stats, admin and note
?:kv_lt_rewards birthday rewards (type B, once per customer and year) and extra points of multipliers (type P, per order)

Membership itself is core data: ?:usergroup_links (status A for the current tier’s group, D for the other tier groups), written through fn_change_usergroup_status(), so change_usergroup_status_post fires for other add-ons. fn_init_user() recomputes auth.usergroup_ids on every request, so a tier change applies at once.

Stats come straight from ?:orders: leaf orders (is_parent_order != 'Y') of the customer in the qualifying statuses, in the window (timestamp >= start); spend = SUM(subtotal - subtotal_discount) or SUM(total), orders = COUNT(DISTINCT IF(parent_order_id > 0, parent_order_id, order_id)). No bookkeeping of refunds or deletions is needed.

Evaluator::evaluateMany() loads the stats of a batch with one query, then for each customer: manual tier (until its end date) → otherwise the highest active tier whose non-zero thresholds are met (all/any) → upgrade at once, downgrade after the grace period (allow_downgrade off keeps the highest tier) → history, user group links, e-mail.

Triggers: change_order_status_post and delete_order queue the customer and a shutdown function evaluates the queue once per request; update_profile (new customers, entry tier); the scheduler; “Recalculate all customers”; the member page.

change_order_status_post, delete_order, calculate_cart_taxes_pre (priority 310, after Reward points at 300: multiplier), place_order (extra points record), kv_customer_wallet_cashback_amount (Store Credit Wallet: tier cashback), user_init (badge data in $user_info.kv_loyalty_tier), update_profile, update_user_pre (birthday date), post_delete_user, delete_usergroups, gather_additional_product_data_post ($product.kv_lt_early_access), before_dispatch (scheduler fallback). Signatures are identical in 4.17.2 and 4.21.2.

Template hooks: profiles:my_account_menu (badge), products:promo_text (early access label), profiles:account_info and admin profiles:detailed_content (birthday field), admin profiles:update_tools_list (member page link).

// Current tier of a customer (null when none or the add-on is not active)
$tier = fn_kv_loyalty_tiers_get_customer_tier($user_id);
// ['tier_id' => 3, 'name' => 'Gold', 'usergroup_id' => 40, 'rank' => 3, 'points_multiplier' => 2.0, 'cashback_percent' => 3.0]
// Re-evaluate a customer now (false when switched off or unlicensed)
$result = fn_kv_loyalty_tiers_evaluate($user_id);
// Recalculate a segment of customers within a time budget (cursor-based)
$summary = \Tygh\Addons\KvLoyaltyTiers\Evaluator::recalculateAll(0, false, 20, 0, [$user_id_1, $user_id_2]);

Rewards build on: fn_change_user_points() (Reward points) and fn_kv_customer_wallet_credit() (Store Credit Wallet).

Privilege View loyalty tiers. Write methods answer 405.

GET /api/kv_loyalty_tiers tier ladder with thresholds, benefits and member counts
GET /api/kv_loyalty_tiers/<tier_id> one tier
GET /api/kv_loyalty_members?tier_id=&state=grace|override&q=&page=&items_per_page=
GET /api/kv_loyalty_members/<user_id> tier, stats, next tier, progress, history

Nothing is printed for guests: the badge needs a signed-in customer (user_init, my_account block, which is not cached and is rendered per visitor under the 4.21 full page cache), the My tier page is login-only and sends Cache-Control: no-store, and the early access label depends on the product only (products limited to a tier are not visible to guests). Run marks go to var/kv_storage while the full page cache is on.

Reward points: the multiplier changes only the earned reward of the cart (points_info.reward and the line rewards), never points in use. Store Credit Wallet: the tier cashback is added to the wallet’s cashback total (capped at the cashback base). Other add-ons that assign user groups (registration, B2B) are not affected: only tier groups are managed. Age verification: its birthday field is used when it is active.

app/addons/kv_loyalty_tiers/
addon.xml, init.php, func.php
controllers/backend/kv_loyalty_tiers.php tiers, tier editor, members, member page, history, manual tiers,
recalculation, CSV export/import, scheduler run, console cron
controllers/backend/profiles.post.php birthday field (admin)
controllers/backend/kv_loyalty_tiers_license.php
controllers/frontend/kv_loyalty_tiers.php My tier page, cron URL
controllers/frontend/profiles.post.php birthday field (storefront)
schemas/menu, permissions, settings
Tygh/Addons/KvLoyaltyTiers/ Config, Tiers, Stats, Members, Evaluator, Rewards, Notifier,
Scheduler, Account, Install, StorageData, License/, Upgrade/Synchronizer
Tygh/Api/Entities/KvLoyaltyTiers.php, KvLoyaltyMembers.php
design/backend/templates/addons/kv_loyalty_tiers/ views (manage, update, members, member, history), components,
hooks (profiles, index), settings/cron_info.tpl, license/
design/backend/css/addons/kv_loyalty_tiers/ styles.less, license.less
var/themes_repository/responsive/templates/addons/kv_loyalty_tiers/ views/kv_loyalty_tiers/view.tpl, components,
hooks (profiles, products, index)
var/themes_repository/responsive/css/addons/kv_loyalty_tiers/styles.less
var/langs/en/addons/kv_loyalty_tiers.po

The add-on ships the generated License\Client. Gated (switched off without a valid license): automatic and admin re-evaluation (order trigger, schedule, recalculation, “Re-evaluate now”), new tier rewards (multiplier, cashback, birthday rewards) and the storefront output (My tier page, badge, early access label, birthday field). Not gated: the tier editor, members, history, manual tiers (an explicit admin action), CSV and the REST API. Customers keep their tier user groups, so prices and promotions tied to them keep working.