Розничная сеть запускает розыгрыш: за каждые 50 рублей в чеке покупатель получает уникальный код. Код приходит в мобильное приложение, участвует в розыгрыше призов. Звучит просто. На практике — три месяца работы, управляемые блокировки и 11 BDD-тестов. Разбираем, как мы это реализовали в 1С.
Задача: уникальные коды за покупки
Заказчик — торговая сеть с 12 магазинами и программой лояльности. К 15-летию компании решили провести маркетинговую акцию: за каждые 50 рублей покупки — 4-символьный код для розыгрыша автомобиля. Коды должны выдаваться автоматически при пробитии чека, отображаться в мобильном приложении покупателя и быть уникальными в рамках всей акции.
Требования, которые выглядели простыми, но оказались непростыми:
- Код — строго 4 символа (буквы + цифры), уникальный в рамках акции
- Генерация при проведении ЧекККМ и РасходнойНакладной
- При перепроведении чека — не выдавать повторно (идемпотентность)
- Розничные покупатели без карты лояльности — не участвуют
- Коды доступны в мобильном приложении через API
- Несколько касс проводят чеки одновременно — дублей быть не должно
Архитектура: регистр, модуль, блокировка
Хранилище кодов — регистр сведений КодыРозыгрыша с измерениями: Покупатель, Акция, Код. Ресурс — ссылка на документ-основание (чек). Общий модуль РозыгрышСервер инкапсулирует всю логику: генерация кода, проверка уникальности, запись.
В маркетинговую акцию добавили два реквизита: ЭтоРозыгрыш (булево) и СуммаКратности (число — порог для выдачи кода). Когда кассир проводит чек, система проверяет: есть ли активная акция с признаком розыгрыша? Сумма чека делится на кратность? Покупатель идентифицирован по карте? Если все условия выполнены — генерируется код.
Критический момент — защита от дублей при параллельной работе касс. 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 минуты
Ключевой урок: маркетинговые акции в ритейле — это не «добавить поле и кнопку». Это конкурентный доступ, транзакционная целостность, интеграция с внешними системами и регрессионное тестирование. Подходить к таким задачам нужно как к полноценной разработке, а не как к «доработке на вечер».


