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

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

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка системы управления программой лояльности
Средняя
от 1 недели до 3 месяцев
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1214
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    852
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    823
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    815

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

Программа лояльности — это не только «бонусные баллы». Это механизм, который меняет поведение покупателя: заставляет возвращаться, увеличивать средний чек, выбирать конкретный канал. Технически это учётная система баллов, уровней и вознаграждений, встроенная в все точки контакта с клиентом.

Архитектура: счёт, транзакции, уровни

-- Бонусный счёт пользователя
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,     -- всего заработано (для уровней)
    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,  -- порог по accumulated
    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>
            {account.next_tier && (
                <div className="mt-3">
                    <p className="text-xs mb-1">
                        До уровня {account.next_tier.name}: {account.next_tier.remaining.toLocaleString()} баллов
                    </p>
                    <div className="bg-white/30 rounded-full h-1.5">
                        <div
                            className="bg-white rounded-full h-1.5 transition-all"
                            style={{ width: `${account.tier_progress}%` }}
                        />
                    </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 недели.