Автоматизация маркетинговых акций в 1С: промо-коды и кешбэк

Розничная сеть запускает розыгрыш: за каждые 50 рублей в чеке покупатель получает уникальный код. Код приходит в мобильное приложение, участвует в розыгрыше призов. Звучит просто. На практике — три месяца работы, управляемые блокировки и 11 BDD-тестов. Разбираем, как мы это реализовали в 1С.

Задача: уникальные коды за покупки

Заказчик — торговая сеть с 12 магазинами и программой лояльности. К 15-летию компании решили провести маркетинговую акцию: за каждые 50 рублей покупки — 4-символьный код для розыгрыша автомобиля. Коды должны выдаваться автоматически при пробитии чека, отображаться в мобильном приложении покупателя и быть уникальными в рамках всей акции.

Требования, которые выглядели простыми, но оказались непростыми:

  • Код — строго 4 символа (буквы + цифры), уникальный в рамках акции
  • Генерация при проведении ЧекККМ и РасходнойНакладной
  • При перепроведении чека — не выдавать повторно (идемпотентность)
  • Розничные покупатели без карты лояльности — не участвуют
  • Коды доступны в мобильном приложении через API
  • Несколько касс проводят чеки одновременно — дублей быть не должно
Архитектура системы промо-кодов в 1С

Архитектура: регистр, модуль, блокировка

Хранилище кодов — регистр сведений КодыРозыгрыша с измерениями: Покупатель, Акция, Код. Ресурс — ссылка на документ-основание (чек). Общий модуль РозыгрышСервер инкапсулирует всю логику: генерация кода, проверка уникальности, запись.

В маркетинговую акцию добавили два реквизита: ЭтоРозыгрыш (булево) и СуммаКратности (число — порог для выдачи кода). Когда кассир проводит чек, система проверяет: есть ли активная акция с признаком розыгрыша? Сумма чека делится на кратность? Покупатель идентифицирован по карте? Если все условия выполнены — генерируется код.

Критический момент — защита от дублей при параллельной работе касс. 4-символьный код даёт пространство в 1 679 616 комбинаций (36 в четвёртой степени). При 10 000 покупок за акцию вероятность коллизии невелика, но она есть. А гонка данных на уровне транзакций — ещё вероятнее: две кассы генерируют код одновременно, обе проверяют уникальность, обе видят «свободно», обе записывают. Дубль.

Управляемые блокировки: как исключить гонку

Решение — управляемая блокировка данных внутри транзакции. Перед генерацией кода устанавливаем исключительную блокировку на регистр КодыРозыгрыша по измерению «Акция». Второй поток, дойдя до этой точки, встаёт в очередь и ждёт, пока первый завершит транзакцию.

Паттерн управляемой блокировки для защиты от дублей кодов

Последовательность действий атомарна: начать транзакцию — заблокировать — сгенерировать код — проверить уникальность — записать — зафиксировать. Если что-то пошло не так — откат всей транзакции, код не записан, следующая попытка при перепроведении.

Идемпотентность при перепроведении: перед генерацией нового кода проверяем — есть ли уже запись в регистре с этим документом-основанием. Если есть — пропускаем. Бухгалтер может перепровести чек сколько угодно раз, дополнительные коды не появятся.

Бонусная программа: два сценария списания

Параллельно с розыгрышем у заказчика работала бонусная программа: кешбэк 3% от покупок. Бонусы копятся, бонусами можно оплатить до 30% чека. Но бонусы не вечные — предусмотрены два сценария списания.

Первый — акционные бонусы сгорают по дате. Причём не через фиксированные 90 дней, а через ПериодСгорания, который индивидуален для каждого покупателя и привязан к его текущей скидке. Золотая карта — 180 дней. Серебряная — 90. Обычная — 60. Старый код использовал фиксированную константу — покупатели с золотыми картами теряли бонусы раньше времени.

Жизненный цикл бонусов: начисление, проверка активности, списание

Второй сценарий — кешбэк списывается по неактивности. Нет покупок 90 дней — бонусы обнуляются. Казалось бы, просто: запросил обороты за 90 дней, у кого ноль — списать. Но виртуальная таблица .Обороты() выдавала ноль для покупателей, которые сделали покупку и возврат на одинаковую сумму. Итог = 0, система решала — неактивен, списать. Хотя человек заходил в магазин на прошлой неделе.

Исправление: заменили виртуальную таблицу .Обороты() на запрос к физической таблице регистра ПродажиТоваров с условием КОЛИЧЕСТВО(РАЗЛИЧНЫЕ Регистратор) > 0. Если есть хотя бы одна запись за период — покупатель активен, неважно какой итоговый оборот.

Сторнирование бонусов при возврате

Отдельная головная боль — возвраты. Покупатель возвращает акционный товар — нужно списать бонусы, начисленные за эту покупку. Старый код делал запрос к регистру по регистратору (чеку продажи) и сторнировал все бонусы за этот чек. Проблема: если за одну смену покупатель пробил три чека с бонусами, а вернул товар из одного — сторнировались бонусы всех трёх.

Причина: запрос фильтровал по Регистратор = ЧекПродажи, но не учитывал, что в одной смене может быть несколько чеков одного покупателя. Исправление — итерация по табличной части Бонусы конкретного чека продажи вместо запроса к регистру. Сторнируются ровно те бонусы, которые были начислены за возвращаемый товар.

Регламентное задание списания обрабатывает данные порциями по 50 000 строк. При 80 000 активных покупателей и нескольких миллионах записей в регистре — это критично. Один документ СписаниеБонусов на 5 000 строк с движениями по трём регистрам проводился 40 секунд. Разбивка на порции сократила время до 8 секунд на порцию, а общее время регламента — с 12 минут до 3.

API для мобильного приложения

Коды розыгрыша нужны в мобильном приложении. HTTP-сервис buyerInfo уже возвращал данные покупателя: имя, баланс бонусов, историю покупок. Мы расширили ответ полем raffleCodes — массив объектов с кодом, датой выдачи и названием акции.

Мобильное приложение показывает коды в профиле покупателя. Каждый код — отдельная карточка с QR для проверки на розыгрыше. Никаких SMS, никаких печатных купонов — всё в приложении.

Покрытие тестами

Для системы промо-кодов написали 11 BDD-сценариев на Vanessa-Automation:

  • Генерация кода при покупке на сумму больше кратности
  • Отсутствие кода при покупке меньше кратности
  • Уникальность кода в рамках акции
  • Идемпотентность при перепроведении чека
  • Отсутствие кода для розничного покупателя без карты
  • Два кода при покупке на двойную кратность
  • Корректность возврата кодов через API buyerInfo
  • Параллельная генерация с двух касс (стресс-тест)
  • Сторнирование при возврате — только нужные бонусы
  • Списание по неактивности — покупка + возврат не считается неактивностью
  • Списание акционных бонусов по индивидуальному сроку

Три из этих тестов ловят баги, которые были в продуктиве до нашей доработки. Теперь при каждом обновлении конфигурации — автоматический прогон. Регрессия исключена.

Итоги проекта

Акция длилась два месяца. За это время:

  • Выдано 23 000+ уникальных промо-кодов
  • Ноль дублей (управляемая блокировка работает)
  • Ноль ложных списаний бонусов (физическая таблица вместо виртуальной)
  • Время сторнирования при возврате — корректное, без побочных эффектов
  • Покрытие BDD-тестами — 11 сценариев, прогон за 4 минуты

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