0
Обложка: Как создать цифровую валюту за 2 недели

Как создать цифровую валюту за 2 недели

Рассказываем, почему разработать собственный сервис иногда быстрее и дешевле, чем взять готовый SaaS.

Всем привет! На связи Александр Терехов — технический директор сети смарт-офисов SOK. 2 месяца назад мы запустили систему лояльности с собственной внутренней валютой, которой можно оплачивать аренду офиса и услуги коворкинга. Это первый подобный кейс в сфере корпоративной офисной недвижимости. Сегодня расскажу, как мы сделали ее всего за 2 недели и на какие бизнес-результаты теперь рассчитываем.

Зачем мы придумали SOK Coins

Немного о нас. Мы компания SOK — сеть смарт-офисов, развиваем 8 площадок в Москве, Казани и Санкт-Петербурге. Зачем нам понадобилась внутренняя валюта? Если коротко: лиды в сфере аренды коммерческой недвижимости стоят дорого, а удерживать их не так просто.

Чтобы это исправить, мы решили больше заботиться о тех, кто уже работает с нами. Увеличивать показатель LTV — срок жизни клиента и ARPD — средний доход в расчёте на один рабочий стол. Захотели поощрять клиентов за то, что они давно работают с нами. В результате этой идеи родился проект SOK Coins — бонусный счет для резидентов сети наших смарт-офисов.

Как реализовали идею

В начале собирались «срезать угол»: взять готовую систему лояльности — UDS или другое решение, и интегрировать его в нашу PMS — систему управления объектом недвижимости. Для нее в свое время мы тоже взяли готовый продукт от SPACEPASS (php, Yii2, nodeJS, Symfony, PosgreSQL, CI на GitLab и Docker) , выбрали on-premise развертывание c правом самостоятельных доработок, и успешно развиваем его уже больше года. Но после анализа рынка поняли, что «срезание угла» — это не про интеграцию готового решения с нашей PMS, а про ее самостоятельную доработку.

Дело в том, что за 2 месяца мы так и не нашли легко интегрируемую платформу лояльности для нашей ниши. Все механики существующих платформ — инструменты типа «приведи друга и получи бонус» или «купи 5 чашек кофе, получи шестую в подарок». Это не то, что работает в сегменте офисной недвижимости. Нам было важно «субсидировать» длительность аренды, своевременные оплаты и вклад резидента в развитие комьюнити SOK. И для всего этого мы разработали собственную логику. В результате, решение идти по пути доработки собственной PMS оказалось выигрышным и наиболее быстрым.

Но как так получилось? Ведь мы не специализируемся на разработке ПО, и ресурс у нас ограничен. Все просто: в нашей PMS уже была готовая сущность «Сертификат». Это депозитный внутренний счет, который клиент пополняет сам через web-личный кабинет или мобильное приложение на выбранную сумму. И может списывать с него средства для оплаты аренды офиса, рабочего места, переговорных и любых других услуг наших рабочих пространств.

Этот продукт пользовался спросом у клиентов, заменять его на бонусный счет программы лояльности SOK Coins было нежелательно. Поэтому мы просто дублировали данную сущность, допилили фронт шаблонными инструментами (Metronic), утвердили коммерческую политику по реализации программы лояльности и силами внештатного программиста разработали под нее логику автоматических начислений за нужные нам полезные действия. Все успели за 2 недели. С начала лета запустили проект на всю базу резидентов.

Как сейчас работает система

У любой программы лояльности, помимо бонусного счета, есть еще два компонента — логика начислений и логика трат бонусной валюты. С ними пришлось повозиться: нельзя было промахнуться с размером начислений, чтобы не сломать юнит-экономику. В процессе внутреннего согласования участвовали все отделы, кроме эксплуатации и инжиниринга. Вместе решали: за что начислять бонусы и как сохранить выгоду для компании? Внутри команды развернулись дебаты.

Первая идея — поощрять резидентов, которые укажут на недостаток в сервисе. Но что, если люди начнут их искать только для того, чтобы заработать баллы? Да и постоянно извиняться даже за самую небольшую проблему — странно с точки зрения психологии коммуникации. Не поможет построить здоровые отношения с резидентами.

В итоге привязали все к деньгам и немножко к лояльности. Сейчас мы начисляем коины за:

  • Самостоятельную оплату услуг онлайн;
  • Длительное бесперебойное пользование услугами — чем оно дольше, тем выше бонусы за каждый платеж;
  • Оформление долгосрочных подписок;
  • Реферальную активность;
  • Активность внутри сообщества: организацию мероприятий и клубов внутри коворкинга.

И да, система лояльности действительно получилась «жирной». Если обычно компании предлагают бонусы в размере 1-3% от суммы платежа, мы даем 5, 10, 15% и больше. Допустим, давний клиент SOK платит за офис 300 тысяч рублей в месяц. По условиям программы, если он с нами больше 2 лет — с каждого арендованного офиса получает «коинсбэк» в размере 15%. А это 45 тысяч, которыми можно заплатить за следующий месяц аренды или любые другие услуги.

Градация проста: чем дольше клиент с нами — тем больше получит кешбэк. Резиденты с трехмесячным стажем получают 5% в бонусах с каждого платежа, а все, кто с нами дольше двух лет — 15%. Посмотреть сумму бонусов можно в личном кабинете резидента на сайте и в мобильном приложении SOK.


Так выглядит меню бонусов в нашей системе 

Так выглядит меню бонусов в нашей системе

Считаем первые результаты SOK Coins

Как внедрение SOK Coins скажется на LTV — можно будет посчитать только в конце года. Но некоторые выводы можно озвучить уже сейчас:

Ощутимо вырос процент самостоятельных онлайн-покупок — с 3% до 28%. Это помогло разгрузить менеджеров, которые общаются с клиентами. Сообщество стало активнее: внутри коворкингов появляются новые клубы. Теперь резиденты меньше заинтересованы уйти к конкурентам, даже если у них дешевле.

Выгодна ли система для бизнеса? Она полностью в рамках нашей unit-экономики. В нашем бизнесе относительно большие расходы на привлечение клиентов — один лид в среднем обходится в 1500 рублей, а иногда и дороже. Если работаем с брокерами, им нужно заплатить солидную комиссию. От этого страдает наша валовая прибыль. Часть маржи лучше отдавать не брокеру, а клиенту — чтобы он дольше пользовался нашими услугами. Так мы снижаем ротацию резидентов: сложно отказаться от услуг офисного оператора, когда на бонусном счете лежат средства на пару месяцев аренды.

Как все реализовано технически

Начисление баллов лояльности работает на базе существующего функционала сертификатов. Сертификат — это товар, который можно положить в корзину и купить. Он имеет заранее фиксированную цену, она равняется количеству бонусных баллов. Клиенты (или администратор) оформляют заказ, который содержит сертификат с определенной ценой. В дальнейшем клиент может использовать этот сертификат для оплаты других услуг.

Таким образом, начисление баллов лояльности сводится к автоматическому оформлению заказа, который содержит сертификат.

Технически это реализовано при помощи событий Yii: при оформлении заказа генерируется соответствующее событие “event”, на которое реагирует сервис начисления “coins”. Он, в свою очередь, анализирует оформляемый заказ и, если все условия пройдены, создаёт ещё один заказ, который содержит сертификат с необходимой ценой. В результате клиент видит в личном кабинете сразу два заказа: один непосредственно с заказанными услугами, второй — с подарочным сертификатом.

Начисление баллов на основе событий Yii имеет большое значение. Для получения обновлений от SPACEPASS необходимо вести доработки SOK таким образом, чтобы сохранялась совместимость с продуктом SPACEPASS. Событие, которое возникает при оформлении заказа, генерирует базовый продукт SPACEPASS, мы же, в свою очередь, лишь добавляем еще один обработчик этого события. Таким образом, начисление coins остается самобытным и отделенным от базового продукта, а значит, потенциальные конфликты между логикой SPACEPASS и SOK сведены к минимуму.

Пара слов о правилах начисления баллов. Есть набор заранее определенных условий: минимальная сумма заказа, время с последнего предыдущего заказа, содержимое заказа, количество рабочих мест и период аренды. Может быть бесконечное число правил начисления, которые содержат какие-либо из приведенных выше условий. Эти правила реализуются в виде ActiveRecord и хранятся в базе данных. У нас есть интерфейс по редактированию этих правил. Вот, как он выглядит:

Основную бизнес-логику реализует вспомогательный класс для работы с SOK Coins — CoinsService. Весь программный код нашей PMS разбит на модули. Код CoinsService находится в отдельном модуле, который мы в SOK писали для себя.

Подключение классов, которые будут использоваться:

<?php
namespace common\services;
use api\models\ResourceRate;
use common\components\COrder;
use common\components\order\COrderCart;
use common\components\Time;
use common\models\CoinsRule;
use common\models\Location;
use common\models\Order;
use common\models\OrderResource;
use common\models\OrderStatus;
use common\models\OrderTariff;
use common\models\Package;
use common\models\PaymentMethod;
use common\models\ProductCategory;
use common\models\ProductCategory2product;
use Exception;
use InvalidArgumentException;
use Yii;
use yii\db\Exception as ExceptionDb;

Вспомогательный класс для работы с coins (баллы лояльности):

class CoinsService
{
    public const COINS_RESOURCE_RATE_ID = 483; //COINS ID resource_rate
    public const TARIFFS_CATEGORY_ID = 175; //Категория, объединяющая тарифы Флекс, Фикс, Клуб
    private const ALLOW_TIME_BETWEEN = 3600; // - допустимый промежуток (сек.) между тарифами, чтобы считать их непрерывными (одна из логик начисления SOK Coins - непрерывность пользования услугами SOK) 

Код CoinsServise поочередно перебирает все правила (объекты ActiveRecord), проверяет возможность начисления баллов лояльности и суммирует количество баллов, если заказ соответствует условиям этого правила. В конце создает заказ, который содержит сертификат с требуемым количеством баллов.

Возврат списка правил, подходящих для заказа:

     * @param Order $order
     * @return CoinsRule[]
     */

Набор правил, применимый для объекта Order 
(Объект Order - основной транзакционный объект PMS)
 
    public static function applicableRules(Order $order): array
    {
        if($order->payment_id == PaymentMethod::KEY_FREE_ID) {
            return []; //на бесплатные заказы бонусы не начисляются
$rules = [];
        foreach(CoinsRule::find()->where(['coworking_id' => $order->coworking_id])->with('productCategory')->all() as $rule) {
            / @var CoinsRule $rule */   - перечисляются все имеющиеся в системе правила на предмет соответствия заказу. Из заказов отбираются только те, которые удовлетворяют условиям начисления SOK Coins 
            $categoryIDs = self::childCategoryIDs($rule->product_category_id);
            / @var int $quantity Количество продуктов в заказе, соответствующих категории правила */
            if($rule->productCategory->type === ProductCategory::TYPE_TARIFF) {
                $quantity = OrderTariff::find()
                    ->where([
                        'order_id' => $order->id,
                        'tariff_id' => ProductCategory2product::find()->select('product_id')->where(['category_id' => $categoryIDs])
                    ])->sum('quantity');
            } else {
                $quantity = OrderResource::find()
                    ->where([
                        'order_id' => $order->id,
                        'resource_rate_id' => ProductCategory2product::find()->select('product_id')->where(['category_id' => $categoryIDs])
                    ])->sum('quantity');
            }
            if(!$quantity) {
                continue; //заказ не содержит тарифов, на которые начисляются бонусы
            }
            if($rule->yourself && $order->user_id != $order->manager_id) { //Покупка должна быть оформлена самостоятельно
                continue;
            }
            if($rule->min_tariff_quantity > 0 && $quantity < $rule->min_tariff_quantity) { //Минимальное количество тарифов в заказе
                continue;
            }
            if($rule->max_tariff_quantity > 0 && $quantity > $rule->max_tariff_quantity) { //Максимальное количество тарифов в заказе
                continue;
            }
            if($rule->tariff_period > 0 || $rule->extend_before > 0) {
                $packages = self::orderPackages($order, $categoryIDs, $rule->productCategory->type); //Тарифы заказа
                $daysBetween = null; //Количество дней с момента окончания действия последнего тарифа до момента создания заказа
                $tariffStartDate = $order->ts_start;
                foreach($packages as $package) {
                    if($tariffStartDate - $package->ts_end > self::ALLOW_TIME_BETWEEN) {
                        continue;
                    }
                    if($package->ts_start < $tariffStartDate) {
                        $tariffStartDate = $package->ts_start;
                    }
                    if(!$daysBetween) {
                        $daysBetween = ($order->paid_at - $package->ts_end) / 86400;
                    }
                }
                $daysPeriod = ($order->ts_start - $tariffStartDate) / 86400;
                unset($package, $tariffStartDate);
                if($rule->tariff_period > 0 && $daysPeriod < $rule->tariff_period) { //Кол-во дней, в течении которых тариф должен действовать непрерывно
                    continue;
                }
                if($rule->extend_before > 0 && $daysBetween * -1 > $rule->extend_before) { //Кол-во дней до окончания тарифа, за которые должен продлён или куплен иной тариф
                    continue;
                }
            }
            $rules[] = $rule;
        }
        return $rules;
    }
    private Location $location;
    private ?int $companyID;
    private ?int $userID;
    /
     * @param int|Location $location
     * @param int|null $companyID
     * @param int|null $userID Для юридических лиц используется только при создании заказа
     */
    public function __construct($location, ?int $companyID = null, ?int $userID = null)
    {
        if(!$companyID && !$userID) {
            throw new InvalidArgumentException('Company ID or user ID required');
        }
        / @noinspection PhpFieldAssignmentTypeMismatchInspection */
        $this->location = is_int($location) ? Location::findOne($location) : $location;
        $this->companyID = $companyID;
        $this->userID = $companyID ? null : $userID;
    }
* Создаёт заказ и связанный с ним пакет coins, зачисляя таким образом баллы лояльности
     * @param int $count Количество баллов
     * @param string|null $comment Комментарий к заказу
     * @return Order|null
     * @throws ExceptionDb
     * @throws Exception
     */

Наша PMS-система состоит из разных модулей, которые взаимодействуют друг с другом, и часто не знают друг о друге. Взаимодействие модулей происходит по методу событий. Обработчики событий SOK Coins:

class SOKEventHandlers extends BaseObject - обработчик, который реагирует на событие “Оплата заказа” - после этого выполняется код SOK Coins, который начисляет депозит ниже 
{
    public function init()
    {
        parent::init();
        Event::on(Order::class, Order::EVENT_PAID, [$this, 'depositCoins']);
    }
    /
     * Обработчик, который начисляет баллы лояльности за оформление заказа
     * @param Event $event
     * @throws Exception
     */
    public function depositCoins(Event $event): void
    {
        / @var Order $order */
        $order = $event->sender;
        $coins = 0;
        foreach(CoinsService::applicableRules($order) as $rule) {
            $coins += self::amountForOrder($order, $rule); - перечисление правил, применимых для заказа, и суммирование Coins
        }
        if($coins > 0) {
            (new CoinsService($order->location_id, $order->company_id, $order->user_id))->deposit($coins, S::t('sok.common.coins', 'for order #').$order->id); - создание заказа на зачисление Coins

Важный момент — как мы защищаемся от накрутки баллов лояльности. Событие, на которое реагирует сервис, вызывается после получения оплаты или когда администратор делает пометку, что заказ оплачен. Таким образом, нужно сперва оплатить заказ, чтобы баллы начислились. Единственный вариант для накрутки — отменить заказ после его оплаты. Такой сценарий требует ручного вмешательства, поэтому защиты от него не предусмотрено. Но и с фродом за 4 месяца работы программы лояльности мы пока не сталкивались.

Наши планы и амбиции

Что мы имеем в итоге? Наша цель на старте выполнена — запуститься быстро и сделать результат без больших трат на эксперименты и тесты. Теперь у нас есть гибкая и легко расширяемая программа лояльности в рамках собственной PMS — со своей экономикой и валютой.

Наши дальнейшие планы:

  • Постоянно тестировать новые политики накопления бонусов — в зависимости от запросов бизнеса.
  • Запустить комбинированные оплаты. Например, «коины + депозит» и «коины + оплата по счету».
  • Доработать программу лояльности визуально — с ачивками, историей начисления и списания коинов и прочими привычными атрибутами.
  • Интегрировать SOK-Coins с существующей партнерской программой — чтобы за коины можно было не только арендовать офис, но и, например, пообедать в соседнем кафе.

Чему нас научила эта история? Сейчас говорят, что рынок насыщен цифровыми решениями под любые задачи — «ищущий да найдет пути интеграции». Но наш опыт доказывает: взять готовое не всегда проще, быстрее и дешевле. Строить собственную цифровую экосистему для работы вдолгую, адаптировать ее гибко под запросы бизнеса — актуально, как никогда.