Розробка системи управління програмою лояльності

Наша компанія займається розробкою, підтримкою та обслуговуванням сайтів будь-якої складності. Від простих односторінкових сайтів до масштабних кластерних систем, побудованих на мікро сервісах. Досвід розробників підтверджено сертифікатами від вендорів.
Розробка та обслуговування будь-яких видів сайтів:
Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Пропоновані послуги
Показано 1 з 1 послугУсі 2065 послуг
Розробка системи управління програмою лояльності
Середня
від 1 тижня до 3 місяців
Часті питання
Наші компетенції:
Етапи розробки
Останні роботи
  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    874
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851

Розробка системи управління програмою лояльності

Програма лояльності — це не тільки "бонусні бали". Це механізм, який змінює поведінку покупця: змушує повертатися, збільшувати середній чек, вибирати конкретний канал. Технічно це облікова система балів, рівнів і вознаграждень, вбудована у всі точки контакту з клієнтом.

Архітектура: счет, транзакції, рівні

-- Бонусний счет користувача
CREATE TABLE loyalty_accounts (
    id              BIGSERIAL PRIMARY KEY,
    user_id         BIGINT UNIQUE REFERENCES users(id),
    balance         DECIMAL(12,2) DEFAULT 0,     -- поточний баланс
    lifetime_earned DECIMAL(12,2) DEFAULT 0,     -- всього заробленo (для рівнів)
    tier_id         BIGINT REFERENCES loyalty_tiers(id),
    expires_at      DATE,                         -- дата сгорання балів
    updated_at      TIMESTAMPTZ DEFAULT NOW()
);

-- Всі рухи балів (append-only лог)
CREATE TABLE loyalty_transactions (
    id              BIGSERIAL PRIMARY KEY,
    account_id      BIGINT REFERENCES loyalty_accounts(id),
    type            VARCHAR(32) NOT NULL, -- 'earn', 'redeem', 'expire', 'adjust', 'refund'
    amount          DECIMAL(12,2) NOT NULL, -- позитивне або негативне
    balance_after   DECIMAL(12,2) NOT NULL,
    reason          VARCHAR(255),
    source_type     VARCHAR(64),  -- 'order', 'manual', 'birthday', 'referral'
    source_id       BIGINT,
    created_at      TIMESTAMPTZ DEFAULT NOW()
);

-- Рівні програми
CREATE TABLE loyalty_tiers (
    id              BIGSERIAL PRIMARY KEY,
    name            VARCHAR(64) NOT NULL,    -- Bronze, Silver, Gold, Platinum
    min_lifetime    DECIMAL(12,2) NOT NULL,  -- поріг за累積
    earn_multiplier DECIMAL(4,2) DEFAULT 1.0, -- множник нарахування
    redeem_rate     DECIMAL(4,2) DEFAULT 1.0, -- 1 бал = X рублів
    perks           JSONB                     -- додаткові привілеї
);

Транзакційний лог — архітектурне рішення. Баланс завжди рахується з історії або зберігається денормалізовано й пересчитується при розбіжності. Дозволяє аудітувати будь-який рух.

Нарахування балів

class LoyaltyService {
    public function earnPoints(User $user, float $amount, string $sourceType, int $sourceId): LoyaltyTransaction {
        $account = LoyaltyAccount::firstOrCreate(['user_id' => $user->id]);
        $tier    = $account->tier ?? LoyaltyTier::where('min_lifetime', 0)->orderBy('min_lifetime')->first();

        $points = round($amount * $tier->earn_multiplier * config('loyalty.earn_rate'));
        // earn_rate: наприклад, 0.05 = 5 балів за кожні 100 рублів

        return DB::transaction(function() use ($account, $points, $sourceType, $sourceId) {
            $newBalance = $account->balance + $points;
            $account->update([
                'balance'         => $newBalance,
                'lifetime_earned' => $account->lifetime_earned + $points,
            ]);

            // Пересчет рівня
            $newTier = LoyaltyTier::where('min_lifetime', '<=', $account->lifetime_earned)
                ->orderByDesc('min_lifetime')
                ->first();
            if ($newTier && $newTier->id !== $account->tier_id) {
                $account->update(['tier_id' => $newTier->id]);
                event(new TierUpgraded($account->user, $newTier));
            }

            return LoyaltyTransaction::create([
                'account_id'    => $account->id,
                'type'          => 'earn',
                'amount'        => $points,
                'balance_after' => $newBalance,
                'source_type'   => $sourceType,
                'source_id'     => $sourceId,
                'reason'        => 'Нарахування за покупку',
            ]);
        });
    }
}

Списання балів при оплаті

public function redeemPoints(User $user, float $pointsToRedeem, int $orderId): array {
    $account = LoyaltyAccount::where('user_id', $user->id)->lockForUpdate()->first();

    if (!$account || $account->balance < $pointsToRedeem) {
        throw new InsufficientPointsException();
    }

    $tier       = $account->tier;
    $discount   = $pointsToRedeem * $tier->redeem_rate; // бали -> рублі
    $newBalance = $account->balance - $pointsToRedeem;

    DB::transaction(function() use ($account, $pointsToRedeem, $newBalance, $orderId, $discount) {
        $account->update(['balance' => $newBalance]);
        LoyaltyTransaction::create([
            'account_id'    => $account->id,
            'type'          => 'redeem',
            'amount'        => -$pointsToRedeem,
            'balance_after' => $newBalance,
            'source_type'   => 'order',
            'source_id'     => $orderId,
            'reason'        => 'Оплата балами',
        ]);
    });

    return ['discount' => $discount, 'points_used' => $pointsToRedeem, 'remaining' => $newBalance];
}

Сгорання балів

Бали можуть мати строк дії. Завдання в планировщику:

// Щодня о 02:00
Schedule::call(function() {
    // Знаходимо транзакції earn, які сгорають сьогодні
    $expiring = LoyaltyTransaction::where('type', 'earn')
        ->whereDate('expires_at', today())
        ->where('expired', false)
        ->get();

    foreach ($expiring as $transaction) {
        $available = $this->loyaltyService->getAvailableFromTransaction($transaction);
        if ($available > 0) {
            $this->loyaltyService->expirePoints($transaction->account, $available, $transaction->id);
        }
    }
})->dailyAt('02:00');

Правила нарахування: конфіговані кампанії

Замість хардкода множників — таблиця кампаний:

CREATE TABLE loyalty_campaigns (
    id          BIGSERIAL PRIMARY KEY,
    name        VARCHAR(255),
    type        VARCHAR(32),    -- 'multiplier', 'fixed', 'category', 'birthday'
    multiplier  DECIMAL(4,2),
    fixed_bonus DECIMAL(10,2),
    conditions  JSONB,          -- {"min_order": 2000, "category_ids": [5, 12]}
    starts_at   TIMESTAMPTZ,
    ends_at     TIMESTAMPTZ,
    active      BOOLEAN DEFAULT TRUE
);
public function getApplicableCampaigns(Order $order): Collection {
    return LoyaltyCampaign::active()
        ->where('starts_at', '<=', now())
        ->where(fn($q) => $q->whereNull('ends_at')->orWhere('ends_at', '>=', now()))
        ->get()
        ->filter(fn($campaign) => $this->campaignApplies($campaign, $order));
}

private function campaignApplies(LoyaltyCampaign $campaign, Order $order): bool {
    $conditions = $campaign->conditions ?? [];

    if (isset($conditions['min_order']) && $order->total < $conditions['min_order']) {
        return false;
    }
    if (isset($conditions['category_ids'])) {
        $hasCategory = $order->items->pluck('product.category_id')
            ->intersect($conditions['category_ids'])
            ->isNotEmpty();
        if (!$hasCategory) return false;
    }
    return true;
}

UI: віджет баланса й історія транзакцій

const LoyaltyWidget: React.FC = () => {
    const { data: account } = useQuery({
        queryKey: ['loyalty', 'account'],
        queryFn: () => api.get('/loyalty/account'),
    });

    if (!account) return null;

    return (
        <div className="loyalty-widget bg-gradient-to-r from-amber-400 to-orange-500 rounded-xl p-4 text-white">
            <div className="flex justify-between items-center">
                <div>
                    <p className="text-sm opacity-80">Бонусний баланс</p>
                    <p className="text-3xl font-bold">{account.balance.toLocaleString()}</p>
                    <p className="text-xs opacity-70">балів</p>
                </div>
                <div className="text-right">
                    <p className="text-sm opacity-80">Рівень</p>
                    <p className="font-semibold">{account.tier.name}</p>
                    <p className="text-xs opacity-70">×{account.tier.earn_multiplier} до балів</p>
                </div>
            </div>
        </div>
    );
};

Інтеграція з чекаутом

Покупець вибирає, скільки балів застосувати:

const maxRedeemable = Math.min(
    loyaltyAccount.balance,
    order.total * (loyaltySettings.max_redeem_percent / 100)
);

Терміни реалізації

Базова система з нарахуванням, списанням та історією транзакцій: 1,5–2 тижні. Додавання рівнів, кампаній з множниками, сгорання балів і віджетів для фронтенду: 3–4 тижні. Мобільна карта лояльності з QR-кодом та інтеграція з POS-терміналами: плюс 2–3 тижні.