Акции и лояльность
Правила скидок, их суммирование, расчёт лучшей цены, кэшбэк, уровни и рефералы — без потери денег.
За 30 секунд
Акции и программы лояльности — это то, как магазин возвращает людей: скидки, бесплатная доставка, подарки, кэшбэк, уровни и награды за приглашение друзей. Под капотом это движок правил, который решает, какие предложения применимы к корзине, как они сочетаются и сколько в итоге платит покупатель. Самое сложное — понятные правила суммирования, предсказуемый результат «лучшей цены» и защита от злоупотреблений.
Почему я это использую
- Я использую движки акций на правилах, потому что строил их для нескольких магазинов: заказ еды в ресторанах, косметика и винный клуб — всем нужны были скидки, которые маркетинг может настраивать без разработчика.
- Маркетинговые команды меняют предложения каждую неделю. Если каждая кампания требует изменения кода, магазин либо стоит на месте, либо выкатывает баги.
- Покупатели злятся, когда два валидных предложения дают цену хуже, чем одно. Перебор комбинаций и выбор лучшего результата это предотвращает.
- Деньги лояльности — это настоящие деньги. Кошельки, кэшбэк и рефералы требуют той же аккуратности, что и платежи: леджеров, правил сгорания и проверок на мошенничество.
Как это работает
01Анатомия правила акции
У каждой акции три части: условия, действие и лимиты. Условия решают, применима ли она: минимальная сумма корзины, определённые товары или категории, сегмент покупателя или уровень лояльности, только первый заказ, канал (веб или приложение), диапазон дат, день недели. Действие — это то, что она даёт: процентную скидку, фиксированную скидку, фиксированную цену, «купи X — получи Y», подарочный товар или бесплатную доставку. Лимиты держат её под контролем: общее число использований, использования на покупателя, максимальная скидка, бюджет. Акции бывают автоматическими (применяются, когда выполняются условия) и ручными (активируются кодом или кнопкой). Именно хранение правил как данных — строк с типизированными условиями и действиями — а не как кода, позволяет маркетингу работать без разработчиков.
- Процентная: 10% на корзину или выбранные строки, при необходимости с ограничением сверху.
- Фиксированная: 5.00 скидки на заказы от 30.00.
- Первый заказ, день рождения, уровень лояльности: условия на покупателя, а не на корзину.
- Подарочный товар или бесплатная доставка: действия, которые меняют строки или сборы, а не только итог.
02Суммирование, приоритет и эксклюзивность
Суммирование (stacking) — это сочетание нескольких акций в одном заказе. Без правил скидки накладываются друг на друга и превращаются в убытки. Практичная модель: у каждой акции есть приоритет и режим суммирования — эксклюзивная (не сочетается ни с чем), сочетается с любой или сочетается только внутри своей группы. Определите и порядок применения: процентные скидки обычно применяются раньше фиксированных, а скидки на строки — раньше скидок на корзину, потому что порядок меняет результат. Решите, применяется ли скидка к цене до или после других скидок, и никогда не позволяйте итогу опуститься ниже нуля или ниже заданной маржи. Запишите эти правила в короткую таблицу, которую утвердит маркетинг; затем реализуйте ровно эту таблицу и протестируйте её.
03Лучшая цена для покупателя
Когда подходят сразу несколько предложений, но суммировать их все нельзя, какое побеждает? Самый дружелюбный ответ: та комбинация, которая даёт покупателю наименьший итог. Движок сначала отфильтровывает подходящие акции, затем строит допустимые правилами суммирования комбинации, симулирует каждую на копии корзины и выбирает самый дешёвый результат — с детерминированным правилом разрешения ничьей, например сначала более высокий приоритет, потом более старая акция. Число комбинаций быстро растёт, поэтому держите его ограниченным: эксклюзивные предложения образуют сценарии из одного элемента, а сочетаемые обычно суммируются жадно внутри своей группы. Возвращайте не только итог, но и пояснение по каждой строке («Летняя распродажа −10%») — оно показывается в чеке и упрощает ответы на вопросы в поддержке.
type Cart = { lines: { id: string; price: number; qty: number }[] };
type Promo = { id: string; priority: number; exclusive: boolean; apply: (c: Cart) => number };
// Returns the discount combination with the lowest final total
export function bestScenario(cart: Cart, eligible: Promo[]) {
const subtotal = cart.lines.reduce((s, l) => s + l.price * l.qty, 0);
const stackable = eligible.filter((p) => !p.exclusive).sort((a, b) => b.priority - a.priority);
const scenarios: Promo[][] = [stackable, ...eligible.filter((p) => p.exclusive).map((p) => [p])];
let best = { promos: [] as Promo[], total: subtotal };
for (const promos of scenarios) {
const discount = promos.reduce((sum, p) => sum + p.apply(cart), 0);
const total = Math.max(0, subtotal - discount);
if (total < best.total) best = { promos, total };
}
return best; // ties keep the earlier, higher-priority scenario
}
04Кошельки, кэшбэк и уровни лояльности
Кэшбэк возвращает процент от заказа на кошелёк покупателя в виде бонусов магазина. Относитесь к кошельку как к леджеру: никогда не храните только баланс — храните каждое начисление и списание с причиной и ссылкой на заказ, а баланс вычисляйте или проверяйте по этим строкам. Начисляйте кэшбэк только после доставки заказа или после окончания срока возврата, иначе люди будут покупать, получать бонусы и оформлять возврат. Оплата с кошелька — это способ оплаты на checkout, который применяется после акций. Уровни лояльности (например, Bronze, Silver, Gold) вознаграждают покупателей за сумму покупок или баллы за период. Определите, как покупатель поднимается на уровень, когда может опуститься и есть ли льготный период. Баллы и балансы часто сгорают после периода неактивности; отправьте напоминание до того, как это случится.
- Строки леджера: начисление, списание, сгорание, ручная корректировка — у каждой указано, кто и почему.
- Ручные корректировки сотрудниками заслуживают аудит-лога, а крупные — второго подтверждения.
- Пересчитывайте уровни по расписанию и синхронизируйте их с сегментами покупателей.
05Рефералы, ваучеры и проверки на мошенничество
Реферальная программа вознаграждает покупателя за приглашение друзей: каждый пользователь получает код или ссылку, друг регистрируется по ней, и оба получают награду после первого подходящего заказа друга. Отслеживайте клики и регистрации, а в мобильных приложениях используйте deferred deep links, чтобы код пережил установку приложения. Начисляйте награды отложенной пакетной задачей, а не мгновенно, чтобы можно было отменить награды за возвращённые заказы. Рефералы и ваучеры притягивают злоупотребления: самоприглашения со второго аккаунта, много аккаунтов на одном телефоне или карте, коды, выложенные на купонных сайтах. Базовые проверки ловят большую часть: то же устройство, телефон, отпечаток карты или адрес, что у пригласившего; возвращённый заказ; слишком много регистраций по одному коду за короткое время. Ваучеры должны быть случайными, достаточно длинными, чтобы их нельзя было подобрать, с ограничением частоты запросов и одноразовыми, если они имеют денежную ценность.
Когда применять
- Маркетинг часто запускает кампании и должен настраивать их без разработчиков.
- Бизнес держится на повторных покупателях: заказ еды, косметика, товары для животных, кофе, винные клубы.
- Вам нужно несколько типов наград — скидки, подарки, кэшбэк, уровни, рефералы, — которые должны предсказуемо взаимодействовать.
Когда не стоит
- Магазину с одним-двумя простыми промокодами достаточно встроенных купонов платформы.
- Когда маржа низкая, а команда не может измерить эффект акций, — скидки без аналитики просто раздают деньги.
- Не создавайте валюту в баллах, если простая процентная скидка достигает той же цели.
Альтернативы
- Скидки платформы (Shopify, WooCommerce)
Хороши для стандартных купонов и автоматических скидок; ограничены, когда суммирование или кастомные условия усложняются.
- Talon.One / Voucherify
Хостинговые движки акций и лояльности с конструкторами правил и API; быстро внедряются, но стоят денег регулярно, в зависимости от объёма.
- SaaS для лояльности (Smile.io, Yotpo)
Готовые виджеты баллов и рефералов; меньше контроля над правилами и данными.
Лучшие практики
- Храните акции как данные с типизированными условиями, действиями и лимитами.
- Пропишите явные правила суммирования и приоритетов и получите согласование от бизнеса.
- Перебирайте комбинации и детерминированно выбирайте лучший итог для покупателя.
- Показывайте пояснение по каждой строке для каждой применённой скидки.
- Ведите кошельки и баллы как леджеры; никогда не ограничивайтесь одной колонкой баланса.
- Откладывайте кэшбэк и реферальные награды до момента, когда заказ уже нельзя вернуть.
- Логируйте и проверяйте ручные корректировки; для крупных требуйте второго подтверждения.
Частые ошибки
- Хардкодить кампании, из-за чего каждая акция — это деплой.
- Позволять скидкам накладываться так, что они превышают стоимость заказа или уходят ниже себестоимости.
- Разные результаты на странице корзины и на checkout, потому что движок работает в двух местах.
- Начислять кэшбэк при оплате, а потом терять деньги на возвратах.
- Реферальные программы без проверок на мошенничество, которые притягивают фермы фейковых аккаунтов.
Глубже
06Архитектура движка: чистый pricing-конвейер
Самые надёжные движки акций — это чистые функции: получая корзину, контекст покупателя, время и список активных акций, они возвращают оценённую корзину и объяснение, ничего не записывая в базу. Поэтому их легко тестировать на точных суммах, безопасно запускать при каждом изменении корзины, и результат одинаков на странице корзины, на оформлении и в превью в админке. Стройте конвейер по этапам: загрузить кандидатов (заранее отфильтрованных по дате, каналу и статусу с помощью индекса), проверить условия, собрать сценарии, применить действия к копии корзины, выбрать лучший и сформировать корректировки по строкам. Побочные эффекты — учёт использования, списание с ваучера, начисление кэшбэка — происходят позже, только когда заказ создан или завершён, внутри транзакции заказа.
type Ctx = { now: Date; customerId?: string; segment: string[]; channel: "web" | "app" };
type Adjustment = { lineId?: string; promoId: string; amount: number; label: string };
// Pure: same input → same output, no I/O
export function priceCart(cart: Cart, ctx: Ctx, promos: Promo[]) {
const eligible = promos.filter((p) => p.isActive(ctx.now) && p.matches(cart, ctx));
const best = bestScenario(cart, eligible);
const adjustments: Adjustment[] = best.promos.flatMap((p) => p.adjustments(cart));
return { total: best.total, adjustments };
}
07Лимиты использования при конкурентном доступе
«Первые 500 покупателей» и «одно использование на покупателя» звучат просто, пока 2 000 человек не оформляют заказ в одну и ту же минуту. Посчитать использования через SELECT COUNT, а потом вставить запись — это гонка: все видят 499. Обеспечивайте лимиты средствами базы. Для глобального лимита держите строку-счётчик и увеличивайте её условно (UPDATE promotions SET used = used + 1 WHERE id = :id AND used < max_uses) в транзакции заказа. Для лимитов на покупателя вставляйте строку redemption с уникальным ограничением на (promotion_id, customer_id). Освобождайте использование, если заказ отменён до оплаты. Лимиты бюджета работают так же, только с суммами вместо количества. Проверяйте ещё раз на оформлении и смиритесь с тем, что страница корзины может показать акцию, которая через секунду закончится; сообщайте об этом покупателю понятно.
08Как измерить, работает ли акция
Акция — это затраты; вопрос в том, что на них купили. Отслеживайте по каждой акции: заказы, сумму скидок, выручку, маржу после скидки, новых и повторных покупателей и долю использования. Сырые цифры вводят в заблуждение, потому что многие покупатели купили бы и так. Честная метрика — инкрементальность: сравните случайно выбранную группу, получившую предложение, с контрольной группой без него и посмотрите на разницу в заказах и марже. Даже простое разделение 90/10 для купона из рассылки даёт ответы лучше любого дашборда. Для программ лояльности следите за долей повторных покупок и временем между заказами по уровням, а также за обязательствами по непогашенным баллам или балансам кошельков, которые финансисты могут учитывать как отложенные расходы.
- Храните ID применённых акций и суммы в строках заказа для отчётности.
- Показывайте стоимость скидок в процентах от выручки по каждой кампании.
- Держите контрольную группу для автоматических предложений вроде купонов на возврат клиента.
09Граничные случаи учёта баллов и кошельков
Кошельки на основе ledger порождают граничные случаи, которые лучше решить заранее. Сгорание: при списании по принципу FIFO расходы сначала используют самые старые начисления, поэтому задача сгорания точно знает, сколько осталось от каждого начисления; храните остаток по каждой партии начислений, чтобы это было дёшево. Частичные возвраты: если заказ, частично оплаченный бонусами из кошелька, частично возвращается, решите, что возвращается первым — бонусы или деньги на карту, — и отменяйте кэшбэк пропорционально. Конкурентность: списания должны блокировать строку кошелька покупателя или использовать условный апдейт, чтобы два параллельных оформления не потратили один и тот же баланс. Валюта: кошелёк должен хранить одну валюту. С юридической стороны в некоторых странах бонусы магазина или баллы могут считаться stored value или электронными деньгами со своими правилами защиты потребителей и учёта, поэтому проверьте это, прежде чем разрешать покупателям пополнять кошелёк реальными деньгами.
Реальные ситуации
Купон утёк на сайт со скидками
Проблема
Код блогера на 30%, рассчитанный на небольшую аудиторию, появляется на агрегаторе купонов. За несколько часов его используют тысячи раз, и маржа за день уходит в минус.
Как решить
Немедленно поставьте акцию на паузу из админки — у каждой акции должен быть переключатель активности, который срабатывает без деплоя. Уже оформленные заказы обычно выполняют, чтобы сохранить доверие, если только условия не позволяют отмену. Затем исправьте дизайн: задайте кампаниям блогеров общий лимит использований и лимит бюджета, обеспечивайте их атомарно и добавьте лимит на покупателя с уникальным ограничением на redemption. Для таргетированных предложений используйте уникальные одноразовые коды, сгенерированные для каждого получателя, а не один общий код. Добавьте алерт на скорость использований, например больше чем в три раза выше ожидаемого числа в час, чтобы следующую утечку поймать за минуты.
Маркетинг хочет «купи два — самый дешёвый бесплатно»
Проблема
Текущий движок поддерживает только процентные и фиксированные скидки на всю корзину. Новое предложение должно работать на категорию со смешанными ценами и количествами.
Как решить
Добавьте новый тип действия, а не частный случай. Разверните подходящие строки в единицы (строка с количеством 3 становится тремя единицами), отсортируйте их по цене и в каждой группе нужного размера отметьте самую дешёвую единицу как скидку 100%. Возвращайте корректировки по строкам, чтобы строка с двумя единицами могла получить одну бесплатную. Решите и задокументируйте правила: какая группа получает выгоду — самая дешёвая или самая дорогая, применяются ли другие скидки к бесплатной единице и как работает частичный возврат. Добавьте тесты на смешанные цены, нечётные количества и возвраты и показывайте «1 бесплатно» в строке чека.
Балансы кэшбэка растут быстрее ожидаемого
Проблема
Финансисты замечают, что неиспользованные балансы кошельков за полгода выросли втрое, и у некоторых покупателей большие балансы от заказов, которые потом вернули.
Как решить
Сначала аудит: пересоберите балансы из ledger и найдите начисления, исходные заказы которых были возвращены без соответствующей отмены. Исправьте процесс так, чтобы кэшбэк начислялся только после закрытия окна возврата, а возврат автоматически записывал сторнирующую проводку в ledger. Введите сгорание неактивных балансов с напоминанием, скажем, за 14 дней и записывайте сгорание как отдельный тип проводки. Каждый месяц отражайте непогашенный баланс как обязательство, чтобы финансисты могли его планировать. Если балансы нужно вернуть у покупателей, которые их уже потратили, разрешите отрицательный баланс, который зачитывается против будущих начислений, вместо того чтобы списывать деньги с покупателей.
Шпаргалка
- Глобальный лимит использований, атомарно
UPDATE promotions SET used = used + 1 WHERE id = :id AND used < max_uses;- Одно использование на покупателя
CREATE UNIQUE INDEX redemptions_uq ON promotion_redemptions (promotion_id, customer_id);- Баланс кошелька из ledger
SELECT COALESCE(SUM(amount), 0) FROM wallet_entries WHERE customer_id = :id;- Заблокировать кошелёк перед списанием
SELECT id FROM wallets WHERE customer_id = :id FOR UPDATE;- Случайный код ваучера (Node)
crypto.randomBytes(8).toString("base64url").toUpperCase() // ~64 bits of entropy- Rate limit на попытки ввода ваучера (Laravel)
Route::post('/cart/voucher', ApplyVoucher::class)->middleware('throttle:10,1');- Индекс активных акций
CREATE INDEX promotions_active_idx ON promotions (status, starts_at, ends_at);- Начисление реферальных наград раз в час
Schedule::job(new SettleReferralRewards)->hourly()->withoutOverlapping();
В моих реальных проектах
- На платформе для сети ресторанов я построил движок акций примерно на 1,335 строк: обычные акции, акции на первый заказ, по уровню лояльности и с кэшбэком, ручные и автоматические, подарочные товары, с правилами суммирования и симуляцией комбинаций для выбора лучшего итога; плюс кошелёк кэшбэка с процентами по уровням и погашением ваучеров.
- В интернет-магазине косметики похожий движок примерно на 1,294 строки обрабатывает обычные акции, акции на первый заказ, по уровню лояльности, с кэшбэком и с бесплатным подарком, с выбором лучшего сценария и сгоранием по расписанию, и связан с кошельком бонусов магазина.
- На платформе товаров для животных я сделал реферальную программу с кодами приглашений, отслеживанием кликов и шеринга, deferred deep links для установок приложения, проверками на мошенничество и ежечасным начислением наград; а винный клуб работает на кодах регистрации бутылок, баллах и пяти уровнях лояльности.
Вопросы и ответы
Q1Как вы решаете, какая акция применяется, если подходят несколько?
Сначала я отфильтровываю акции, условия которых совпадают с корзиной и покупателем. Затем строю только те комбинации, которые разрешены правилами суммирования: эксклюзивные акции — по одной, сочетаемые — вместе в порядке приоритета. Я симулирую каждый сценарий на копии корзины и выбираю наименьший итог с детерминированным правилом разрешения ничьей, например по приоритету. Один и тот же движок работает и для страницы корзины, и для checkout, поэтому покупатель никогда не увидит другую цену при оплате. Результат включает пояснение по каждой строке для чеков и поддержки.
Q2Почему кошелёк лучше моделировать как леджер, а не как колонку баланса?
Колонка баланса говорит, сколько у человека есть, но не почему. В леджере каждое начисление, списание, сгорание и ручная корректировка — это строка с суммой, причиной, ссылкой и автором. Вы можете объяснить любой баланс, отменить конкретную транзакцию, списать старые бонусы по возрасту и проверить действия сотрудников. С конкурентностью тоже безопаснее: вы вставляете строки, а не перезаписываете число. Закэшированная колонка баланса допустима ради скорости, но она должна выводиться из леджера и регулярно проверяться.
Q3Как остановить реферальное мошенничество?
Награды выплачиваются отложенной задачей начисления, только после того как первый заказ друга выполнен и срок возврата прошёл. Перед выплатой проверки сравнивают новый аккаунт с пригласившим: то же устройство, номер телефона, отпечаток платёжной карты, адрес доставки или паттерн IP. Лимиты скорости помечают коды со слишком большим числом регистраций за короткое время. Подозрительные случаи откладываются на ручную проверку, а не отклоняются автоматически. Коды можно отозвать, а возвращённые заказы откатывают награды через леджер.
Q4Сначала процент или сначала фиксированная сумма?
Это меняет результат, поэтому это должно быть осознанное правило. На корзине в 100.00 скидка 10%, а затем 5.00 даёт 85.00, а 5.00, а затем 10% даёт 85.50 — значит, процент первым выгоднее для покупателя. Многие магазины сначала применяют скидки на строки, затем на корзину, а процентные — раньше фиксированных. Важнее всего согласованность: один и тот же порядок везде, задокументированный и покрытый тестами с точными суммами.
Q5Как бы вы спроектировали уровни лояльности?
Определите метрику (сумма покупок или баллы) и окно (скользящие 12 месяцев или календарный год), пороги и выгоды каждого уровня, например более высокий процент кэшбэка. Затем определите неудобные моменты: когда покупателя понижают, есть ли льготный период или нижняя граница и что происходит с возвратами. Задача по расписанию пересчитывает уровни и записывает изменения, а принадлежность к уровню питает сегменты покупателей для кампаний. Покупатель всегда должен видеть свой прогресс до следующего уровня.
Вопросы уровня senior
S1Полный перебор для лучшей цены или жадное применение — как выбрать?
Полный перебор всех комбинаций даёт действительно лучшую цену, но растёт экспоненциально: двадцать совместимых акций — это около миллиона подмножеств. Жадное применение по приоритету быстрое и предсказуемое, но может упустить более выгодную комбинацию. На практике я совмещаю оба подхода: группы суммирования и эксклюзивность сокращают пространство до нескольких сценариев, внутри каждого сценария акции применяются жадно, а движок сравнивает итоги сценариев. Я ограничиваю число оцениваемых сценариев и логирую, когда лимит достигнут. Движок также должен быть детерминированным, со стабильными правилами разрешения ничьих, потому что цена, меняющаяся между двумя одинаковыми запросами, разрушает доверие. Маркетинг обычно ценит предсказуемость и объяснимость выше, чем выжимание математически оптимальной скидки.
S2Движок акций лучше писать самому или покупать?
Покупайте, когда потребности типовые и важна скорость: облачные движки дают конструкторы правил, бюджеты, коды, аналитику и A/B-тесты, а команде не нужно поддерживать сложную подсистему. Цена — тарификация по объёму, дополнительный сетевой вызов в пути расчёта цены (поэтому нужен fallback на случай медленного ответа) и ограничения, когда правила зависят от ваших собственных данных, например слотов доставки или меню филиалов. Пишите сами, когда акции — ключевое конкурентное преимущество, когда правила глубоко зависят от доменных данных или когда цену нужно считать офлайн или за миллисекунды. Если пишете сами, держите движок чистым, хорошо протестированным модулем, где акции — это данные, чтобы решение позже можно было пересмотреть.
S3Как не дать программе лояльности превратиться в финансовое обязательство, которое никто не понимает?
С первого дня относитесь к баллам и кошелькам как к деньгам. Каждый баланс — это ledger начислений и списаний, поэтому финансисты видят выданные, погашенные, сгоревшие и непогашенные суммы по месяцам. Определите стоимость балла и политику сгорания и регулярно отчитывайтесь о непогашенных обязательствах; по распространённым стандартам учёта награды за лояльность, выданные вместе с продажей, могут откладывать признание части выручки от этой продажи, поэтому подключайте бухгалтера заранее. Ограничьте начисление наград за заказы со скидкой или возвращённые заказы. Сделайте правила уровней явными, включая понижение, чтобы привилегии не росли бесконечно. И наконец, время от времени сравнивайте программу с контрольной группой: если участники покупают не больше похожих неучастников, программа — это просто скидка с лишними шагами.
Глоссарий
- Stacking (суммирование)
- Применение более чем одной акции к одному заказу или строке.
- Эксклюзивная акция
- Предложение, которое нельзя совмещать ни с какой другой акцией.
- Сценарий
- Одна допустимая комбинация акций, смоделированная на копии корзины для сравнения итогов.
- Корректировка
- Сумма скидки, привязанная к конкретной строке или заказу, вместе с акцией, которая её вызвала.
- Кэшбэк
- Процент от заказа, возвращаемый покупателю бонусами магазина, обычно после доставки.
- Партия начисления
- Одно начисление в кошелёк с отслеживаемым остатком, чтобы сгорание и списание по FIFO были точными.
- Инкрементальность
- Дополнительные продажи, которые действительно вызвала акция, измеренные относительно группы, не получившей её.
- Контрольная группа (holdout)
- Покупатели, намеренно исключённые из предложения, чтобы измерить его реальный эффект.
- Deferred deep link
- Ссылка, данные которой переживают установку приложения, так что после первого запуска оно открывается с реферальным кодом.
Что изучить дальше
Есть идея продукта?Давайте создадим его.
Расскажите о своей идее, продукте или команде. Обычно отвечаю в течение одного рабочего дня.