Перейти к содержимому

Бонусные баллы - внутренний счёт, начисление и частичная оплата

Собираем бонусную программу из штатного внутреннего счёта: начисление, частичная оплата, история и обмен с учётной системой.

Механика

Готовой бонусной программы в продукте нет, и начинать стоит именно с этого. Уровни, сгорание, кэшбэк по категориям и карты лояльности - это всё своя разработка либо готовое решение из Маркетплейса, а не настройка.

Штатное хранилище всё же существует, и называется оно внутренним счётом покупателя. Счёт хранит сумму в валюте, умеет пополняться и умеет списываться при оплате заказа платёжной системой внутреннего счёта.

Курс баллов задают своей валютой, если один балл не равен рублю. Валюта с курсом превращает баллы в обычные деньги платформы, и вся арифметика заказа начинает работать без единой строки своего кода.

Начисляют баллы за оплаченный заказ, а не за созданный. Заказ создаётся за секунду, отменяется за минуту, и начисление по факту создания превращает бонусную программу в бесплатную раздачу денег.

Ограничение доли оплаты живёт на сервере, а не в форме. Правило «баллами не больше половины суммы» проверяют при создании платежа, потому что форму оформления покупатель видит и может поправить.

Частичная оплата - это два платежа у одного заказа. Один платёж уходит на внутренний счёт, второй - в платёжную систему, и заказ считается оплаченным только после обоих.

Обмен с учётной системой по баллам штатно не поддерживается. Регистры накопления из учётной системы в магазин не приезжают, поэтому остаток баллов возят своим каналом или свойством заказа.

Шаги

  1. Решить, где живут баллы: внутренний счёт, своя валюта или готовое решение.
  2. Включить платёжную систему внутреннего счёта и проверить её на тестовом заказе.
  3. Начислять баллы обработчиком события оплаты, с отметкой в самом заказе.
  4. Ограничить долю оплаты баллами проверкой на сервере, а не в форме.
  5. Показать остаток и историю операций в личном кабинете покупателя.
  6. Согласовать остаток с учётной системой своим каналом обмена.

Код

Смотрим остаток на счёте покупателя:

\Bitrix\Main\Loader::includeModule('sale');
$account = \Bitrix\Sale\Internals\UserAccountTable::getRow([
'filter' => ['=USER_ID' => $userId, '=CURRENCY' => 'RUB'],
'select' => ['CURRENT_BUDGET', 'LOCKED'],
]);
// LOCKED - это заблокированная часть остатка: она уже обещана незакрытым заказам
// у каждой валюты свой счёт: баллы в своей валюте не смешаются с рублями

Счёт заводится сам при первом пополнении, отдельно на каждую валюту. Именно поэтому баллы удобно держать своей валютой: остаток не смешивается с настоящими деньгами покупателя.

Начисляем баллы после оплаты заказа:

\Bitrix\Main\EventManager::getInstance()->addEventHandler(
'sale', 'OnSaleOrderPaid', ['\\Vendor\\Bonus', 'onPaid']);
public static function onPaid(\Bitrix\Main\Event $event): void
{
$order = $event->getParameter('ENTITY');
$bonus = round($order->getBasket()->getPrice() * 0.05); // процент от товаров
CSaleUserAccount::UpdateAccount($order->getUserId(), $bonus,
'RUB', 'BONUS', $order->getId(), 'Начисление за заказ');
// последний аргумент попадает в комментарий операции по счёту
}

Событие поднимается в момент перехода заказа в оплаченное состояние. Процент считают от суммы товаров, а не от итога: доставка и её стоимость к бонусной программе отношения обычно не имеют.

Защищаемся от повторного начисления:

$flag = $order->getPropertyCollection()->getItemByOrderPropertyId($bonusPropId);
if ($flag->getValue() === 'Y') { return; } // по этому заказу уже начисляли
$flag->setValue('Y');
$order->save();
// отметка живёт в самом заказе, поэтому переживает любые перезапуски
// свойство для неё заводят служебным: покупателю его показывать незачем

Событие оплаты поднимается не один раз за жизнь заказа. Менеджер снимает и возвращает признак оплаты, обмен присылает статус повторно, и без отметки покупатель получает баллы столько раз, сколько это случится.

Ограничиваем долю оплаты баллами:

$maxByBonus = $order->getPrice() * 0.5; // не больше половины суммы заказа
$fromAccount = min($requested, $maxByBonus, (float)$account['CURRENT_BUDGET']);
if ($fromAccount <= 0) {
return new \Bitrix\Main\Error('Оплата баллами по этому заказу недоступна');
}
// три ограничения сразу: запрос покупателя, правило магазина и остаток счёта

Правило про долю оплаты проверяют на сервере, при создании самого платежа. В форме оформления его тоже показывают, но верят только серверной проверке: значение из браузера приходит таким, каким его отправили.

Создаём платёж с внутреннего счёта:

$payment = $order->getPaymentCollection()->createItem(
\Bitrix\Sale\PaySystem\Manager::getObjectById(
\Bitrix\Sale\PaySystem\Manager::getInnerPaySystemId()));
$payment->setField('SUM', $fromAccount);
$payment->setField('CURRENCY', $order->getCurrency());
$order->save();
// остаток суммы уходит вторым платежом в обычную платёжную систему
// в форме оформления оплату со счёта включают отдельным параметром компонента

Частичная оплата собирается из двух платежей, привязанных к одному заказу. Списание со счёта при этом делает сама платформа, а не свой код: ручное списание мимо платежа разъезжается с заказом при первой же отмене.

Показываем историю операций в кабинете:

$rs = CSaleUserTransact::GetList(['TRANSACT_DATE' => 'DESC'],
['USER_ID' => $userId, 'CURRENCY' => 'RUB']);
while ($row = $rs->Fetch()) {
printf("%s %+.2f %s\n", $row['TRANSACT_DATE'], $row['AMOUNT'], $row['COMMENTS']);
}
// знак суммы разделяет начисления и списания: отдельного поля для этого нет

История начислений и списаний ведётся платформой сама, без единой строки кода. В кабинете её показывают штатные компоненты счёта: список операций и пополнение. Покупателю показывают её как есть: вопрос «куда делись мои баллы» закрывается списком операций, а не перепиской с поддержкой.

Возвращаем баллы при отмене заказа:

CSaleUserAccount::UpdateAccount($order->getUserId(),
-$bonusCharged, 'RUB', 'BONUS_CANCEL', $order->getId());
// возврат тоже операция по счёту, а не правка остатка в базе

Отмена заказа обязана отменять и начисление, сделанное по этому заказу. Правку остатка прямым запросом к таблице при этом не делают: история операций перестанет сходиться с остатком, и разобраться в претензии покупателя станет невозможно.

Отдаём остаток в учётную систему:

$prop = $order->getPropertyCollection()->getItemByOrderPropertyId($balancePropId);
$prop->setValue((string)$account['CURRENT_BUDGET']); // остаток на момент заказа
$order->save();
// штатный обмен заказами довезёт это свойство как обычный реквизит

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

Ограничения

Внутренний счёт - это деньги, а не баллы. У него нет срока сгорания, уровней программы, кэшбэка по категориям и ограничений по товарам, и всё перечисленное пишется своим кодом поверх счёта.

Сгорание баллов делают своим заданием по расписанию. Задание проходит по операциям, считает срок и списывает просроченное отдельной операцией, обязательно уведомляя покупателя заранее.

Начисление лучше привязывать к отгрузке, а не к оплате. Оплаченный, но возвращённый заказ - обычное дело в рознице, и баллы за него приходится забирать обратно, что покупатели воспринимают болезненно.

Оплата баллами сталкивается с онлайн-кассой. Чек пробивается на всю сумму заказа с разделением по способам расчёта, поэтому долю, оплаченную счётом, кассовый модуль должен видеть отдельной строкой платежа.

Готовое решение из Маркетплейса закрывает эту задачу целиком. Свой код оправдан там, где правила программы нестандартны, а на обычной механике накопления чужое решение дешевле собственной разработки.

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

Типичные проблемы

Баллы начислили за заказ, который потом отменили.

Начисление привязано к созданию заказа, а не к его оплате или отгрузке. Момент начисления переносят ближе к деньгам.

Покупатель оплатил баллами весь заказ целиком.

Ограничение доли проверялось только в форме оформления, а сумма пришла из браузера. Долю проверяют при создании платежа на сервере.

По одному заказу баллы начислились несколько раз.

Событие оплаты поднимается при каждом изменении признака оплаты. В заказе ставят отметку о выполненном начислении.

Остаток баллов на сайте не совпадает с учётной системой.

Регистры накопления штатным обменом не выгружаются вовсе. Остаток возят свойством заказа или отдельным файлом.

Списание прошло, а заказ остался неоплаченным.

Баллы списали своим кодом мимо платежа заказа. Списание со счёта делает платёжная система внутреннего счёта.

В личном кабинете нет истории начислений.

Показывается только остаток, а операции по счёту не выводятся. История лежит в транзакциях счёта покупателя.

Частые вопросы

Есть ли в Битриксе встроенная система оплаты баллами?

Есть внутренний счёт покупателя и платёжная система оплаты с этого счёта. Полноценной бонусной программы с уровнями и сгоранием в коробке нет: её собирают своим кодом или берут готовым решением.

Как сделать курс баллов к рублю не один к одному?

Завести для баллов свою валюту и задать ей курс. Дальше пересчёт делает сама платформа, а внутренний счёт в этой валюте хранит баллы отдельно от денег.

Как обменяться бонусами между сайтом и 1С?

Своим каналом: свойством заказа с остатком либо отдельным файлом по расписанию. Регистры накопления учётной системы штатный обмен не переносит, и рассчитывать на это не стоит.

Как запретить оплату баллами всей суммы?

Проверять долю при создании платежа на сервере и уменьшать сумму до допустимой. Ограничение только в форме оформления обходится подменой значения в браузере.

Где хранится история начислений и списаний?

В транзакциях внутреннего счёта: каждая операция записывается с суммой, датой и описанием. Оттуда же её показывают покупателю в личном кабинете.

Смежное

Первоисточник