Приветствуем всех разработчиков и архитекторов систем лояльности! В современной розничной торговле и сфере услуг бонусные программы стали неотъемлемым инструментом удержания клиентов. Однако при проектировании собственного модуля лояльности в 1С перед разработчиком неизбежно встает сложная архитектурная задача: как эффективно и без потери производительности реализовать механизм сгорания бонусов по истечении их срока действия?
В этой статье мы подробно разберем несколько подходов к решению этой задачи — от классического партионного учета до изящных архитектурных решений, применяемых в типовых конфигурациях компании «1С» (таких как ERP, «Управление торговлей» и «Комплексная автоматизация»). Мы проанализируем их плюсы и минусы, посмотрим на примеры кода и выясним, как избежать проблем с производительностью системы при работе с десятками тысяч клиентов.
Давайте по шагам разберем варианты реализации этой логики.
---
Первое решение, которое интуитивно приходит на ум любому разработчику 1С, знакомому со складским учетом — это организация партионного учета бонусов. Суть метода заключается в том, что каждому начислению бонусов присваивается уникальный идентификатор партии (обычно это сам документ начисления или специальный элемент справочника).
При такой архитектуре регистр накопления остатков БонусныеБаллы будет содержать следующие измерения:
При начислении бонусов мы делаем движение "Приход" с указанием конкретной партии и даты сгорания. Когда клиент тратит бонусы, система выполняет списание по методу ФИФО (первым пришел — первым ушел): сначала списываются бонусы из самых старых партий. Ежедневно регламентное задание анализирует остатки регистра, находит партии, у которых ДатаСгорания меньше текущей даты, и полностью списывает их остатки в ноль.
Рассмотрим пример алгоритма списания бонусов при оплате покупки по методу ФИФО:
// Пример получения остатков партий бонусов для списания по ФИФО
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| БонусыОстатки.ПартияНачисления КАК ПартияНачисления,
| БонусыОстатки.ПартияНачисления.ДатаСгорания КАК ДатаСгорания,
| БонусыОстатки.НачисленоОстаток КАК Остаток
|ИЗ
| РегистрНакопления.БонусныеБаллы.Остатки(&ТекущаяДата, Клиент = &Клиент) КАК БонусыОстатки
|ГДЕ
| БонусыОстатки.НачисленоОстаток > 0
| И БонусыОстатки.ПартияНачисления.ДатаСгорания > &ТекущаяДата
|УПОРЯДОЧИТЬ ПО
| ДатаСгорания ВОЗР";
Запрос.УстановитьПараметр("Клиент", ПараметрыКлиента);
Запрос.УстановитьПараметр("ТекущаяДата", ТекущаяДата());
РезультатЗапроса = Запрос.Выполнить();
// Далее в коде выполняется стандартный обход выборки с распределением списываемой суммы по партиям
Преимущества партионного учета:
Недостатки и подводные камни:
---
Интересный альтернативный подход заключается в том, чтобы отказаться от хранения партий, но производить движения по регистру "наперед".
При начислении бонусов документ делает сразу две записи в регистр накопления остатков:
Если клиент не успевает потратить бонусы, они автоматически сгорают в указанную дату за счет заранее сделанного "минусового" движения. Однако возникает логический вопрос: что делать, если клиент потратил часть бонусов (например, 50 из 100 начисленных)?
При оформлении расхода нам придется программно искать будущее плановое списание на дату сгорания и корректировать его (делать компенсирующее движение "Приход" на +50 баллов на дату сгорания, чтобы в будущем списалось только фактически оставшиеся 50). Поиск этой даты сгорания и логика корректировок "будущих периодов" делает систему очень хрупкой и склонной к накоплению ошибок при частых изменениях чеков или возвратах.
---
Давайте подробно разберем, как разработчики фирмы «1С» элегантно решили эту задачу в своих типовых конфигурациях. Они отказались от партионного учета, но сохранили быстродействие системы и простоту расчетов с помощью специальной структуры регистра.
В регистре накопления остатков БонусныеБаллы создается два ресурса:
В качестве ключевого измерения используется ДатаОкончанияДействия (или просто дата сгорания).
Когда клиенту начисляют 100 бонусов со сроком действия 30 дней, документ делает два движения типа Приход:
Начислено = +100, КСписанию = 0, ДатаОкончанияДействия = ПустаяДата.Начислено = 0, КСписанию = +100, ДатаОкончанияДействия = ДатаСгорания.Когда клиент тратит 50 бонусов, документ списания делает движение Расход только по ресурсу Начислено с текущей датой. При этом будущие плановые движения по ресурсу КСписанию никак не корректируются! Это позволяет избежать сложных расчетов в транзакции продажи и гарантирует высокую скорость работы кассы.
Ежедневно ночью запускается регламентное задание, которое рассчитывает сумму реального сгорания по следующему алгоритму:
Начислено (на текущий момент).КСписанию на текущую дату.Минимум(Остаток Начислено, Остаток К Списанию).Рассмотрим пример запроса для регламентного задания, определяющего сумму сгорания бонусов:
// Запрос для расчета сумм сгорания бонусов на текущую дату
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ОстаткиБонусов.Клиент КАК Клиент,
| ОстаткиБонусов.НачисленоОстаток КАК ДоступныйОстаток,
| ОстаткиБонусовПоДатам.КСписаниюОстаток КАК ПланируемоеСписание
|ИЗ
| РегистрНакопления.БонусныеБаллы.Остатки(&ТекущаяДата, ) КАК ОстаткиБонусов
|
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрНакопления.БонусныеБаллы.Остатки(&ТекущаяДата, ДатаОкончанияДействия <= &ТекущаяДата) КАК ОстаткиБонусовПоДатам
| ПО ОстаткиБонусов.Клиент = ОстаткиБонусовПоДатам.Клиент
|ГДЕ
| ОстаткиБонусовПоДатам.КСписаниюОстаток > 0";
Запрос.УстановитьПараметр("ТекущаяДата", НачалоДня(ТекущаяДата()));
Результат = Запрос.Выполнить();
В процессе обхода результата запроса мы рассчитываем сумму сгорания как Минимум(ДоступныйОстаток, ПланируемоеСписание) и формируем движения списания, уменьшая оба ресурса. Этот подход работает чрезвычайно быстро и не нагружает систему блокировками при совершении розничных продаж.
---
Помимо выбора технической архитектуры регистра, обязательно проанализируйте следующие бизнес-сценарии, с которыми вы неизбежно столкнетесь при запуске системы:
Используя схему «Начислено / К списанию», сложно напрямую из остатков регистра получить информацию в духе: «У вас 1 декабря сгорит 50 бонусов», так как расходы не уменьшают будущие записи КСписанию до момента регламентного расчета. Чтобы показать клиенту корректный прогноз в СМС или личном кабинете, вам потребуется написать виртуальный алгоритм распределения текущего остатка бонусов по будущим датам сгорания "на лету" (имитируя ФИФО только в момент формирования отчета или отправки сообщения).
Что делать, если клиент возвращает товар, за покупку которого были начислены бонусы? Если он их уже потратил, баланс бонусного счета может уйти в минус — это нормальная практика для большинства ритейлеров. А если возвращается товар, который был частично оплачен бонусами? В этом случае баллы нужно вернуть клиенту на карту. Но с каким сроком действия? Если их первоначальный срок действия уже истек, пока товар находился у покупателя, хорошим тоном будет дать им "вторую жизнь" (например, продлить срок действия возвращенных бонусов еще на 14 дней).
Если у вашей компании широкая филиальная сеть с распределенными базами данных (РИБ), локальные кассы не должны напрямую писать движения по бонусам в свои базы. Клиент может совершить покупку в одном магазине и сразу же поехать в другой, потратив бонусы дважды до прохождения синхронизации. Для решения этой проблемы кассовые программы отправляют быстрые Web- или HTTP-запросы к центральной базе 1С ("Серверу лояльности"), которая выступает единственным и неделимым источником правды для бонусных балансов.
Подводя итог, можно сказать, что для небольших баз данных (до 10-15 тысяч клиентов) вполне жизнеспособен классический партионный учет по методу ФИФО. Однако, если вы закладываете архитектуру "на вырост" и хотите избежать проблем с производительностью и блокировками в будущем, мы настоятельно рекомендуем использовать типовую схему 1С с ресурсами Начислено и КСписанию.