]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
feat(TO8-22): промокоды БЛАГО — активация и промо-списание бонусов
authorVVF <developer@DeepBlue.localdomain>
Fri, 6 Mar 2026 11:11:42 +0000 (14:11 +0300)
committerVVF <developer@DeepBlue.localdomain>
Fri, 6 Mar 2026 11:11:42 +0000 (14:11 +0300)
- Миграция: добавлены поля activated_by, activated_at в таблицу promocode
- Модель Promocode: константа TIP_SALE_PROMOBONUS, метод isActivatable()
- API endpoint actionActivatePromocode(): начисление 350 промо-бонусов с tip_sale='promobonus'
- Модификация actionSale(): автоматический выбор промо-списания vs стандартного, без кэшбека при промо
- Unit-тесты: 15 тестов (isActivatable + алгоритм промо-списания)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
erp24/api2/controllers/BonusController.php
erp24/migrations/m260306_100000_add_activated_fields_to_promocode.php [new file with mode: 0644]
erp24/records/Promocode.php
erp24/tests/unit/controllers/BonusControllerPromoTest.php [new file with mode: 0644]
erp24/tests/unit/records/PromocodePromoTest.php [new file with mode: 0644]
plan_email.md [new file with mode: 0644]

index 2fdbc87eaa63ae3e02ddc836045fe1b73884c440..f7f453e1017bd4adfef0ace0da736cee09a60018 100644 (file)
-<?php
-
-namespace app\controllers;
-
-use yii_app\jobs\SendBonusInfoToSiteJob;
-use DateTime;
-use DateTimeZone;
-use Yii;
-use yii\helpers\ArrayHelper;
-use yii\helpers\Json;
-use yii_app\helpers\ClientHelper;
-use yii_app\records\BonusLevels;
-use yii_app\records\Contest001;
-use yii_app\records\ExportImportTable;
-use yii_app\records\MessagerUser;
-use yii_app\records\NotifiableUser;
-use yii_app\records\Products1c;
-use yii_app\records\Sales;
-use yii_app\records\Timetable;
-use yii_app\records\UniversalCatalogItem;
-use yii_app\records\UserBonusSendToTgLogs;
-use yii_app\records\Users;
-use yii_app\records\UsersAuthCallLog;
-use yii_app\records\UsersBonus;
-use yii_app\records\UsersBonusLevels;
-use yii_app\records\UsersEvents;
-use yii_app\records\UsersPhones;
-use yii_app\records\UsersStopList;
-use yii_app\services\LogService;
-use yii_app\services\SiteService;
-
-class BonusController extends BaseController
-{
-    private const LOG_CATEGORY_BONUS = 'bonus.auth';
-
-    private static $YEAR_PERIOD = 366;
-    private static $FIRST_SALE_PROCENT = 0.1;
-    private static $SECOND_SALE_PROCENT = 0.15;
-    private static $MAX_PROCENT = 0.2;
-    private static $CREDIT_PROCENT = 0.1;
-    private static $CREDIT_HIGH_PROCENT = 0.3;
-    private static $CREDIT_HIGH_PROCENT_PART20 = 0.2;
-
-    const OUT_DIR =
-//        "/tmp";
-        "/var/www/erp24/api2/json"; // "/www/api2/json";
-// __DIR__ . "/../json"; //local
-
-    public function actionGetBonuses()
-    {
-        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
-        $data = file_get_contents('php://input');
-        $result = json_decode($data, true);
-
-        $fl = date('_Y_m_d__H_i_s_');
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . $result['phone']);
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-
-        $__API_PARAMS = ['store_id', 'seller_id', 'phone']; // check_amount, items
-
-        foreach ($__API_PARAMS as $paramName) {
-            if (empty($result[$paramName])) {
-
-                if ($paramName != 'phone') {
-                    LogService::apiErrorLog(json_encode(["error_id" => 0, "error" => "$paramName is required"], JSON_UNESCAPED_UNICODE));
-                }
-
-                return $this->asJson(["error_id" => 0, "error" => "$paramName is required"]);
-            }
-        }
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-
-        $phone = ClientHelper::phoneClear($result['phone']);
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        if (!ClientHelper::phoneVerify($phone)) {
-            return $this->asJson(["error_id" => 0.2, "error" => "phone is required"]);
-        }
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        $result['phone'] = $phone;
-
-        $check_amount = intval($result['check_amount'] ?? 0);
-
-        $user = Users::find()->where(['phone' => $result['phone']])->andWhere(['phone_true' => '1'])->one();
-        $bonusLevels = BonusLevels::find()->where(['active' => 1])->indexBy('alias')->asArray()->all();
-        $bonusLevel = $user->bonus_level ?? "silver";
-
-        $bonus_rate = isset($bonusLevels[$bonusLevel]['bonus_rate'])
-            ? $bonusLevels[$bonusLevel]['bonus_rate'] / 100
-            : self::$FIRST_SALE_PROCENT;
-
-        $cashback_rate = isset($bonusLevels[$bonusLevel]['cashback_rate'])
-            ? $bonusLevels[$bonusLevel]['cashback_rate'] / 100
-            : self::$FIRST_SALE_PROCENT;
-
-        $mess = [];
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        // массив с id товарыми не участвующих в бонусной
-        $items_arr_no = array_values(ArrayHelper::map(
-            UniversalCatalogItem::find()->where(['catalog_alias' => 'unused_nomenclature'])->all(), 'guid', 'guid'));
-        $items_arr_no_bonus_writeoffs = array_values(ArrayHelper::map(
-            UniversalCatalogItem::find()->where(['catalog_alias' => 'non_bonusable_goods'])->all(), 'guid', 'guid'));
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-
-        $all_amount = 0;
-        $has_actions = false;
-        $summa_no = 0;
-        $summa_no_writeoffs = 0;
-        if (!empty($result["items"])) {
-            foreach ($result["items"] as $item) {
-                if (in_array($item["product_id"], $items_arr_no)) {
-                    $summa_no = $summa_no + $item["price"] * $item["quantity"];
-                    $has_actions = true;
-                } elseif (in_array($item["product_id"], $items_arr_no_bonus_writeoffs)) {
-                    $summa_no_writeoffs = $summa_no_writeoffs + $item["price"] * $item["quantity"];
-                }
-                $all_amount += $item["price"] * $item["quantity"];
-            }
-        }
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        $baza_nachislenie = $all_amount - $summa_no;
-
-        $check_amount = $check_amount - $summa_no;
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        //$cnt = intval(Sales::find()->where(['phone' => $result['phone'], 'operation' => Sales::OPERATION_SALE])->count());
-        //$max_procent = $cnt == 0 ? self::$FIRST_SALE_PROCENT : ($cnt == 1 ? self::$SECOND_SALE_PROCENT : self::$MAX_PROCENT);
-        $max_procent = $bonus_rate;
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        $percent = ($result['phone'] == "79049031399") ? 0.9 : $max_procent;
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        $userFound = Users::find()->where(['phone' => $result['phone']])->one();
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        /** @var $userFound Users */
-        $salesCount = -1; /* Из-за нулевого значения по умолчанию куча клиентов получило бонус 20% за покупку */
-        if ($userFound && $userFound->telegram_created_at) {
-            $salesCount = intval(Sales::find()->where(['phone' => $result['phone'], 'operation' => Sales::OPERATION_SALE])
-                ->andWhere(['>=', 'date', $userFound->telegram_created_at])->count());
-        }
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        $credit_procent = $userFound && $userFound->source > 0 && $salesCount == 0 ? self::$CREDIT_HIGH_PROCENT : $cashback_rate;
-        $will_be_credited_bonuses = $credit_procent * $baza_nachislenie;
-
-        $will_be_credited_bonuses = round($will_be_credited_bonuses);
-        $mess["will_be_credited_bonuses"] = $will_be_credited_bonuses;
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        $store_id = ClientHelper::getExportId($result['store_id'], "city_store", 1);
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        // Логи введённых номеров телефонов кассирами
-        $userPhone = UsersPhones::find()->where(['phone' => $result['phone']])->andWhere(['store_id' => $store_id])
-            ->andWhere(['seller_id' => $result['seller_id']])->one();
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        if (!$userPhone) {
-            $userPhone = new UsersPhones();
-            $userPhone->phone = $result['phone'];
-            $userPhone->store_id = $store_id;
-            $userPhone->store_guid = $result['store_id'];
-            $userPhone->seller_id = $result['seller_id'];
-        }
-        if (!$userPhone->store_guid) {
-            $userPhone->store_guid = $result['store_id'];
-        }
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        $userPhone->date = date('Y-m-d H:i:s');
-        $userPhone->save();
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        if ($userPhone->getErrors()) {
-            file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            LogService::apiErrorLog(json_encode(["error_id" => 1, "error" => $userPhone->getErrors()], JSON_UNESCAPED_UNICODE));
-
-            return $this->asJson(["error_id" => 1, "error" => $userPhone->getErrors()]);
-        }
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        $user = Users::find()->where(['phone' => $result['phone']])->andWhere(['phone_true' => '1'])->one();
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        if (!$user) {
-            file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '-нет в бонусной программе-' . __LINE__, FILE_APPEND);
-            $mess["new_client"] = true;
-            $mess["message_cashier"] = "Заполните данные клиента";
-            $mess["error"] = "Покупателя " . $result['phone'] . " нет в бонусной программе!";
-
-            return $this->asJson($mess);
-        }
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        $keycode = $user->keycode;
-        $black_list = $user->black_list;
-
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        if (!$black_list) {
-            file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            $stop = UsersStopList::find()->select(['phone'])->where(['phone' => $result['phone']])->one();
-            if ($stop) {
-                $black_list = 1;
-                $user->black_list = 1;
-                file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-                $user->save();
-                file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-                if ($user->getErrors()) {
-                    file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-                    LogService::apiErrorLog(json_encode(["error_id" => 3, "error" => $user->getErrors()], JSON_UNESCAPED_UNICODE));
-
-                    return $this->asJson(["error_id" => 3, "error" => $user->getErrors()]);
-                }
-            }
-        }
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        $name = $user->name;
-        $user_balans = ClientHelper::getBonusBalance($result['phone']);
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        $baza_spisanie = $baza_nachislenie - $summa_no_writeoffs;
-        if ($baza_spisanie < 0) {
-            $baza_spisanie = 0;
-        }
-        $max = $baza_spisanie * $percent;  // максимально можем разрешить списывать до 30 процентов от суммы заказа
-        $max = round($max);
-        $available_bonus = $user_balans;
-        if ($available_bonus > $max) { // если баллов бонусов больше чем 30 процентов списываем по максимуму 30
-            $available_bonus = $max;
-        }
-//        $baza = $check_amount - $bonus;
-
-        $mess["message_cashier"] = "Клиент $name найден"; // Код: $keycode
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        if ($black_list) {
-            $mess['error'] = 'Этот номер в черном списке';
-
-            return $this->asJson($mess);
-        }
-
-        $txt = $has_actions ? 'В чеке есть акционные товары, на них бонусы не начислятся.' : '';
-
-        $mess["result"] = true;
-        $mess["auth_code"] = $keycode;
-        $mess["name"] = $name;
-        $mess["total_bonuses"] = $user_balans;
-        $mess["bonus_level"] = $bonusLevel;
-        $mess["burn_balans"] = $user->burn_balans;
-        $mess["available_bonuses"] = $available_bonus;
-        $mess["message_cashier"] = $txt . " Спросите последние 4 цифры телефона который позвонит клиенту $user_balans";
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        LogService::apiLogs(1, json_encode($mess, JSON_UNESCAPED_UNICODE));
-
-        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__ . ' OK ', FILE_APPEND);
-        return $this->asJson($mess);
-    }
-
-//    public function actionSendMessage()
-//    {
-//        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
-//        $data = file_get_contents('php://input');
-//        $result = json_decode($data, true);
-//
-//        $__API_PARAMS = ['store_id', 'seller_id', 'phone'];
-//
-//        foreach ($__API_PARAMS as $paramName) {
-//            if (empty($result[$paramName])) {
-//
-//                if ($paramName != 'phone') {
-//                    LogService::apiErrorLog(json_encode(["error_id" => 1, "error" => "$paramName is required"], JSON_UNESCAPED_UNICODE));
-//                }
-//
-//                return $this->asJson(["error" => "$paramName is required"]);
-//            }
-//        }
-//
-//        $phone = ClientHelper::phoneClear($result['phone']);
-//        if (!ClientHelper::phoneVerify($phone)) {
-//            return $this->asJson(["error_id" => 0.2, "error" => "phone is required"]);
-//        }
-//        $result['phone'] = $phone;
-//
-//        $user = Users::find()->where(['phone' => $result['phone']])->andWhere(['phone_true' => '1'])->andWhere(['black_list' => '0'])->one();
-//        if (!$user) {
-//            $mess["error"] = "Покупателя " . $result['phone'] . " нет в бонусной программе!";
-//
-//            return $this->asJson($mess);
-//        }
-//        $keycode = $user->keycode;
-//
-//        $mess = [];
-//        $mess["result"] = true;
-//
-//        $mess['auth_code'] = $user->keycode;
-//        $mess['message_cashier'] = 'Отсканируйте QR код из телеграм бота или введите его руками';
-//
-//        LogService::apiLogs(1, json_encode($mess, JSON_UNESCAPED_UNICODE));
-//
-//        return $this->asJson($mess);
-//    }
-
-    public function actionSendMessage() {
-        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
-        $data = file_get_contents('php://input');
-        $result = json_decode($data, true);
-
-        $__API_PARAMS = ['store_id', 'seller_id', 'phone'];
-
-        foreach ($__API_PARAMS as $paramName) {
-            if (empty($result[$paramName])) {
-
-                if ($paramName != 'phone') {
-                    LogService::apiErrorLog(json_encode(["error_id" => 1, "error" => "$paramName is required"], JSON_UNESCAPED_UNICODE));
-                }
-
-                return $this->asJson(["error" => "$paramName is required"]);
-            }
-        }
-
-        $phone = ClientHelper::phoneClear($result['phone']);
-        if (!ClientHelper::phoneVerify($phone)) {
-            return $this->asJson(["error_id" => 0.2, "error" => "phone is required"]);
-        }
-        $result['phone'] = $phone;
-
-        $user = Users::find()->where(['phone' => $result['phone']])->andWhere(['phone_true' => '1'])->andWhere(['black_list' => '0'])->one();
-        if (!$user) {
-            $mess["error"] = "Покупателя " . $result['phone'] . " нет в бонусной программе!";
-
-            return $this->asJson($mess);
-        }
-
-        $mess["message_cashier"] = "Звонок-последние 4 цифры телефона";
-
-        $userAuthCallLog = UsersAuthCallLog::find()->select(['COUNT(*) as cnt'])->where(['phone' => $result['phone']])
-            ->andWhere(['store_id' => $result['store_id']])->andWhere(['>=', 'date', date('Y-m-d H:i:s', strtotime('-10 minutes'))])->one();
-
-        $cnt = $userAuthCallLog ? $userAuthCallLog->cnt : 1;
-
-        $keycode = '';
-
-        if ($cnt < 2) {
-            $body = @file_get_contents("https://sms.ru/code/call?phone=" . $result['phone'] . "&api_id=4DFE45F9-1897-79C0-6872-08F05D6B7FA4&ip=" . $_SERVER["REMOTE_ADDR"]);
-            $json_res = json_decode($body, true, 512, JSON_UNESCAPED_UNICODE);
-            if ($json_res["status"] == "OK") {
-                $keycode = $json_res["code"];
-                $user->keycode = '' . $keycode;
-                $user->password = ClientHelper::generatePassword(8);;
-                $user->save();
-                if ($user->getErrors()) {
-                    LogService::apiErrorLog(json_encode(["error_id" => 3.1415, "error" => $user->getErrors()], JSON_UNESCAPED_UNICODE));
-                    return $this->asJson(["error_id" => 3.1415, "error" => $user->getErrors()]);
-                }
-                $mess["auth_code"] = $keycode;
-                $mess["message_cashier"] = "Попытка:$cnt Звонок клиенту! последние 4 цифры номера";
-            }
-        } elseif ($cnt > 2) {
-            $mess["message_cashier"] = "Попытка $cnt -извиняемся перед клиентом";
-        }
-        $name = "$keycode Попытка $cnt  " . $_SERVER["REMOTE_ADDR"];
-        $userAuthCallLog = new UsersAuthCallLog;
-        $userAuthCallLog->date = date('Y-m-d H:i:s');
-        $userAuthCallLog->store_id = $result['store_id'];
-        $userAuthCallLog->seller_id = $result['seller_id'];
-        $userAuthCallLog->phone = $result['phone'];
-        $userAuthCallLog->name = $name;
-        $userAuthCallLog->save();
-        if ($userAuthCallLog->getErrors()) {
-            LogService::apiErrorLog(json_encode(["error_id" => 4.15, "error" => $userAuthCallLog->getErrors()], JSON_UNESCAPED_UNICODE));
-            return $this->asJson(["error_id" => 4.15, "error" => $userAuthCallLog->getErrors()]);
-        }
-
-        Yii::info("keykod={$user->keycode} store_id={$result['store_id']} seller_id={$result['seller_id']} phone={$result['phone']} $name", self::LOG_CATEGORY_BONUS);
-
-        $mess["timeout"] = 15;
-
-        LogService::apiLogs(1, json_encode($mess, JSON_UNESCAPED_UNICODE));
-
-        return $this->asJson($mess);
-    }
-
-//        $mess["message_cashier"] = "Звонок-последние 4 цифры телефона";
-//
-//        $userAuthCallLog = UsersAuthCallLog::find()->select(['COUNT(*) as cnt'])->where(['phone' => $result['phone']])
-//            ->andWhere(['store_id' => $result['store_id']])->andWhere(['>=', 'date', date('Y-m-d H:i:s', strtotime('-10 minutes'))])->one();
-//
-//        $cnt = $userAuthCallLog ? $userAuthCallLog->cnt : 1;
-//
-//        if ($cnt < 2) {
-//            $body = @file_get_contents("https://sms.ru/code/call?phone=" . $result['phone'] . "&api_id=4DFE45F9-1897-79C0-6872-08F05D6B7FA4&ip=" . $_SERVER["REMOTE_ADDR"]);
-//            $json_res = json_decode($body, true, 512, JSON_UNESCAPED_UNICODE);
-//
-//            if ($json_res["status"] == "OK") {
-//                $keycode = $json_res["code"];
-//                $user->keycode = '' . $keycode;
-//                $user->password = ClientHelper::generatePassword(8);;
-//                $user->save();
-//
-//                if ($user->getErrors()) {
-//
-//                    LogService::apiErrorLog(json_encode(["error_id" => 3, "error" => $user->getErrors()], JSON_UNESCAPED_UNICODE));
-//
-//                    return $this->asJson(["error_id" => 3, "error" => $user->getErrors()]);
-//                }
-//
-//                $mess["auth_code"] = $keycode;
-//                $mess["message_cashier"] = "Попытка:$cnt Звонок клиенту! последние 4 цифры номера";
-//            }
-//        } else if ($cnt > 2) {
-//            $mess["message_cashier"] = "Попытка $cnt -извиняемся перед клиентом";
-//        }
-//
-//        $name = "$keycode Попытка $cnt  " . $_SERVER["REMOTE_ADDR"];
-//        $userAuthCallLog = new UsersAuthCallLog;
-//        $userAuthCallLog->date = date('Y-m-d H:i:s');
-//        $userAuthCallLog->store_id = $result['store_id'];
-//        $userAuthCallLog->seller_id = $result['seller_id'];
-//        $userAuthCallLog->phone = $result['phone'];
-//        $userAuthCallLog->name = $name;
-//        $userAuthCallLog->save();
-//        if ($userAuthCallLog->getErrors()) {
-//
-//            LogService::apiErrorLog(json_encode(["error_id" => 4, "error" => $userAuthCallLog->getErrors()], JSON_UNESCAPED_UNICODE));
-//
-//            return $this->asJson(["error_id" => 4, "error" => $userAuthCallLog->getErrors()]);
-//        }
-//
-
-    public function actionSaveClientInfo()
-    {
-        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
-        $data = file_get_contents('php://input');
-        $result = json_decode($data, true);
-
-        $fl = date('_Y_m_d__H_i_s_');
-        file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . $result['phone']);
-        file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-
-
-        $__API_PARAMS = ['store_id', 'seller_id', 'phone']; // first_name, second_name, sex, birth_day, referral_id, comment, events
-
-        foreach ($__API_PARAMS as $paramName) {
-            if (empty($result[$paramName])) {
-
-                if ($paramName != 'phone') {
-                    LogService::apiErrorLog(json_encode(["error_id" => 1, "error" => "$paramName is required"], JSON_UNESCAPED_UNICODE));
-                }
-
-                return $this->asJson(["error_id" => 1, "error" => "$paramName is required"]);
-            }
-        }
-        file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        $phone = ClientHelper::phoneClear($result['phone']);
-        file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        if (!ClientHelper::phoneVerify($phone)) {
-            file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            return $this->asJson(["error_id" => 1.2, "error" => "phone is required"]);
-        }
-        $result['phone'] = $phone;
-
-        $source = $result["source"] ?? 0;
-        $store_id = $result["store_id"];
-        $store_id_guid = $store_id;
-        $seller_id = $result["seller_id"];
-        $phone = $result["phone"];
-        $first_name = $result["first_name"] ?? "";
-        $second_name = $result["second_name"] ?? "";
-        $sex2 = $result["sex"] ?? "";
-        $birth_day = $result["birth_day"] ?? "";
-        $referral_id = $result["referral_id"] ?? null;
-        $comment = $result["comment"] ?? "";
-        $events = $result["events"] ?? [];
-        $sex = "man";
-        if ($sex2 == "male") {
-            $sex = "man";
-        }
-        if ($sex2 == "female") {
-            $sex = "women";
-        }
-//        if ($referral_phone == $phone) {
-//            $referral_phone = "";
-//        }
-
-        $mess = [];
-        file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        /* @var $user Users */
-        $user = Users::find()->where(['phone' => $result['phone']])->andWhere(['phone_true' => '1'])->andWhere(['black_list' => '0'])->one();
-        file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        if ($user) {
-            file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            $user->referral_id = $referral_id != $user->id ? $referral_id : null;
-            $user->pol = $sex;
-            $user->bdate = $birth_day;
-            $user->name = "$first_name $second_name";
-            $user->comment = $comment;
-            $user->password = ClientHelper::generatePassword(8);
-            $user->keycode = '' . rand(1000, 9999);
-            $user->source = $source == 2 ? 1 : 0;
-            $user->save(); // иначе не пройдём валидацию, т.к. множество полей в бд не заполнены.
-            file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            if ($user->getErrors()) {
-                file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-
-                LogService::apiErrorLog(json_encode(["error_id" => 2, "error" => $user->getErrors()], JSON_UNESCAPED_UNICODE));
-
-                return $this->asJson(["error_id" => 2, "error" => $user->getErrors()]);
-            }
-            file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            $userEventOld = UsersEvents::find()->where(['phone' => $phone])->orderBy(['date_add' => SORT_ASC])->one();
-            if ($userEventOld && $userEventOld->date_add < date('Y-m-d H:i:s', time() - 2 * 86400)) { // Дата добавление последнего события не старше двух дней
-                file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-                $mess["result"] = true;
-                $mess["message_cashier"] = "Возможность внесения памятных дат ограничена";
-
-                LogService::apiLogs(1, json_encode($mess, JSON_UNESCAPED_UNICODE));
-
-                return $this->asJson($mess);
-            }
-            file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-
-            $nomer_event = 1;
-            $dates = [];
-
-            foreach ($events as $k => $mass) {
-                $date = $mass["date"] ?? '';
-                $event_id = intval($mass['event_id'] ?? 0);
-
-                $datea = explode("-", $date);
-                $date_end = date("Y", time() + self::$YEAR_PERIOD * 86400) . "-" . $datea[1] . "-" . $datea[2];
-                $userEvent2 = UsersEvents::find()->where(['phone' => $phone])->andWhere(['date_day' => $datea[2]])->andWhere(['date_month' => $datea[1]])->one();
-                file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-                if ($userEvent2) {
-                    $userEvent2->delete();
-                }
-                $userEvent3 = new UsersEvents;
-                $userEvent3->number = $nomer_event;
-                $userEvent3->date = $date;
-                $userEvent3->tip_id = $event_id;
-                $userEvent3->phone = $phone;
-                $userEvent3->date_day = $datea[2];
-                $userEvent3->date_month = $datea[1];
-                $userEvent3->date_add = date('Y-m-d H:i:s');
-                $userEvent3->tip = strval('???');
-                $userEvent3->name = 'М';
-                $userEvent3->sex = 'm';
-                $userEvent3->date_edit = date("Y-m-d H:i:s");
-                $userEvent3->date_edit_info = date("Y-m-d H:i:s");
-                file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-                $userEvent3->save(); // иначе не пройдём валидацию, т.к. множество полей в бд не заполнены.
-                if ($userEvent3->getErrors()) {
-                    file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-
-                    LogService::apiErrorLog(json_encode(["error_id" => 3, "error" => $userEvent3->getErrors()], JSON_UNESCAPED_UNICODE));
-
-                    return $this->asJson(["error_id" => 3, "error" => $userEvent3->getErrors()]);
-                }
-                $dates [] = $date;
-                $nomer_event++;
-            }
-        } else {
-            file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            $created_name = $seller_id;
-            $rand = rand(1000, 9999);
-            $name = "$first_name $second_name";
-            $pass = ClientHelper::generatePassword(8);
-            $product1 = Products1c::find()->select(['name'])->where(['tip' => 'admin'])->andWhere(['id' => $seller_id])->one();
-            $product2 = Products1c::find()->select(['name'])->where(['tip' => 'city_store'])->andWhere(['id' => $store_id])->one();
-
-            $created_name = $product1 ? $product1->name : '';
-            $created_store = $product2 ? $product2->name : '';
-
-            $store_id_new = ExportImportTable::find()->select(['entity_id'])->where(['entity' => 'city_store'])->andWhere(['export_id' => '1'])
-                ->andWhere(['export_val' => $store_id])->one();
-            $seller_id_new = ExportImportTable::find()->select(['entity_id'])->where(['entity' => 'admin'])->andWhere(['export_id' => '1'])
-                ->andWhere(['export_val' => $seller_id])->one();
-            if ($store_id_new) {
-                $store_id_int = $store_id_new->entity_id;
-            }
-            if ($seller_id_new) {
-                $seller_id_int = $seller_id_new->entity_id;
-            }
-            file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            Users::deleteAll(['phone' => $phone, 'phone_true' => '0']);
-            file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            $user2 = new Users;
-            $user2->source = $source;
-            $user2->pol = $sex;
-            $user2->keycode = strval($rand);
-            $user2->phone = $phone;
-            $user2->name = $name;
-            $user2->name_name = $first_name;
-            $user2->name_last = $second_name;
-            $user2->password = $pass;
-            $user2->phone_true = strval(1);
-            $user2->bdate = $birth_day;
-            $user2->referral_id = $referral_id;
-            $user2->comment = $comment;
-            $user2->created_id = $seller_id_int ?? 0;
-            $user2->created_name = $created_name;
-            $user2->seller_id = strval($seller_id);
-            $user2->store_id = $store_id_guid;
-            $user2->created_store_id = $store_id_int ?? 0;
-            $user2->created_store = $created_store;
-            $user2->date = date('Y-m-d H:i:s');
-            $user2->sale_store_id = $store_id_int ?? 0;
-            $user2->sale_store = '';
-            $user2->sms_info = 1;
-            $user2->reklama_info = 1;
-            $user2->info = '';
-            $setka_id = 1;
-            $user2->setka_id = $setka_id;
-            $user2->card = "" . ($phone * 2 + 1608 + $setka_id); // генерируем номер карты который зависит от номера сетки + ДР Тимура
-            $user2->save();  // иначе не пройдём валидацию, т.к. множество полей в бд не заполнены.
-            file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            if ($user2->getErrors()) {
-                file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-
-                LogService::apiErrorLog(json_encode(["error_id" => 4, "error" => $user2->getErrors()], JSON_UNESCAPED_UNICODE));
-
-                return $this->asJson(["error_id" => 4, "error" => $user2->getErrors()]);
-            }
-
-            file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            if ($store_id == '56524cb1-4763-11ea-8cce-b42e991aff6c') {
-                file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-                $admin_id = ClientHelper::getExportId($seller_id, "admin", 1);
-
-                $usersBonus = new UsersBonus;
-                $usersBonus->date = date('Y-m-d H:i:s');
-                $usersBonus->tip = 'plus';
-                $usersBonus->tip_sale = 'podarok';
-                $usersBonus->phone = $phone;
-                $usersBonus->name = "Приветственные бонусы посетителю сайта";
-                $usersBonus->store_id = $store_id_int ?? 0;
-                $usersBonus->site_id = 0;
-                $usersBonus->referal_id = 0;
-                $usersBonus->admin_id = $admin_id;
-                $usersBonus->price = 0;
-                $usersBonus->price_skidka = 0;
-                $usersBonus->bonus = 50;
-                $usersBonus->store_id_1c = $store_id;
-                $usersBonus->seller_id_1c = $seller_id;
-                $usersBonus->date_start = date('Y-m-d 08:00:00', strtotime('+1 day', strtotime($usersBonus->date)));
-                $usersBonus->date_end = date('Y-m-d H:i:s', strtotime('+1 week', strtotime($usersBonus->date_start)));
-                $usersBonus->save();
-                file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-                if ($usersBonus->getErrors()) {
-                    file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-                    LogService::apiErrorLog(json_encode(["error_id" => 5, "error" => $usersBonus->getErrors()], JSON_UNESCAPED_UNICODE));
-
-                    return $this->asJson(["error_id" => 5, "error" => $usersBonus->getErrors()]);
-                }
-            }
-        }
-
-        $mess["result"] = true;
-        $mess["message_cashier"] = "Данные клиента сохранены";
-
-        LogService::apiLogs(1, json_encode($mess, JSON_UNESCAPED_UNICODE));
-
-        file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__ . ' OK ', FILE_APPEND);
-        return $this->asJson($mess);
-    }
-
-    public function actionSale()
-    {
-        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
-        $data = file_get_contents('php://input');
-        $result = json_decode($data, true);
-        $resultTest = $result;
-        $fl = date('_Y_m_d__H_i_s_');
-        $json=json_encode($resultTest,JSON_UNESCAPED_UNICODE);
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '_info.json', PHP_EOL . '--' . $result['phone']);
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '_info.json', ' '.date("d.m.Y H:i:s",time()).' JSON: '.$json.'  ', FILE_APPEND);
-
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . $result['phone']);
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-
-        $__API_PARAMS = ['store_id', 'seller_id', 'phone', 'check_amount', 'check_id', 'check_name']; // items, auth_code, write_off_bonuses
-
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        foreach ($__API_PARAMS as $paramName) {
-
-            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            if (empty($result[$paramName])) {
-
-                if ($paramName != 'phone') {
-                    file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '-error-' . __LINE__, FILE_APPEND);
-                    LogService::apiErrorLog(json_encode(["error_id" => 1, "error" => "$paramName is required"], JSON_UNESCAPED_UNICODE));
-                }
-
-                return $this->asJson(["error_id" => 1, "error" => "$paramName is required"]);
-            }
-        }
-
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        $phone = ClientHelper::phoneClear($result['phone']);
-
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        if (!ClientHelper::phoneVerify($phone)) {
-
-            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '-error-' . __LINE__, FILE_APPEND);
-            return $this->asJson(["error_id" => 1.2, "error" => "phone is required"]);
-        }
-
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        $result['phone'] = $phone;
-        $result['items'] = $result['items'] ?? [];
-
-        $store_id = $result["store_id"];
-        $seller_id = $result["seller_id"];
-        $check_amount = $result["check_amount"];
-        $check_id = $result["check_id"];
-        $check_name = $result["check_name"];
-        $lid_id = $result["lid_id"] ?? 0;
-        $auth_code = $result['auth_code'] ?? 0;
-        $write_off_bonuses = intval($result["write_off_bonuses"] ?? 0); // только при продаже
-
-        $user = Users::find()->where(['phone' => $result['phone']])->andWhere(['phone_true' => '1'])->one();
-        $bonusLevels = BonusLevels::find()->where(['active' => 1])->indexBy('alias')->asArray()->all();
-        $bonusLevel = $user->bonus_level ?? "silver";
-        $cashback_rate = isset($bonusLevels[$bonusLevel]['cashback_rate'])
-            ? $bonusLevels[$bonusLevel]['cashback_rate'] / 100
-            : self::$FIRST_SALE_PROCENT;
-
-//        $referal_rate = isset($bonusLevels[$bonusLevel]['referal_rate'])
-//            ? $bonusLevels[$bonusLevel]['referal_rate'] / 100
-//            : self::$CREDIT_HIGH_PROCENT;
-
-        $bonus_rate = isset($bonusLevels[$bonusLevel]['bonus_rate'])
-            ? $bonusLevels[$bonusLevel]['bonus_rate'] / 100
-            : self::$FIRST_SALE_PROCENT;
-
-
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        $amount_real = 0;
-        $items_arr_no = array_values(ArrayHelper::map(
-            UniversalCatalogItem::find()->where(['catalog_alias' => 'unused_nomenclature'])->all(), 'guid', 'guid'));
-        $items_arr_no_bonus_writeoffs = array_values(ArrayHelper::map(
-            UniversalCatalogItem::find()->where(['catalog_alias' => 'non_bonusable_goods'])->all(), 'guid', 'guid'));
-
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        $summa_no = 0;
-        $summa_no_writeoffs = 0;
-        $amount_all = 0;
-
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        foreach ($result['items'] as $item) {
-            if (in_array($item["product_id"], $items_arr_no)) {
-                $summa_no = $summa_no + $item["price"] * $item["quantity"];
-            } else if (in_array($item["product_id"], $items_arr_no_bonus_writeoffs)) {
-                $summa_no_writeoffs = $summa_no_writeoffs + $item["price"] * $item["quantity"];
-            } else {
-                $amount_real = $amount_real + $item["price"] * $item["quantity"];
-            }
-            $amount_all = $amount_all + $item["price"] * $item["quantity"];
-        }
-
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-
-        $cnt = intval(Sales::find()->where(['phone' => $result['phone'], 'operation' => Sales::OPERATION_SALE])->count());
-
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-       // $max_procent = $cnt == 0 ? self::$FIRST_SALE_PROCENT : ($cnt == 1 ? self::$SECOND_SALE_PROCENT : self::$MAX_PROCENT);
-        $max_procent = $bonus_rate;
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        // если списывается в попытке больше бонусов чем может списаться -
-        $percent = $phone == "79049031399" ? 0.9 : $max_procent;
-        $write_off_bonuses_theory = round($amount_real * $percent);
-        if ($write_off_bonuses > $write_off_bonuses_theory) {
-            $write_off_bonuses = $write_off_bonuses_theory;
-        }
-        $user_balans = ClientHelper::getBonusBalance($phone);
-        if ($user_balans < $write_off_bonuses) {
-            $write_off_bonuses = $user_balans;
-        }
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        // сумма со скидкой
-        $summa_chek = $amount_all - $write_off_bonuses;
-        $baza_back = $amount_real + $summa_no_writeoffs - $write_off_bonuses;
-
-        $mess = [];
-
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-
-        /** @var $user Users */
-        if (!$user) {
-
-            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            $mess["error"] = "Покупателя " . $result['phone'] . " нет в бонусной программе!";
-
-            return $this->asJson($mess);
-        }
-        $user->burn_balans = max(0, $user->burn_balans - $write_off_bonuses);
-        // [balans - burn_balance, burn_balans] - показать клиенту что мы сожгли сжигаемый баланс
-
-// старая точка проверки кода
-//        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-//        if ($user->keycode != strval($auth_code)) {
-//
-//            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '-auth_code not valid-' . __LINE__ . ' keycode ' .$user->keycode . '|  auth_code ' . strval($auth_code), FILE_APPEND);
-//            return $this->asJson(['error' => 'auth_code not valid']);
-//        }
-
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        $user_id = $user->id;
-//        $keycode = $user->keycode;
-//        $name = $user->name;
-//        $referral_id = $user->referral_id;
-//        $sale_avg_price = $user->sale_avg_price;
-        $sale_price = $user->sale_price;
-        $sale_cnt = $user->sale_cnt;
-//        if ($referral_id == $user_id) {
-//            $referral_id = 0;
-//        }
-        $ip = $_SERVER['REMOTE_ADDR'];
-
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-
-
-        $store_id_1c = $store_id;
-        $site_id = 0;
-
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        // получаем внутренний ID продаца - сотрудника из таблицы admin
-        $admin_id = ClientHelper::getExportId($seller_id, "admin", 1);
-        // получаем внутренний ID продаца - сотрудника из таблицы admin
-        $store_id = ClientHelper::getExportId($store_id_1c, "city_store", 1);
-
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        $writeOffAlready = false;
-        if (!empty($lid_id)) {
-
-            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            $writeOffAlready = UsersBonus::find()->where(['lid_id' => $lid_id, 'phone' => $phone, 'tip_sale' => 'sale', 'tip' => 'minus'])->one() != null;
-        }
-
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-
-        $user_balans_new = $user_balans;
-        if ($write_off_bonuses && !$writeOffAlready) {
-            // Проверка кода только при списании
-            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            if ($user->keycode != strval($auth_code)) {
-
-                file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '-auth_code not valid-' . __LINE__ . ' keycode ' .$user->keycode . '|  auth_code ' . strval($auth_code), FILE_APPEND);
-                return $this->asJson(['error' => 'auth_code not valid']);
-            }
-
-            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            $user_balans_new = $user_balans - $write_off_bonuses;
-            $name_b = "Спиcание бонусов по чеку $check_name";
-            $usersBonus = new UsersBonus;
-            $usersBonus->date = date('Y-m-d H:i:s');
-            $usersBonus->tip = 'minus';
-            $usersBonus->tip_sale = 'sale';
-            $usersBonus->phone = $phone;
-            $usersBonus->name = $name_b;
-            $usersBonus->check_id = $check_id;
-            $usersBonus->store_id = $store_id;
-            $usersBonus->ip = $ip;
-            $usersBonus->site_id = $site_id; // ???
-            $usersBonus->referal_id = 0;// $referal_id;
-            $usersBonus->admin_id = $admin_id;
-            $usersBonus->price = $summa_chek;
-            $usersBonus->price_skidka = $write_off_bonuses;
-            $usersBonus->bonus = $write_off_bonuses;
-            $usersBonus->store_id_1c = $store_id_1c;
-            $usersBonus->seller_id_1c = $seller_id;
-            $usersBonus->user_id = $user_id;             // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-            $usersBonus->lid_id = $lid_id;               // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-            $usersBonus->date_start = $usersBonus->date; // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-            $usersBonus->date_end = $usersBonus->date;   // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-            $usersBonus->date_dell = $usersBonus->date;  // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-
-            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-
-            $usersBonus->save(); // иначе не пройдём валидацию, т.к. множество полей в бд не заполнены.
-
-            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-
-            if ($user->first_minus_balance === null) {
-
-                file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-                $user->first_minus_balance = $usersBonus->date;
-                $user->save();
-
-                file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            }
-
-            if ($usersBonus->getErrors()) {
-
-                file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-
-                LogService::apiErrorLog(json_encode(["error_id" => 4, "error" => $usersBonus->getErrors()], JSON_UNESCAPED_UNICODE));
-
-                file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-                return $this->asJson(["error_id" => 4, "error" => $usersBonus->getErrors()]);
-            }
-
-            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            Yii::info("MINUS write_off_bonuses={$write_off_bonuses}", self::LOG_CATEGORY_BONUS);
-        }
-        //начисляем кэшбек клиенту 10% от покупки - с базы за вычитом бонусов которые он списывает
-
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        $userFound = Users::find()->where(['phone' => $result['phone']])->one();
-        /** @var $userFound Users */
-        $salesCount = -1; /* Из-за нулевого значения по умолчанию куча клиентов получило бонус 20% за покупку */
-
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        if ($userFound && $userFound->telegram_created_at) {
-            $salesCount = intval(Sales::find()->where(['phone' => $result['phone'], 'operation' => Sales::OPERATION_SALE])
-                ->andWhere(['>=', 'date', $userFound->telegram_created_at])->count());
-
-            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        }
-        $credit_procent_index = $userFound && $userFound->source > 0 && $salesCount == 0 ? 1 : 0;
-
-
-        $back10 = $back20 = 0;
-        $back1 = $back = round($baza_back * $cashback_rate);
-        $nm = "Возврат с покупки " . (100 * $cashback_rate) . "% $check_name сумма чека $check_amount";
-
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        $userBonus2 = UsersBonus::find()->where(['phone' => $phone])->andWhere(['check_id' => $check_id])->andWhere(['site_id' => $site_id])
-            ->andWhere(['store_id' => $store_id])->andWhere(['tip' => 'plus'])->andWhere(['bonus' => $back])->andWhere(['name' => $nm])->one();
-        if (!$userBonus2) {
-
-            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            $user_balans_new += $back;
-
-            $userBonus2 = new UsersBonus;
-            $userBonus2->tip = 'plus';
-            $userBonus2->tip_sale = 'sale';
-            $userBonus2->date = date('Y-m-d H:i:s');
-            $userBonus2->date_start = date('Y-m-d H:i:s', strtotime('+1 day', time()));
-            $userBonus2->date_end = date('Y-m-d H:i:s', strtotime('+' . self::$YEAR_PERIOD . ' day', time()));
-            $userBonus2->phone = $phone;
-            $userBonus2->name = $nm;
-            $userBonus2->check_id = $check_id;
-            $userBonus2->store_id = $store_id;
-            $userBonus2->bonus = $back;
-            $userBonus2->ip = $ip;
-            $userBonus2->site_id = $site_id;
-            $userBonus2->referal_id = 0; // $referal_id;
-            $userBonus2->admin_id = $admin_id;
-            $userBonus2->price = $summa_chek;
-            $userBonus2->store_id_1c = $store_id_1c;
-            $userBonus2->seller_id_1c = $seller_id;
-            $userBonus2->user_id = $user_id;
-            $userBonus2->lid_id = $lid_id;
-            $userBonus2->price_skidka = 0;
-            $userBonus2->date_dell = $userBonus2->date_end;
-
-            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            $userBonus2->save();
-
-            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            if ($userBonus2->getErrors()) {
-
-                file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-
-                LogService::apiErrorLog(json_encode(["error_id" => 5, "error" => $userBonus2->getErrors()], JSON_UNESCAPED_UNICODE));
-
-                return $this->asJson(["error_id" => 5, "error" => $userBonus2->getErrors()]);
-            } else {
-                $back10 = $back;
-            }
-
-            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            Yii::info("PLUS bonus={$back}", self::LOG_CATEGORY_BONUS);
-
-            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            if ($credit_procent_index) {
-                file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-                $back = round($baza_back * self::$CREDIT_HIGH_PROCENT_PART20);
-                $nm = "Возврат с покупки " . (100 * self::$CREDIT_HIGH_PROCENT_PART20) . "% $check_name сумма чека $check_amount";
-
-                $user_balans_new += $back;
-
-                $userBonus2 = new UsersBonus();
-                $userBonus2->tip = 'plus';
-                $userBonus2->tip_sale = 'sale';
-                $userBonus2->date = date('Y-m-d H:i:s');
-                $userBonus2->date_start = date('Y-m-d H:i:s', strtotime('+1 day', time()));
-                $userBonus2->date_end = date('Y-m-d H:i:s', strtotime('+3 month', time()));
-                $userBonus2->phone = $phone;
-                $userBonus2->name = $nm;
-                $userBonus2->check_id = $check_id;
-                $userBonus2->store_id = $store_id;
-                $userBonus2->bonus = $back;
-                $userBonus2->ip = $ip;
-                $userBonus2->site_id = $site_id;
-                $userBonus2->referal_id = 0;
-                $userBonus2->admin_id = $admin_id;
-                $userBonus2->price = $summa_chek;
-                $userBonus2->store_id_1c = $store_id_1c;
-                $userBonus2->seller_id_1c = $seller_id;
-                $userBonus2->user_id = $user_id;
-                $userBonus2->lid_id = $lid_id;
-                $userBonus2->price_skidka = 0;
-                $userBonus2->date_dell = $userBonus2->date_end;
-
-                file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-                $userBonus2->save();
-
-                file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-                if ($userBonus2->getErrors()) {
-
-                    file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-
-                    LogService::apiErrorLog(json_encode(["error_id" => 5.2, "error" => $userBonus2->getErrors()], JSON_UNESCAPED_UNICODE));
-
-                    return $this->asJson(["error_id" => 5.2, "error" => $userBonus2->getErrors()]);
-                } else {
-                    $back20 = $back;
-                }
-                if ($userFound->telegram_created_at == null) {
-                    $userFound->telegram_created_at = date("Y-m-d H:i:s");
-
-                    file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-                    $userFound->save();
-                    if ($userFound->getErrors()) {
-
-                        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-
-                        LogService::apiErrorLog(json_encode(["error_id" => 5.3, "error" => $userFound->getErrors()], JSON_UNESCAPED_UNICODE));
-
-                        return $this->asJson(["error_id" => 5.3, "error" => $userFound->getErrors()]);
-                    }
-                }
-
-                $notifiableUser = new NotifiableUser;
-                $notifiableUser->phone = $phone;
-                $notifiableUser->type = "first_given_bonus";
-                $notifiableUser->data = "" . ($back1 + $back);
-
-                file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-                $notifiableUser->save();
-
-                file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-                if ($notifiableUser->getErrors()) {
-
-                    file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-                    return $this->asJson(["error_id" => 5.4, "error" => $notifiableUser->getErrors()]);
-                }
-            }
-        }
-
-//        /////// Добавляем бонусов рефералу
-//        if ($referal_id && $back) {
-//            $name = "Вознаграждение за приведенного друга";
-//            $referalBonus = UsersBonus::find()->where(['phone' => $phone])->andWhere(['referal_id' => $referal_id])->andWhere(['tip' => 'plus'])->one(); // phone = referal_id ???
-//            if (!$referalBonus) {
-//                $referalBonus = new UsersBonus;
-//                $referalBonus->tip = 'plus';
-//                $referalBonus->tip_sale = 'referal';
-//                $referalBonus->date = date('Y-m-d H:i:s');
-//                $referalBonus->date_start = date('Y-m-d H:i:s', strtotime('+1 day', time()));
-//                $referalBonus->date_end = date('Y-m-d H:i:s', strtotime('+' . self::$YEAR_PERIOD . ' day', time()));
-//                $referalBonus->phone = $phone;
-//                $referalBonus->name = $name;
-//                $referalBonus->check_id = $check_id;
-//                $referalBonus->store_id = $store_id;
-//                $referalBonus->bonus = $back;
-//                $referalBonus->ip = $ip;
-//                $referalBonus->site_id = $site_id; // ??? $user_id_referal
-//                $referalBonus->referal_id = $referal_id; // ???
-//                $referalBonus->admin_id = $admin_id;
-//                $referalBonus->price = $summa_chek;
-//                $referalBonus->store_id_1c = $store_id_1c;
-//                $referalBonus->seller_id_1c = $seller_id;
-//                $referalBonus->save();
-//                if ($referalBonus->getErrors()) {
-//                    return $this->asJson(["error_id" => 3, "error" => $referalBonus->getErrors()]);
-//                }
-//            }
-//        }
-
-        ///////
-//        $itogo = 0;
-//        foreach ($result["items"] as $k => $mass) {
-//            $seller_id_item = $mass["seller_id"];
-//            $product_id = $mass["product_id"];
-//            $price = $mass["price"];
-//            $quantity = $mass["quantity"];
-//            $sm = $price * $quantity;
-//            //$info .=" id=$product_id ($quantity шт. x $price руб.)  = $sm руб.,";
-//            $itogo += $sm;
-//
-//            //получаем внутренний ID товара
-//            $item_id = ClientHelper::get_export_id($product_id, "products",1);
-//
-//            //товары к продаже
-//            $salesItem = new SalesItems;
-//            $salesItem->date = date('Y-m-d H:i:s');
-//            $salesItem->phone = $phone;
-//            $salesItem->check_id = $check_id;
-//            $salesItem->store_id = $store_id;
-//            $salesItem->store_id_1c = $store_id_1c;
-//            $salesItem->seller_id = $seller_id_item;
-//            $salesItem->admin_id = $admin_id;
-//            $salesItem->id_1c = $product_id;
-//            $salesItem->item_id = $item_id;
-//            $salesItem->kol = $quantity;
-//            $salesItem->summa = $sm;
-//            $salesItem->referal_id = 0; // $referal_id;
-//            $salesItem->color_id = 0; // $color_id ???
-//            $salesItem->lid_id = $lid_id;
-//            $salesItem->complect_id = 0;                 // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-//            $salesItem->name = '???';                    // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-//            $salesItem->skidka = $mass['discount'] ?? 0; // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-//            $salesItem->vozvrat = 0;                     // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-//            $salesItem->save();
-//            if ($salesItem->getErrors()) {
-//                return $this->asJson(["error_id" => 4, "error" => $salesItem->getErrors()]);
-//            }
-//        }
-        // sale_avg_price sale_price
-        $sale_price += $check_amount;
-        $sale_avg_price = round($sale_price / ($sale_cnt + 1));
-
-        $user->keycode = "" . rand(1000, 9999);
-        $user->password = ClientHelper::generatePassword(8);
-        $user->date_last_sale = date('Y-m-d H:i:s');
-        $user->sale_cnt = $sale_cnt + 1;
-        if ($user->sale_cnt == 1) {
-            $user->date_first_sale = $user->date_last_sale;
-        }
-        $user->sale_store_id = $store_id;
-        $user->sale_price = $sale_price;
-        $user->sale_avg_price = $sale_avg_price;
-        $user->check_id_last_sale = $check_id;
-        if (!$user->date) {
-            $user->date = (new DateTime('now', new DateTimeZone('Europe/Moscow')))->format('Y-m-d H:i:sP');
-        }
-        $user->balans = ClientHelper::getBonusBalance($phone);
-
-//        $user->email_old = "example@example.ru"; // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-//        $user->phone_old = "71111111111";        // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-//        $user->check_id_forgot = "???";          // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-//        $user->sid_forgot = "???";               // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-//        $user->alerts_balans = "???";            // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-//        $user->alerts_date = "???";              // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-//        $user->alerts_reklama = "???";           // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-//        $user->seller_id = "???";                // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        $user->save();
-        if ($user->getErrors()) {
-
-
-            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            LogService::apiErrorLog(json_encode(["error_id" => 6, "error" => $user->getErrors()], JSON_UNESCAPED_UNICODE));
-
-            Yii::info("BEFORE END errors=" . json_encode($user->getErrors(), JSON_UNESCAPED_UNICODE), self::LOG_CATEGORY_BONUS);
-
-            return $this->asJson(["error_id" => 6, "error" => $user->getErrors()]);
-        } else {
-            $this->updateUserBonusLevel($user, $sale_price, $check_id, $check_name);
-        }
-        Yii::info("BEFORE END", self::LOG_CATEGORY_BONUS);
-//        $itogo -= $write_off_bonuses;
-
-//        // продажа заносим в таблицу
-//        $sale = new Sales;
-//        $sale->date = date("Y-m-d H:i:s");
-//        $sale->phone = $phone;
-//        $sale->operation = 'Продажа';
-//        $sale->store_id = $store_id;
-//        $sale->admin_id = $admin_id;
-//        $sale->seller_id = $seller_id;
-//        $sale->store_id_1c = $store_id_1c;
-//        $sale->id = $check_id;
-//        $sale->number = $check_name;
-//        $sale->summ = $amount_all;
-//        $sale->skidka = $write_off_bonuses;
-//        $sale->status = "???";        // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-//        $sale->payments = "???";      // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-//        $sale->pay_arr = "???";       // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-//        $sale->sales_check = "???";   // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-//        $sale->order_id = "";         // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-//        $sale->terminal_id = "???";   // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-//        $sale->terminal = "???";      // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-//        $sale->kkm_id = "???";        // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-//        $sale->held = 0;              // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-//        $sale->date_up = $sale->date; // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении
-//        $sale->save();
-//        if ($sale->getErrors()) {
-//            return $this->asJson(["error_id" => 6, "error" => $sale->getErrors()]);
-//        }
-
-        $mess["result"] = true;
-        $mess["message_cashier"] = "Бонусы списаны";
-        $mess["user_balans_old"] = $user_balans;
-        $mess["user_balans_new"] = $user_balans_new;
-        $mess["user_balans_actual"] = $user->balans;
-        LogService::apiLogs(1, json_encode($mess, JSON_UNESCAPED_UNICODE));
-        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__ . ' OK ', FILE_APPEND);
-
-        $totalBonus = $back10 + $back20;
-
-        $input = [
-            'phone' => $phone,
-            'bonusCount' => $totalBonus,
-            'purchaseDate' => date("Y-m-d H:i:s"),
-            'orderId' => $check_id,
-        ];
-
-        $userBonusSendToTgLogs = new UserBonusSendToTgLogs;
-        $userBonusSendToTgLogs->input_hash = md5(Json::encode($input));
-        $userBonusSendToTgLogs->input = Json::encode($input);
-        $userBonusSendToTgLogs->check_id = $check_id;
-        $userBonusSendToTgLogs->phone = $phone;
-        $userBonusSendToTgLogs->bonusCount = $totalBonus;
-        $userBonusSendToTgLogs->status = 1;
-        $userBonusSendToTgLogs->date = date('Y-m-d H:I:s');
-        $userBonusSendToTgLogs->save();
-        if ($userBonusSendToTgLogs->getErrors()) {
-            LogService::apiErrorLog(json_encode(["error_id" => 100.001, "error" => $userBonusSendToTgLogs->getErrors()], JSON_UNESCAPED_UNICODE));
-        }
-        Yii::$app->queue->push(new SendBonusInfoToSiteJob($input));
-
-//        SiteService::notifySiteAboutBonuses($phone, $totalBonus, date("Y-m-d H:i:s"), $check_id);
-
-        return $this->asJson($mess);
-    }
-
-    /**
-     * Создаёт новую запись в таблице UsersBonusLevels.
-     *
-     * @param Users $user
-     * @param string $bonusLevel
-     * @param string $check_id
-     * @param string $check_name
-     * @param string $createdAt
-     * @return bool
-     */
-    protected function createBonusHistoryRecord($user, $bonusLevel, $check_id, $check_name, $createdAt)
-    {
-        $bonusRecord = new UsersBonusLevels();
-        $bonusRecord->phone = $user->phone;
-        $bonusRecord->user_id = $user->id;
-        $bonusRecord->bonus_level = $bonusLevel;
-        $bonusRecord->date_from = $createdAt;
-        $bonusRecord->check_id = $check_id;
-        $bonusRecord->check_name = $check_name;
-        $bonusRecord->active = 1;
-
-        if (!$bonusRecord->save()) {
-            LogService::apiErrorLog(
-                json_encode(["error_id" => 100, "error" => $bonusRecord->getErrors()], JSON_UNESCAPED_UNICODE)
-            );
-            return false;
-        }
-        return true;
-    }
-
-    /**
-     * Обновляет бонусный уровень пользователя.
-     *
-     * @param Users  $user       Модель пользователя.
-     * @param float  $sale_price Текущая сумма покупок.
-     * @param string $check_id   Идентификатор чека.
-     * @param string $check_name Имя (номер) чека.
-     */
-    protected function updateUserBonusLevel($user, $sale_price, $check_id, $check_name)
-    {
-        $bonusLevels = BonusLevels::find()
-            ->where(['active' => 1])
-            ->orderBy(['threshold' => SORT_ASC])
-            ->all();
-
-        $computedBonusLevel = null;
-        foreach ($bonusLevels as $level) {
-            if ($sale_price > $level->threshold) {
-                $computedBonusLevel = $level->alias;
-            }
-        }
-        $newBonusLevel = $computedBonusLevel ?? 'silver';
-
-        $existingHistoryLevel = UsersBonusLevels::find()
-            ->where(['or', ['phone' => $user->phone], ['user_id' => $user->id]])
-            ->andWhere(['active' => 1])
-            ->one();
-
-        $now = date('Y-m-d H:i:s');
-
-        if (empty($user->bonus_level) || $user->bonus_level !== $newBonusLevel) {
-            $user->bonus_level = $newBonusLevel;
-            if (!$user->save()) {
-                LogService::apiErrorLog(
-                    json_encode(["error_id" => 6.1, "error" => $user->getErrors()], JSON_UNESCAPED_UNICODE)
-                );
-            }
-
-            if ($existingHistoryLevel) {
-                $existingHistoryLevel->active = 0;
-                $existingHistoryLevel->date_to = $now;
-                if (!$existingHistoryLevel->save()) {
-                    LogService::apiErrorLog(
-                        json_encode(
-                            ["error_id" => 6.2, "error" => $existingHistoryLevel->getErrors()],
-                            JSON_UNESCAPED_UNICODE
-                        )
-                    );
-                }
-            }
-
-            $this->createBonusHistoryRecord($user, $newBonusLevel, $check_id, $check_name, $now);
-        }
-    }
-
-    public function actionGetClientInfo()
-    {
-        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
-        $data = file_get_contents('php://input');
-        $result = json_decode($data, true);
-
-        $__API_PARAMS = ['phone'];
-
-        foreach ($__API_PARAMS as $paramName) {
-            if (empty($result[$paramName])) {
-
-                if ($paramName != 'phone') {
-                    LogService::apiErrorLog(json_encode(["error_id" => 1, "error" => "$paramName is required"], JSON_UNESCAPED_UNICODE));
-                }
-
-                return $this->asJson(["error_id" => 1, "error" => "$paramName is required"]);
-            }
-        }
-
-        $phone = ClientHelper::phoneClear($result['phone']);
-        if (!ClientHelper::phoneVerify($phone)) {
-            return $this->asJson(["error_id" => 1.2, "error" => "phone is required"]);
-        }
-
-        $mess = [];
-        $user = Users::find()->select(['id', 'keycode', 'bonus_level', 'burn_balans', 'name', 'referral_id', 'bdate', 'comment', 'pol', 'extract(epoch FROM  date) as date'])
-            ->where(['phone' => $phone])->one();
-        if (!$user) {
-            $mess["error"] = "Покупателя " . $phone . " нет в бонусной программе!";
-
-            return $this->asJson($mess);
-        }
-        $name = explode(" ", $user->name);
-        $birth_day = $user->bdate;
-        $first_name = $name[0] ?? '';
-        $second_name = $name[1] ?? '';
-        $comment = $user->comment;
-        $pol = "male";
-        if ($user->pol == "women") {
-            $pol = "female";
-        }
-        // если с момента добавления клиента прошло не более 5 часов позволяем редактировать даты иначе запрещаем редактирование
-        if ($user->date > time() - 3600 * 5) {
-            $mess["birth_day_readonly"] = true;
-            $mess["events_readonly"] = false;
-        }
-        if ($birth_day) {
-            $mess["birth_day_readonly"] = true;
-        }
-
-        $data = UsersEvents::find()->where(['phone' => $phone])->orderBy(['date' => SORT_DESC])->all();
-        foreach ($data as $row) {
-            if (strlen($row->date_day) == 1) {
-                $row->date_day = "0" . $row->date_day;
-            }
-            if (strlen($row->date_month) == 1) {
-                $row->date_month = "0" . $row->date_month;
-            }
-            if (!isset($mess["events"])) {
-                $mess["events"] = [];
-            }
-            $mess["events"][] = ["date" => $row->date, "event_id" => $row->tip_id];
-        }
-
-        $user_balance = ClientHelper::getBonusBalance($phone);
-
-        $mess["result"] = true;
-        $mess["sex"] = $pol;
-        $mess["first_name"] = $first_name;
-        $mess["second_name"] = $second_name;
-        $mess["birth_day"] = $birth_day;
-        $mess["comment"] = $comment;
-        $mess["balance"] = $user_balance;
-        $mess["bonus_level"] = $user->bonus_level;
-        $mess["burn_balans"] = $user->burn_balans;
-
-        LogService::apiLogs(1, json_encode($mess, JSON_UNESCAPED_UNICODE));
-
-        return $this->asJson($mess);
-    }
-
-    public function actionReturn()
-    {
-        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
-        $data = file_get_contents('php://input');
-        $result = json_decode($data, true);
-
-        $__API_PARAMS = ['store_id', 'check_id']; // check_name, seller_id
-
-        foreach ($__API_PARAMS as $paramName) {
-            if (!isset($result[$paramName])) {
-
-                LogService::apiErrorLog(json_encode(["error_id" => 1, "error" => "$paramName is required"], JSON_UNESCAPED_UNICODE));
-
-                return $this->asJson(["error_id" => 1, "error" => "$paramName is required"]);
-            }
-        }
-
-        $store_id = $result["store_id"];
-//        $seller_id = $result["seller_id"] ?? '';
-        $check_id = $result["check_id"];
-//        $check_name = $result["check_name"] ?? '';
-
-        UsersBonus::deleteAll(['and', ['check_id' => $check_id],
-            ['>', 'date', date('Y-m-d H:i:s', strtotime('-3 day', time()))]]);
-
-        // api_logs... event: return, seller_id, when, check
-
-        LogService::apiLogs(1, json_encode(['Удачный возврат'], JSON_UNESCAPED_UNICODE));
-
-        return $this->asJson(['ok']);
-    }
-
-    public function actionAuthCodeFail()
-    {
-        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
-        $data = file_get_contents('php://input');
-        $result = json_decode($data, true);
-
-        $__API_PARAMS = [/*'store_id', 'seller_id',*/
-            'phone'];
-
-        foreach ($__API_PARAMS as $paramName) {
-            if (empty($result[$paramName])) {
-
-                if ($paramName != 'phone') {
-                    LogService::apiErrorLog(json_encode(["error_id" => 1, "error" => "$paramName is required"], JSON_UNESCAPED_UNICODE));
-                }
-
-                return $this->asJson(["error_id" => 1, "error" => "$paramName is required"]);
-            }
-        }
-
-        $phone = ClientHelper::phoneClear($result['phone']);
-        if (!ClientHelper::phoneVerify($phone)) {
-            return $this->asJson(["error_id" => 1.2, "error" => "phone is required"]);
-        }
-        $result['phone'] = $phone;
-
-//        $seller_id = $result['seller_id'];
-//        $store_id = $result['store_id'];
-
-        $user = Users::find()->where(['phone' => $result['phone']])->andWhere(['phone_true' => '1'])->one();
-        if (!$user) {
-            $mess["error"] = "Покупателя $phone нет в бонусной программе!";
-
-            return $this->asJson($mess);
-        }
-
-        $user->keycode = "" . rand(1000, 9999);
-        $user->save();
-
-        if ($user->getErrors()) {
-
-            LogService::apiErrorLog(json_encode(["error_id" => 3, "error" => $user->getErrors()], JSON_UNESCAPED_UNICODE));
-
-            return $this->asJson(["error_id" => 3, "error" => $user->getErrors()]);
-        }
-
-        $mess = [];
-        $mess["result"] = true;
-        // api_logs seller, store
-
-        LogService::apiLogs(1, json_encode($mess, JSON_UNESCAPED_UNICODE));
-
-        return $this->asJson($mess);
-    }
-
-    public function actionCurrentItems()
-    {
-        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
-        $data = file_get_contents('php://input');
-        $result = json_decode($data, true);
-
-        $__API_PARAMS = ['store_id', 'seller_id', 'phone', 'amount_no_discount', 'amount_to_pay'];
-
-        foreach ($__API_PARAMS as $paramName) {
-            if (empty($result[$paramName])) {
-
-                if ($paramName != 'phone') {
-                    LogService::apiErrorLog(json_encode(["error_id" => 1, "error" => "$paramName is required"], JSON_UNESCAPED_UNICODE));
-                }
-
-                return $this->asJson(["error_id" => 1, "error" => "$paramName is required"]);
-            }
-        }
-
-        $phone = ClientHelper::phoneClear($result['phone']);
-        if (!ClientHelper::phoneVerify($phone)) {
-            return $this->asJson(["error_id" => 1.2, "error" => "phone is required"]);
-        }
-        $result['phone'] = $phone;
-
-//        $store_id = $result["store_id"];
-//        $seller_id = $result["seller_id"];
-        $phone = $result["phone"];
-//        $check_amount = intval($result["check_amount"] ?? 0);
-//        $check_id = $result["check_id"] ?? '';
-//        $check_name = $result["check_name"] ?? '';
-//        $items = $result['items'] ?? [];
-
-        $mess = [];
-
-        $user = Users::find()->where(['phone' => $result['phone']])->andWhere(['phone_true' => '1'])->one();
-        if (!$user) {
-            $mess["error"] = "Покупателя $phone нет в бонусной программе!";
-
-            return $this->asJson($mess);
-        }
-
-//        $user_balans = ClientHelper::getBonusBalance($phone);
-//        $max = $itogo * self::$MAX_PROCENT;  // максимально можем разрешить списывать до 30 процентов от суммы заказа
-//        $max = ceil($max);
-//        $available_bonus = $user_balans;
-//        if ($available_bonus > $max) { // если баллов бонусов больше чем 30 процентов списываем по максимуму 30
-//            $available_bonus = $max;
-//        }
-//        $baza = $check_amount - $available_bonus;
-//        $back = ceil(self::$CREDIT_PROCENT * $baza);
-
-
-//        foreach ($items as $k => $mass) {
-//            $seller_id_item = $mass["seller_id"];
-//            $product_id = $mass["product_id"];
-//            $price = $mass["price"];
-//            $quantity = $mass["quantity"];
-//            $sm = $price * $quantity;
-//            $info .=" id=$product_id ($quantity шт. x $price руб.)  = $sm руб.,";
-//            $itogo += $sm;
-//
-//            //получаем внутренний ID товара
-//            $item_id = ClientHelper::getExportId($product_id, "products",1);
-//        }
-
-        LogService::apiLogs(1, json_encode($mess, JSON_UNESCAPED_UNICODE));
-
-        return $this->asJson(["NOT IMPLEMENTED"]);
-    }
-
-    public function actionGetSettings()
-    {
-        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
-        $data = file_get_contents('php://input');
-        $result = json_decode($data, true);
-
-        $__API_PARAMS = ['store_id', 'seller_id'];
-
-        foreach ($__API_PARAMS as $paramName) {
-            if (!isset($result[$paramName])) {
-
-                LogService::apiErrorLog(json_encode(["error_id" => 1, "error" => "$paramName is required"], JSON_UNESCAPED_UNICODE));
-
-                return $this->asJson(["error_id" => 1, "error" => "$paramName is required"]);
-            }
-        }
-
-        $store_id = $result['store_id'];
-        $seller_id = $result['seller_id'];
-
-        $mess = [];
-        $mess["result"] = true;
-        $mess["attempts_auth_code"] = 5; // Количество возможных попыток ввода кода подтверждения
-        $mess["list_events"] = [
-            ["id" => 1, "name" => "День рождения"],
-            ["id" => 2, "name" => "8 марта"],
-            ["id" => 3, "name" => "День матери"],
-            ["id" => 4, "name" => "День влюбленных"],
-            ["id" => 5, "name" => "День свадьбы"],
-            ["id" => 6, "name" => "Другое"]
-        ];
-        $mess["send_current_items"] = false; // Отправлять или нет текущие позиции чека
-
-        LogService::apiLogs(1, json_encode($mess, JSON_UNESCAPED_UNICODE));
-
-        return $this->asJson($mess);
-    }
-
-    public function actionGetUnusedNumenclatur() {
-        $items_arr_no = array_values(ArrayHelper::map(
-            UniversalCatalogItem::find()->where(['catalog_alias' => 'unused_nomenclature'])->all(), 'guid', 'guid'));
-        return $this->asJson(['unused_nomenclature' => $items_arr_no]);
-    }
-
-    public function actionGetContest001Participant() {
-        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
-
-        $request = Yii::$app->request->getRawBody();
-
-        try {
-            $result = Json::decode($request);
-        } catch (\Exception $ex) {
-            return $this->asJson(['error' => ['code' => 400, 'message' => 'Json body invalid']]);
-        }
-
-        if (!isset($result['phone'])) {
-            return $this->asJson(["error_id" => 1, "error" => "phone is required"]);
-        }
-
-        $phone = ClientHelper::phoneClear($result['phone']);
-        if (!ClientHelper::phoneVerify($phone)) {
-            return $this->asJson(["error_id" => 1.2, "error" => "phone is required"]);
-        }
-        $result['phone'] = $phone;
-
-        $mess = [];
-
-        $contestants = Contest001::find()->where(['phone' => $phone])->all();
-        if (count($contestants) > 0) {
-            $mess['is_participant'] = true;
-            $raffle_numbers = [];
-            foreach ($contestants as $contestant) {
-                $raffle_numbers[] = $contestant->number;
-            }
-            $mess['raffle_numbers'] = implode(', ', $raffle_numbers);
-        } else {
-            $mess['is_participant'] = false;
-        }
-
-        LogService::apiLogs(1, json_encode($mess, JSON_UNESCAPED_UNICODE));
-
-        return $this->asJson(['response' => $mess]);
-    }
-
-    public function actionAdd() {
-        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
-
-        $request = Yii::$app->request->getRawBody();
-
-        $requestTest = json_decode(\Yii::$app->getRequest()->getRawBody(), true);
-
-        $fl = date('_Y_m_d__H_i_s_');
-
-        file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', implode(',', $requestTest));
-        file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        try {
-            $result = Json::decode($request);
-            file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        } catch (\Exception $ex) {
-            file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            return $this->asJson(['error' => ['code' => 400, 'message' => 'Json body invalid']]);
-        }
-
-        $__API_PARAMS = ['phone', 'description', 'tip_sale', 'bonus', 'date_end'];
-
-        foreach ($__API_PARAMS as $paramName) {
-            if (!isset($result[$paramName])) {
-                file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-                if ($paramName != 'phone') {
-                    file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-                    LogService::apiErrorLog(json_encode(["error_id" => 1, "error" => "$paramName is required"], JSON_UNESCAPED_UNICODE));
-                }
-                file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-                return $this->asJson(["error_id" => 1, "error" => "$paramName is required"]);
-            }
-        }
-        file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        //
-        if (!in_array($result['tip_sale'], ['podarok', 'senat', 'nino802', 'sale', '14feb', '23feb', '8mar', 'quest001'])) {
-            file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            return $this->asJson(["error_id" => 1.1, "error" => "tip_sale не разрешён (podarok, senat, nino802)"]);
-        }
-
-        $phone = ClientHelper::phoneClear($result['phone']);
-        file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        if (!ClientHelper::phoneVerify($phone)) {
-            file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            return $this->asJson(["error_id" => 1.2, "error" => "phone is required"]);
-        }
-        $result['phone'] = $phone;
-
-        $stop = UsersStopList::find()->select(['phone'])->where(['phone' => $result['phone']])->one();
-        file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        if ($stop) {
-            file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            return $this->asJson(["error_id" => 4, "error" => 'Номер телефона числится в стоп листе']);
-        }
-
-        $bonus = min((int)$result['bonus'], 1000);
-        file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        $found = UsersBonus::find()->where(['phone' => $phone])->andWhere(['>=', 'date_start', date('Y-m-d H:i:s', time() - self::$YEAR_PERIOD * 86400)])
-            ->andWhere(['tip_sale' => $result['tip_sale']])->andWhere(['tip' => 'plus'])->andWhere(['bonus' => $bonus])->one();
-        file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        if ($found) {
-            file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            return $this->asJson(["error_id" => 3, "error" => 'Бонусы уже начисляли']);
-        }
-        file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        $userBonus = new UsersBonus;
-        $userBonus->phone = $phone;
-        $userBonus->name = $result['description'];
-        $userBonus->date = date('Y-m-d H:i:s');
-        $userBonus->site_id = 1;
-        $userBonus->setka_id = 1;
-        $userBonus->tip = 'plus';
-        $userBonus->tip_sale = $result['tip_sale'];
-        $userBonus->bonus = $bonus;
-
-        $userBonusDateStart = $result['date_start'] ?? $userBonus->date;
-        $userBonus->date_start = date('Y-m-d H:i:s', strtotime($userBonusDateStart));
-
-        $userBonusDateEnd = $result['date_end'];
-        $userBonus->date_end = date('Y-m-d H:i:s', strtotime($userBonusDateEnd));
-
-        $userBonus->save();
-        file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-        if ($userBonus->getErrors()) {
-            file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);
-            LogService::apiErrorLog(json_encode(["error_id" => 2, "error" => $userBonus->getErrors()], JSON_UNESCAPED_UNICODE));
-            return $this->asJson(["error_id" => 2, "error" => $userBonus->getErrors()]);
-        }
-        file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__ . ' OK', FILE_APPEND);
-        return $this->asJson(['response' => true]);
-    }
-}
+<?php\r
+\r
+namespace app\controllers;\r
+\r
+use yii_app\jobs\SendBonusInfoToSiteJob;\r
+use DateTime;\r
+use DateTimeZone;\r
+use Yii;\r
+use yii\helpers\ArrayHelper;\r
+use yii\helpers\Json;\r
+use yii_app\helpers\ClientHelper;\r
+use yii_app\records\BonusLevels;\r
+use yii_app\records\Contest001;\r
+use yii_app\records\ExportImportTable;\r
+use yii_app\records\MessagerUser;\r
+use yii_app\records\NotifiableUser;\r
+use yii_app\records\Products1c;\r
+use yii_app\records\Sales;\r
+use yii_app\records\Timetable;\r
+use yii_app\records\UniversalCatalogItem;\r
+use yii_app\records\UserBonusSendToTgLogs;\r
+use yii_app\records\Users;\r
+use yii_app\records\UsersAuthCallLog;\r
+use yii_app\records\UsersBonus;\r
+use yii_app\records\UsersBonusLevels;\r
+use yii_app\records\UsersEvents;\r
+use yii_app\records\UsersPhones;\r
+use yii_app\records\UsersStopList;\r
+use yii_app\records\Promocode;\r
+use yii_app\services\LogService;\r
+use yii_app\services\SiteService;\r
+\r
+class BonusController extends BaseController\r
+{\r
+    private const LOG_CATEGORY_BONUS = 'bonus.auth';\r
+\r
+    private static $YEAR_PERIOD = 366;\r
+    private static $FIRST_SALE_PROCENT = 0.1;\r
+    private static $SECOND_SALE_PROCENT = 0.15;\r
+    private static $MAX_PROCENT = 0.2;\r
+    private static $CREDIT_PROCENT = 0.1;\r
+    private static $CREDIT_HIGH_PROCENT = 0.3;\r
+    private static $CREDIT_HIGH_PROCENT_PART20 = 0.2;\r
+\r
+    const OUT_DIR =\r
+//        "/tmp";\r
+        "/var/www/erp24/api2/json"; // "/www/api2/json";\r
+// __DIR__ . "/../json"; //local\r
+\r
+    public function actionGetBonuses()\r
+    {\r
+        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;\r
+        $data = file_get_contents('php://input');\r
+        $result = json_decode($data, true);\r
+\r
+        $fl = date('_Y_m_d__H_i_s_');\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . $result['phone']);\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+\r
+        $__API_PARAMS = ['store_id', 'seller_id', 'phone']; // check_amount, items\r
+\r
+        foreach ($__API_PARAMS as $paramName) {\r
+            if (empty($result[$paramName])) {\r
+\r
+                if ($paramName != 'phone') {\r
+                    LogService::apiErrorLog(json_encode(["error_id" => 0, "error" => "$paramName is required"], JSON_UNESCAPED_UNICODE));\r
+                }\r
+\r
+                return $this->asJson(["error_id" => 0, "error" => "$paramName is required"]);\r
+            }\r
+        }\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+\r
+        $phone = ClientHelper::phoneClear($result['phone']);\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        if (!ClientHelper::phoneVerify($phone)) {\r
+            return $this->asJson(["error_id" => 0.2, "error" => "phone is required"]);\r
+        }\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        $result['phone'] = $phone;\r
+\r
+        $check_amount = intval($result['check_amount'] ?? 0);\r
+\r
+        $user = Users::find()->where(['phone' => $result['phone']])->andWhere(['phone_true' => '1'])->one();\r
+        $bonusLevels = BonusLevels::find()->where(['active' => 1])->indexBy('alias')->asArray()->all();\r
+        $bonusLevel = $user->bonus_level ?? "silver";\r
+\r
+        $bonus_rate = isset($bonusLevels[$bonusLevel]['bonus_rate'])\r
+            ? $bonusLevels[$bonusLevel]['bonus_rate'] / 100\r
+            : self::$FIRST_SALE_PROCENT;\r
+\r
+        $cashback_rate = isset($bonusLevels[$bonusLevel]['cashback_rate'])\r
+            ? $bonusLevels[$bonusLevel]['cashback_rate'] / 100\r
+            : self::$FIRST_SALE_PROCENT;\r
+\r
+        $mess = [];\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        // массив с id товарыми не участвующих в бонусной\r
+        $items_arr_no = array_values(ArrayHelper::map(\r
+            UniversalCatalogItem::find()->where(['catalog_alias' => 'unused_nomenclature'])->all(), 'guid', 'guid'));\r
+        $items_arr_no_bonus_writeoffs = array_values(ArrayHelper::map(\r
+            UniversalCatalogItem::find()->where(['catalog_alias' => 'non_bonusable_goods'])->all(), 'guid', 'guid'));\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+\r
+        $all_amount = 0;\r
+        $has_actions = false;\r
+        $summa_no = 0;\r
+        $summa_no_writeoffs = 0;\r
+        if (!empty($result["items"])) {\r
+            foreach ($result["items"] as $item) {\r
+                if (in_array($item["product_id"], $items_arr_no)) {\r
+                    $summa_no = $summa_no + $item["price"] * $item["quantity"];\r
+                    $has_actions = true;\r
+                } elseif (in_array($item["product_id"], $items_arr_no_bonus_writeoffs)) {\r
+                    $summa_no_writeoffs = $summa_no_writeoffs + $item["price"] * $item["quantity"];\r
+                }\r
+                $all_amount += $item["price"] * $item["quantity"];\r
+            }\r
+        }\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        $baza_nachislenie = $all_amount - $summa_no;\r
+\r
+        $check_amount = $check_amount - $summa_no;\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        //$cnt = intval(Sales::find()->where(['phone' => $result['phone'], 'operation' => Sales::OPERATION_SALE])->count());\r
+        //$max_procent = $cnt == 0 ? self::$FIRST_SALE_PROCENT : ($cnt == 1 ? self::$SECOND_SALE_PROCENT : self::$MAX_PROCENT);\r
+        $max_procent = $bonus_rate;\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        $percent = ($result['phone'] == "79049031399") ? 0.9 : $max_procent;\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        $userFound = Users::find()->where(['phone' => $result['phone']])->one();\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        /** @var $userFound Users */\r
+        $salesCount = -1; /* Из-за нулевого значения по умолчанию куча клиентов получило бонус 20% за покупку */\r
+        if ($userFound && $userFound->telegram_created_at) {\r
+            $salesCount = intval(Sales::find()->where(['phone' => $result['phone'], 'operation' => Sales::OPERATION_SALE])\r
+                ->andWhere(['>=', 'date', $userFound->telegram_created_at])->count());\r
+        }\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        $credit_procent = $userFound && $userFound->source > 0 && $salesCount == 0 ? self::$CREDIT_HIGH_PROCENT : $cashback_rate;\r
+        $will_be_credited_bonuses = $credit_procent * $baza_nachislenie;\r
+\r
+        $will_be_credited_bonuses = round($will_be_credited_bonuses);\r
+        $mess["will_be_credited_bonuses"] = $will_be_credited_bonuses;\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        $store_id = ClientHelper::getExportId($result['store_id'], "city_store", 1);\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        // Логи введённых номеров телефонов кассирами\r
+        $userPhone = UsersPhones::find()->where(['phone' => $result['phone']])->andWhere(['store_id' => $store_id])\r
+            ->andWhere(['seller_id' => $result['seller_id']])->one();\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        if (!$userPhone) {\r
+            $userPhone = new UsersPhones();\r
+            $userPhone->phone = $result['phone'];\r
+            $userPhone->store_id = $store_id;\r
+            $userPhone->store_guid = $result['store_id'];\r
+            $userPhone->seller_id = $result['seller_id'];\r
+        }\r
+        if (!$userPhone->store_guid) {\r
+            $userPhone->store_guid = $result['store_id'];\r
+        }\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        $userPhone->date = date('Y-m-d H:i:s');\r
+        $userPhone->save();\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        if ($userPhone->getErrors()) {\r
+            file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            LogService::apiErrorLog(json_encode(["error_id" => 1, "error" => $userPhone->getErrors()], JSON_UNESCAPED_UNICODE));\r
+\r
+            return $this->asJson(["error_id" => 1, "error" => $userPhone->getErrors()]);\r
+        }\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        $user = Users::find()->where(['phone' => $result['phone']])->andWhere(['phone_true' => '1'])->one();\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        if (!$user) {\r
+            file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '-нет в бонусной программе-' . __LINE__, FILE_APPEND);\r
+            $mess["new_client"] = true;\r
+            $mess["message_cashier"] = "Заполните данные клиента";\r
+            $mess["error"] = "Покупателя " . $result['phone'] . " нет в бонусной программе!";\r
+\r
+            return $this->asJson($mess);\r
+        }\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        $keycode = $user->keycode;\r
+        $black_list = $user->black_list;\r
+\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        if (!$black_list) {\r
+            file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            $stop = UsersStopList::find()->select(['phone'])->where(['phone' => $result['phone']])->one();\r
+            if ($stop) {\r
+                $black_list = 1;\r
+                $user->black_list = 1;\r
+                file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+                $user->save();\r
+                file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+                if ($user->getErrors()) {\r
+                    file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+                    LogService::apiErrorLog(json_encode(["error_id" => 3, "error" => $user->getErrors()], JSON_UNESCAPED_UNICODE));\r
+\r
+                    return $this->asJson(["error_id" => 3, "error" => $user->getErrors()]);\r
+                }\r
+            }\r
+        }\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        $name = $user->name;\r
+        $user_balans = ClientHelper::getBonusBalance($result['phone']);\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        $baza_spisanie = $baza_nachislenie - $summa_no_writeoffs;\r
+        if ($baza_spisanie < 0) {\r
+            $baza_spisanie = 0;\r
+        }\r
+        $max = $baza_spisanie * $percent;  // максимально можем разрешить списывать до 30 процентов от суммы заказа\r
+        $max = round($max);\r
+        $available_bonus = $user_balans;\r
+        if ($available_bonus > $max) { // если баллов бонусов больше чем 30 процентов списываем по максимуму 30\r
+            $available_bonus = $max;\r
+        }\r
+//        $baza = $check_amount - $bonus;\r
+\r
+        $mess["message_cashier"] = "Клиент $name найден"; // Код: $keycode\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        if ($black_list) {\r
+            $mess['error'] = 'Этот номер в черном списке';\r
+\r
+            return $this->asJson($mess);\r
+        }\r
+\r
+        $txt = $has_actions ? 'В чеке есть акционные товары, на них бонусы не начислятся.' : '';\r
+\r
+        $mess["result"] = true;\r
+        $mess["auth_code"] = $keycode;\r
+        $mess["name"] = $name;\r
+        $mess["total_bonuses"] = $user_balans;\r
+        $mess["bonus_level"] = $bonusLevel;\r
+        $mess["burn_balans"] = $user->burn_balans;\r
+        $mess["available_bonuses"] = $available_bonus;\r
+        $mess["message_cashier"] = $txt . " Спросите последние 4 цифры телефона который позвонит клиенту $user_balans";\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        LogService::apiLogs(1, json_encode($mess, JSON_UNESCAPED_UNICODE));\r
+\r
+        file_put_contents(self::OUT_DIR . '/get_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__ . ' OK ', FILE_APPEND);\r
+        return $this->asJson($mess);\r
+    }\r
+\r
+//    public function actionSendMessage()\r
+//    {\r
+//        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;\r
+//        $data = file_get_contents('php://input');\r
+//        $result = json_decode($data, true);\r
+//\r
+//        $__API_PARAMS = ['store_id', 'seller_id', 'phone'];\r
+//\r
+//        foreach ($__API_PARAMS as $paramName) {\r
+//            if (empty($result[$paramName])) {\r
+//\r
+//                if ($paramName != 'phone') {\r
+//                    LogService::apiErrorLog(json_encode(["error_id" => 1, "error" => "$paramName is required"], JSON_UNESCAPED_UNICODE));\r
+//                }\r
+//\r
+//                return $this->asJson(["error" => "$paramName is required"]);\r
+//            }\r
+//        }\r
+//\r
+//        $phone = ClientHelper::phoneClear($result['phone']);\r
+//        if (!ClientHelper::phoneVerify($phone)) {\r
+//            return $this->asJson(["error_id" => 0.2, "error" => "phone is required"]);\r
+//        }\r
+//        $result['phone'] = $phone;\r
+//\r
+//        $user = Users::find()->where(['phone' => $result['phone']])->andWhere(['phone_true' => '1'])->andWhere(['black_list' => '0'])->one();\r
+//        if (!$user) {\r
+//            $mess["error"] = "Покупателя " . $result['phone'] . " нет в бонусной программе!";\r
+//\r
+//            return $this->asJson($mess);\r
+//        }\r
+//        $keycode = $user->keycode;\r
+//\r
+//        $mess = [];\r
+//        $mess["result"] = true;\r
+//\r
+//        $mess['auth_code'] = $user->keycode;\r
+//        $mess['message_cashier'] = 'Отсканируйте QR код из телеграм бота или введите его руками';\r
+//\r
+//        LogService::apiLogs(1, json_encode($mess, JSON_UNESCAPED_UNICODE));\r
+//\r
+//        return $this->asJson($mess);\r
+//    }\r
+\r
+    public function actionSendMessage() {\r
+        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;\r
+        $data = file_get_contents('php://input');\r
+        $result = json_decode($data, true);\r
+\r
+        $__API_PARAMS = ['store_id', 'seller_id', 'phone'];\r
+\r
+        foreach ($__API_PARAMS as $paramName) {\r
+            if (empty($result[$paramName])) {\r
+\r
+                if ($paramName != 'phone') {\r
+                    LogService::apiErrorLog(json_encode(["error_id" => 1, "error" => "$paramName is required"], JSON_UNESCAPED_UNICODE));\r
+                }\r
+\r
+                return $this->asJson(["error" => "$paramName is required"]);\r
+            }\r
+        }\r
+\r
+        $phone = ClientHelper::phoneClear($result['phone']);\r
+        if (!ClientHelper::phoneVerify($phone)) {\r
+            return $this->asJson(["error_id" => 0.2, "error" => "phone is required"]);\r
+        }\r
+        $result['phone'] = $phone;\r
+\r
+        $user = Users::find()->where(['phone' => $result['phone']])->andWhere(['phone_true' => '1'])->andWhere(['black_list' => '0'])->one();\r
+        if (!$user) {\r
+            $mess["error"] = "Покупателя " . $result['phone'] . " нет в бонусной программе!";\r
+\r
+            return $this->asJson($mess);\r
+        }\r
+\r
+        $mess["message_cashier"] = "Звонок-последние 4 цифры телефона";\r
+\r
+        $userAuthCallLog = UsersAuthCallLog::find()->select(['COUNT(*) as cnt'])->where(['phone' => $result['phone']])\r
+            ->andWhere(['store_id' => $result['store_id']])->andWhere(['>=', 'date', date('Y-m-d H:i:s', strtotime('-10 minutes'))])->one();\r
+\r
+        $cnt = $userAuthCallLog ? $userAuthCallLog->cnt : 1;\r
+\r
+        $keycode = '';\r
+\r
+        if ($cnt < 2) {\r
+            $body = @file_get_contents("https://sms.ru/code/call?phone=" . $result['phone'] . "&api_id=4DFE45F9-1897-79C0-6872-08F05D6B7FA4&ip=" . $_SERVER["REMOTE_ADDR"]);\r
+            $json_res = json_decode($body, true, 512, JSON_UNESCAPED_UNICODE);\r
+            if ($json_res["status"] == "OK") {\r
+                $keycode = $json_res["code"];\r
+                $user->keycode = '' . $keycode;\r
+                $user->password = ClientHelper::generatePassword(8);;\r
+                $user->save();\r
+                if ($user->getErrors()) {\r
+                    LogService::apiErrorLog(json_encode(["error_id" => 3.1415, "error" => $user->getErrors()], JSON_UNESCAPED_UNICODE));\r
+                    return $this->asJson(["error_id" => 3.1415, "error" => $user->getErrors()]);\r
+                }\r
+                $mess["auth_code"] = $keycode;\r
+                $mess["message_cashier"] = "Попытка:$cnt Звонок клиенту! последние 4 цифры номера";\r
+            }\r
+        } elseif ($cnt > 2) {\r
+            $mess["message_cashier"] = "Попытка $cnt -извиняемся перед клиентом";\r
+        }\r
+        $name = "$keycode Попытка $cnt  " . $_SERVER["REMOTE_ADDR"];\r
+        $userAuthCallLog = new UsersAuthCallLog;\r
+        $userAuthCallLog->date = date('Y-m-d H:i:s');\r
+        $userAuthCallLog->store_id = $result['store_id'];\r
+        $userAuthCallLog->seller_id = $result['seller_id'];\r
+        $userAuthCallLog->phone = $result['phone'];\r
+        $userAuthCallLog->name = $name;\r
+        $userAuthCallLog->save();\r
+        if ($userAuthCallLog->getErrors()) {\r
+            LogService::apiErrorLog(json_encode(["error_id" => 4.15, "error" => $userAuthCallLog->getErrors()], JSON_UNESCAPED_UNICODE));\r
+            return $this->asJson(["error_id" => 4.15, "error" => $userAuthCallLog->getErrors()]);\r
+        }\r
+\r
+        Yii::info("keykod={$user->keycode} store_id={$result['store_id']} seller_id={$result['seller_id']} phone={$result['phone']} $name", self::LOG_CATEGORY_BONUS);\r
+\r
+        $mess["timeout"] = 15;\r
+\r
+        LogService::apiLogs(1, json_encode($mess, JSON_UNESCAPED_UNICODE));\r
+\r
+        return $this->asJson($mess);\r
+    }\r
+\r
+//        $mess["message_cashier"] = "Звонок-последние 4 цифры телефона";\r
+//\r
+//        $userAuthCallLog = UsersAuthCallLog::find()->select(['COUNT(*) as cnt'])->where(['phone' => $result['phone']])\r
+//            ->andWhere(['store_id' => $result['store_id']])->andWhere(['>=', 'date', date('Y-m-d H:i:s', strtotime('-10 minutes'))])->one();\r
+//\r
+//        $cnt = $userAuthCallLog ? $userAuthCallLog->cnt : 1;\r
+//\r
+//        if ($cnt < 2) {\r
+//            $body = @file_get_contents("https://sms.ru/code/call?phone=" . $result['phone'] . "&api_id=4DFE45F9-1897-79C0-6872-08F05D6B7FA4&ip=" . $_SERVER["REMOTE_ADDR"]);\r
+//            $json_res = json_decode($body, true, 512, JSON_UNESCAPED_UNICODE);\r
+//\r
+//            if ($json_res["status"] == "OK") {\r
+//                $keycode = $json_res["code"];\r
+//                $user->keycode = '' . $keycode;\r
+//                $user->password = ClientHelper::generatePassword(8);;\r
+//                $user->save();\r
+//\r
+//                if ($user->getErrors()) {\r
+//\r
+//                    LogService::apiErrorLog(json_encode(["error_id" => 3, "error" => $user->getErrors()], JSON_UNESCAPED_UNICODE));\r
+//\r
+//                    return $this->asJson(["error_id" => 3, "error" => $user->getErrors()]);\r
+//                }\r
+//\r
+//                $mess["auth_code"] = $keycode;\r
+//                $mess["message_cashier"] = "Попытка:$cnt Звонок клиенту! последние 4 цифры номера";\r
+//            }\r
+//        } else if ($cnt > 2) {\r
+//            $mess["message_cashier"] = "Попытка $cnt -извиняемся перед клиентом";\r
+//        }\r
+//\r
+//        $name = "$keycode Попытка $cnt  " . $_SERVER["REMOTE_ADDR"];\r
+//        $userAuthCallLog = new UsersAuthCallLog;\r
+//        $userAuthCallLog->date = date('Y-m-d H:i:s');\r
+//        $userAuthCallLog->store_id = $result['store_id'];\r
+//        $userAuthCallLog->seller_id = $result['seller_id'];\r
+//        $userAuthCallLog->phone = $result['phone'];\r
+//        $userAuthCallLog->name = $name;\r
+//        $userAuthCallLog->save();\r
+//        if ($userAuthCallLog->getErrors()) {\r
+//\r
+//            LogService::apiErrorLog(json_encode(["error_id" => 4, "error" => $userAuthCallLog->getErrors()], JSON_UNESCAPED_UNICODE));\r
+//\r
+//            return $this->asJson(["error_id" => 4, "error" => $userAuthCallLog->getErrors()]);\r
+//        }\r
+//\r
+\r
+    public function actionSaveClientInfo()\r
+    {\r
+        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;\r
+        $data = file_get_contents('php://input');\r
+        $result = json_decode($data, true);\r
+\r
+        $fl = date('_Y_m_d__H_i_s_');\r
+        file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . $result['phone']);\r
+        file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+\r
+\r
+        $__API_PARAMS = ['store_id', 'seller_id', 'phone']; // first_name, second_name, sex, birth_day, referral_id, comment, events\r
+\r
+        foreach ($__API_PARAMS as $paramName) {\r
+            if (empty($result[$paramName])) {\r
+\r
+                if ($paramName != 'phone') {\r
+                    LogService::apiErrorLog(json_encode(["error_id" => 1, "error" => "$paramName is required"], JSON_UNESCAPED_UNICODE));\r
+                }\r
+\r
+                return $this->asJson(["error_id" => 1, "error" => "$paramName is required"]);\r
+            }\r
+        }\r
+        file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        $phone = ClientHelper::phoneClear($result['phone']);\r
+        file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        if (!ClientHelper::phoneVerify($phone)) {\r
+            file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            return $this->asJson(["error_id" => 1.2, "error" => "phone is required"]);\r
+        }\r
+        $result['phone'] = $phone;\r
+\r
+        $source = $result["source"] ?? 0;\r
+        $store_id = $result["store_id"];\r
+        $store_id_guid = $store_id;\r
+        $seller_id = $result["seller_id"];\r
+        $phone = $result["phone"];\r
+        $first_name = $result["first_name"] ?? "";\r
+        $second_name = $result["second_name"] ?? "";\r
+        $sex2 = $result["sex"] ?? "";\r
+        $birth_day = $result["birth_day"] ?? "";\r
+        $referral_id = $result["referral_id"] ?? null;\r
+        $comment = $result["comment"] ?? "";\r
+        $events = $result["events"] ?? [];\r
+        $sex = "man";\r
+        if ($sex2 == "male") {\r
+            $sex = "man";\r
+        }\r
+        if ($sex2 == "female") {\r
+            $sex = "women";\r
+        }\r
+//        if ($referral_phone == $phone) {\r
+//            $referral_phone = "";\r
+//        }\r
+\r
+        $mess = [];\r
+        file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        /* @var $user Users */\r
+        $user = Users::find()->where(['phone' => $result['phone']])->andWhere(['phone_true' => '1'])->andWhere(['black_list' => '0'])->one();\r
+        file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        if ($user) {\r
+            file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            $user->referral_id = $referral_id != $user->id ? $referral_id : null;\r
+            $user->pol = $sex;\r
+            $user->bdate = $birth_day;\r
+            $user->name = "$first_name $second_name";\r
+            $user->comment = $comment;\r
+            $user->password = ClientHelper::generatePassword(8);\r
+            $user->keycode = '' . rand(1000, 9999);\r
+            $user->source = $source == 2 ? 1 : 0;\r
+            $user->save(); // иначе не пройдём валидацию, т.к. множество полей в бд не заполнены.\r
+            file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            if ($user->getErrors()) {\r
+                file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+\r
+                LogService::apiErrorLog(json_encode(["error_id" => 2, "error" => $user->getErrors()], JSON_UNESCAPED_UNICODE));\r
+\r
+                return $this->asJson(["error_id" => 2, "error" => $user->getErrors()]);\r
+            }\r
+            file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            $userEventOld = UsersEvents::find()->where(['phone' => $phone])->orderBy(['date_add' => SORT_ASC])->one();\r
+            if ($userEventOld && $userEventOld->date_add < date('Y-m-d H:i:s', time() - 2 * 86400)) { // Дата добавление последнего события не старше двух дней\r
+                file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+                $mess["result"] = true;\r
+                $mess["message_cashier"] = "Возможность внесения памятных дат ограничена";\r
+\r
+                LogService::apiLogs(1, json_encode($mess, JSON_UNESCAPED_UNICODE));\r
+\r
+                return $this->asJson($mess);\r
+            }\r
+            file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+\r
+            $nomer_event = 1;\r
+            $dates = [];\r
+\r
+            foreach ($events as $k => $mass) {\r
+                $date = $mass["date"] ?? '';\r
+                $event_id = intval($mass['event_id'] ?? 0);\r
+\r
+                $datea = explode("-", $date);\r
+                $date_end = date("Y", time() + self::$YEAR_PERIOD * 86400) . "-" . $datea[1] . "-" . $datea[2];\r
+                $userEvent2 = UsersEvents::find()->where(['phone' => $phone])->andWhere(['date_day' => $datea[2]])->andWhere(['date_month' => $datea[1]])->one();\r
+                file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+                if ($userEvent2) {\r
+                    $userEvent2->delete();\r
+                }\r
+                $userEvent3 = new UsersEvents;\r
+                $userEvent3->number = $nomer_event;\r
+                $userEvent3->date = $date;\r
+                $userEvent3->tip_id = $event_id;\r
+                $userEvent3->phone = $phone;\r
+                $userEvent3->date_day = $datea[2];\r
+                $userEvent3->date_month = $datea[1];\r
+                $userEvent3->date_add = date('Y-m-d H:i:s');\r
+                $userEvent3->tip = strval('???');\r
+                $userEvent3->name = 'М';\r
+                $userEvent3->sex = 'm';\r
+                $userEvent3->date_edit = date("Y-m-d H:i:s");\r
+                $userEvent3->date_edit_info = date("Y-m-d H:i:s");\r
+                file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+                $userEvent3->save(); // иначе не пройдём валидацию, т.к. множество полей в бд не заполнены.\r
+                if ($userEvent3->getErrors()) {\r
+                    file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+\r
+                    LogService::apiErrorLog(json_encode(["error_id" => 3, "error" => $userEvent3->getErrors()], JSON_UNESCAPED_UNICODE));\r
+\r
+                    return $this->asJson(["error_id" => 3, "error" => $userEvent3->getErrors()]);\r
+                }\r
+                $dates [] = $date;\r
+                $nomer_event++;\r
+            }\r
+        } else {\r
+            file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            $created_name = $seller_id;\r
+            $rand = rand(1000, 9999);\r
+            $name = "$first_name $second_name";\r
+            $pass = ClientHelper::generatePassword(8);\r
+            $product1 = Products1c::find()->select(['name'])->where(['tip' => 'admin'])->andWhere(['id' => $seller_id])->one();\r
+            $product2 = Products1c::find()->select(['name'])->where(['tip' => 'city_store'])->andWhere(['id' => $store_id])->one();\r
+\r
+            $created_name = $product1 ? $product1->name : '';\r
+            $created_store = $product2 ? $product2->name : '';\r
+\r
+            $store_id_new = ExportImportTable::find()->select(['entity_id'])->where(['entity' => 'city_store'])->andWhere(['export_id' => '1'])\r
+                ->andWhere(['export_val' => $store_id])->one();\r
+            $seller_id_new = ExportImportTable::find()->select(['entity_id'])->where(['entity' => 'admin'])->andWhere(['export_id' => '1'])\r
+                ->andWhere(['export_val' => $seller_id])->one();\r
+            if ($store_id_new) {\r
+                $store_id_int = $store_id_new->entity_id;\r
+            }\r
+            if ($seller_id_new) {\r
+                $seller_id_int = $seller_id_new->entity_id;\r
+            }\r
+            file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            Users::deleteAll(['phone' => $phone, 'phone_true' => '0']);\r
+            file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            $user2 = new Users;\r
+            $user2->source = $source;\r
+            $user2->pol = $sex;\r
+            $user2->keycode = strval($rand);\r
+            $user2->phone = $phone;\r
+            $user2->name = $name;\r
+            $user2->name_name = $first_name;\r
+            $user2->name_last = $second_name;\r
+            $user2->password = $pass;\r
+            $user2->phone_true = strval(1);\r
+            $user2->bdate = $birth_day;\r
+            $user2->referral_id = $referral_id;\r
+            $user2->comment = $comment;\r
+            $user2->created_id = $seller_id_int ?? 0;\r
+            $user2->created_name = $created_name;\r
+            $user2->seller_id = strval($seller_id);\r
+            $user2->store_id = $store_id_guid;\r
+            $user2->created_store_id = $store_id_int ?? 0;\r
+            $user2->created_store = $created_store;\r
+            $user2->date = date('Y-m-d H:i:s');\r
+            $user2->sale_store_id = $store_id_int ?? 0;\r
+            $user2->sale_store = '';\r
+            $user2->sms_info = 1;\r
+            $user2->reklama_info = 1;\r
+            $user2->info = '';\r
+            $setka_id = 1;\r
+            $user2->setka_id = $setka_id;\r
+            $user2->card = "" . ($phone * 2 + 1608 + $setka_id); // генерируем номер карты который зависит от номера сетки + ДР Тимура\r
+            $user2->save();  // иначе не пройдём валидацию, т.к. множество полей в бд не заполнены.\r
+            file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            if ($user2->getErrors()) {\r
+                file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+\r
+                LogService::apiErrorLog(json_encode(["error_id" => 4, "error" => $user2->getErrors()], JSON_UNESCAPED_UNICODE));\r
+\r
+                return $this->asJson(["error_id" => 4, "error" => $user2->getErrors()]);\r
+            }\r
+\r
+            file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            if ($store_id == '56524cb1-4763-11ea-8cce-b42e991aff6c') {\r
+                file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+                $admin_id = ClientHelper::getExportId($seller_id, "admin", 1);\r
+\r
+                $usersBonus = new UsersBonus;\r
+                $usersBonus->date = date('Y-m-d H:i:s');\r
+                $usersBonus->tip = 'plus';\r
+                $usersBonus->tip_sale = 'podarok';\r
+                $usersBonus->phone = $phone;\r
+                $usersBonus->name = "Приветственные бонусы посетителю сайта";\r
+                $usersBonus->store_id = $store_id_int ?? 0;\r
+                $usersBonus->site_id = 0;\r
+                $usersBonus->referal_id = 0;\r
+                $usersBonus->admin_id = $admin_id;\r
+                $usersBonus->price = 0;\r
+                $usersBonus->price_skidka = 0;\r
+                $usersBonus->bonus = 50;\r
+                $usersBonus->store_id_1c = $store_id;\r
+                $usersBonus->seller_id_1c = $seller_id;\r
+                $usersBonus->date_start = date('Y-m-d 08:00:00', strtotime('+1 day', strtotime($usersBonus->date)));\r
+                $usersBonus->date_end = date('Y-m-d H:i:s', strtotime('+1 week', strtotime($usersBonus->date_start)));\r
+                $usersBonus->save();\r
+                file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+                if ($usersBonus->getErrors()) {\r
+                    file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+                    LogService::apiErrorLog(json_encode(["error_id" => 5, "error" => $usersBonus->getErrors()], JSON_UNESCAPED_UNICODE));\r
+\r
+                    return $this->asJson(["error_id" => 5, "error" => $usersBonus->getErrors()]);\r
+                }\r
+            }\r
+        }\r
+\r
+        $mess["result"] = true;\r
+        $mess["message_cashier"] = "Данные клиента сохранены";\r
+\r
+        LogService::apiLogs(1, json_encode($mess, JSON_UNESCAPED_UNICODE));\r
+\r
+        file_put_contents(self::OUT_DIR . '/save_client_info_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__ . ' OK ', FILE_APPEND);\r
+        return $this->asJson($mess);\r
+    }\r
+\r
+    public function actionSale()\r
+    {\r
+        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;\r
+        $data = file_get_contents('php://input');\r
+        $result = json_decode($data, true);\r
+        $resultTest = $result;\r
+        $fl = date('_Y_m_d__H_i_s_');\r
+        $json=json_encode($resultTest,JSON_UNESCAPED_UNICODE);\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '_info.json', PHP_EOL . '--' . $result['phone']);\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '_info.json', ' '.date("d.m.Y H:i:s",time()).' JSON: '.$json.'  ', FILE_APPEND);\r
+\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . $result['phone']);\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+\r
+        $__API_PARAMS = ['store_id', 'seller_id', 'phone', 'check_amount', 'check_id', 'check_name']; // items, auth_code, write_off_bonuses\r
+\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        foreach ($__API_PARAMS as $paramName) {\r
+\r
+            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            if (empty($result[$paramName])) {\r
+\r
+                if ($paramName != 'phone') {\r
+                    file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '-error-' . __LINE__, FILE_APPEND);\r
+                    LogService::apiErrorLog(json_encode(["error_id" => 1, "error" => "$paramName is required"], JSON_UNESCAPED_UNICODE));\r
+                }\r
+\r
+                return $this->asJson(["error_id" => 1, "error" => "$paramName is required"]);\r
+            }\r
+        }\r
+\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        $phone = ClientHelper::phoneClear($result['phone']);\r
+\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        if (!ClientHelper::phoneVerify($phone)) {\r
+\r
+            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '-error-' . __LINE__, FILE_APPEND);\r
+            return $this->asJson(["error_id" => 1.2, "error" => "phone is required"]);\r
+        }\r
+\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        $result['phone'] = $phone;\r
+        $result['items'] = $result['items'] ?? [];\r
+\r
+        $store_id = $result["store_id"];\r
+        $seller_id = $result["seller_id"];\r
+        $check_amount = $result["check_amount"];\r
+        $check_id = $result["check_id"];\r
+        $check_name = $result["check_name"];\r
+        $lid_id = $result["lid_id"] ?? 0;\r
+        $auth_code = $result['auth_code'] ?? 0;\r
+        $write_off_bonuses = intval($result["write_off_bonuses"] ?? 0); // только при продаже\r
+\r
+        $user = Users::find()->where(['phone' => $result['phone']])->andWhere(['phone_true' => '1'])->one();\r
+        $bonusLevels = BonusLevels::find()->where(['active' => 1])->indexBy('alias')->asArray()->all();\r
+        $bonusLevel = $user->bonus_level ?? "silver";\r
+        $cashback_rate = isset($bonusLevels[$bonusLevel]['cashback_rate'])\r
+            ? $bonusLevels[$bonusLevel]['cashback_rate'] / 100\r
+            : self::$FIRST_SALE_PROCENT;\r
+\r
+//        $referal_rate = isset($bonusLevels[$bonusLevel]['referal_rate'])\r
+//            ? $bonusLevels[$bonusLevel]['referal_rate'] / 100\r
+//            : self::$CREDIT_HIGH_PROCENT;\r
+\r
+        $bonus_rate = isset($bonusLevels[$bonusLevel]['bonus_rate'])\r
+            ? $bonusLevels[$bonusLevel]['bonus_rate'] / 100\r
+            : self::$FIRST_SALE_PROCENT;\r
+\r
+\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        $amount_real = 0;\r
+        $items_arr_no = array_values(ArrayHelper::map(\r
+            UniversalCatalogItem::find()->where(['catalog_alias' => 'unused_nomenclature'])->all(), 'guid', 'guid'));\r
+        $items_arr_no_bonus_writeoffs = array_values(ArrayHelper::map(\r
+            UniversalCatalogItem::find()->where(['catalog_alias' => 'non_bonusable_goods'])->all(), 'guid', 'guid'));\r
+\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        $summa_no = 0;\r
+        $summa_no_writeoffs = 0;\r
+        $amount_all = 0;\r
+\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        foreach ($result['items'] as $item) {\r
+            if (in_array($item["product_id"], $items_arr_no)) {\r
+                $summa_no = $summa_no + $item["price"] * $item["quantity"];\r
+            } else if (in_array($item["product_id"], $items_arr_no_bonus_writeoffs)) {\r
+                $summa_no_writeoffs = $summa_no_writeoffs + $item["price"] * $item["quantity"];\r
+            } else {\r
+                $amount_real = $amount_real + $item["price"] * $item["quantity"];\r
+            }\r
+            $amount_all = $amount_all + $item["price"] * $item["quantity"];\r
+        }\r
+\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+\r
+        $cnt = intval(Sales::find()->where(['phone' => $result['phone'], 'operation' => Sales::OPERATION_SALE])->count());\r
+\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+       // $max_procent = $cnt == 0 ? self::$FIRST_SALE_PROCENT : ($cnt == 1 ? self::$SECOND_SALE_PROCENT : self::$MAX_PROCENT);\r
+        $max_procent = $bonus_rate;\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        // если списывается в попытке больше бонусов чем может списаться -\r
+        $percent = $phone == "79049031399" ? 0.9 : $max_procent;\r
+        $write_off_bonuses_theory = round($amount_real * $percent);\r
+        if ($write_off_bonuses > $write_off_bonuses_theory) {\r
+            $write_off_bonuses = $write_off_bonuses_theory;\r
+        }\r
+        $user_balans = ClientHelper::getBonusBalance($phone);\r
+        if ($user_balans < $write_off_bonuses) {\r
+            $write_off_bonuses = $user_balans;\r
+        }\r
+\r
+        // TO8-22: Промо-списание БЛАГО\r
+        // Проверяем: если у клиента есть промо-баланс >= 350, покупка >= 1700 и 350 > стандартного максимума — списываем промо\r
+        $usePromoWriteOff = false;\r
+        $promoWriteOffAmount = 350;\r
+        $promoMinCheckAmount = 1700;\r
+\r
+        $promoPlusSum = (float) UsersBonus::find()\r
+            ->where(['phone' => $phone, 'tip' => 'plus', 'tip_sale' => Promocode::TIP_SALE_PROMOBONUS])\r
+            ->andWhere(['<=', 'date_start', date('Y-m-d H:i:s')])\r
+            ->sum('bonus');\r
+        $promoMinusSum = (float) UsersBonus::find()\r
+            ->where(['phone' => $phone, 'tip' => 'minus', 'tip_sale' => Promocode::TIP_SALE_PROMOBONUS])\r
+            ->sum('bonus');\r
+        $promoBalance = max(0, $promoPlusSum - $promoMinusSum);\r
+\r
+        if ($promoBalance >= $promoWriteOffAmount\r
+            && $amount_all >= $promoMinCheckAmount\r
+            && $promoWriteOffAmount > $write_off_bonuses_theory\r
+        ) {\r
+            $usePromoWriteOff = true;\r
+            $write_off_bonuses = $promoWriteOffAmount;\r
+        }\r
+\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        // сумма со скидкой\r
+        $summa_chek = $amount_all - $write_off_bonuses;\r
+        $baza_back = $amount_real + $summa_no_writeoffs - $write_off_bonuses;\r
+\r
+        $mess = [];\r
+\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+\r
+        /** @var $user Users */\r
+        if (!$user) {\r
+\r
+            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            $mess["error"] = "Покупателя " . $result['phone'] . " нет в бонусной программе!";\r
+\r
+            return $this->asJson($mess);\r
+        }\r
+        $user->burn_balans = max(0, $user->burn_balans - $write_off_bonuses);\r
+        // [balans - burn_balance, burn_balans] - показать клиенту что мы сожгли сжигаемый баланс\r
+\r
+// старая точка проверки кода\r
+//        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+//        if ($user->keycode != strval($auth_code)) {\r
+//\r
+//            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '-auth_code not valid-' . __LINE__ . ' keycode ' .$user->keycode . '|  auth_code ' . strval($auth_code), FILE_APPEND);\r
+//            return $this->asJson(['error' => 'auth_code not valid']);\r
+//        }\r
+\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        $user_id = $user->id;\r
+//        $keycode = $user->keycode;\r
+//        $name = $user->name;\r
+//        $referral_id = $user->referral_id;\r
+//        $sale_avg_price = $user->sale_avg_price;\r
+        $sale_price = $user->sale_price;\r
+        $sale_cnt = $user->sale_cnt;\r
+//        if ($referral_id == $user_id) {\r
+//            $referral_id = 0;\r
+//        }\r
+        $ip = $_SERVER['REMOTE_ADDR'];\r
+\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+\r
+\r
+        $store_id_1c = $store_id;\r
+        $site_id = 0;\r
+\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        // получаем внутренний ID продаца - сотрудника из таблицы admin\r
+        $admin_id = ClientHelper::getExportId($seller_id, "admin", 1);\r
+        // получаем внутренний ID продаца - сотрудника из таблицы admin\r
+        $store_id = ClientHelper::getExportId($store_id_1c, "city_store", 1);\r
+\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        $writeOffAlready = false;\r
+        if (!empty($lid_id)) {\r
+\r
+            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            $writeOffAlready = UsersBonus::find()->where(['lid_id' => $lid_id, 'phone' => $phone, 'tip_sale' => 'sale', 'tip' => 'minus'])->one() != null;\r
+        }\r
+\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+\r
+        $user_balans_new = $user_balans;\r
+        if ($write_off_bonuses && !$writeOffAlready) {\r
+            // TO8-22: При промо-списании auth_code не требуется (списание автоматическое)\r
+            if (!$usePromoWriteOff) {\r
+                // Проверка кода только при стандартном списании\r
+                file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+                if ($user->keycode != strval($auth_code)) {\r
+\r
+                    file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '-auth_code not valid-' . __LINE__ . ' keycode ' .$user->keycode . '|  auth_code ' . strval($auth_code), FILE_APPEND);\r
+                    return $this->asJson(['error' => 'auth_code not valid']);\r
+                }\r
+            }\r
+\r
+            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            $user_balans_new = $user_balans - $write_off_bonuses;\r
+            $tipSaleForWriteOff = $usePromoWriteOff ? Promocode::TIP_SALE_PROMOBONUS : 'sale';\r
+            $name_b = $usePromoWriteOff\r
+                ? "Списание промо-бонусов БЛАГО по чеку $check_name"\r
+                : "Спиcание бонусов по чеку $check_name";\r
+            $usersBonus = new UsersBonus;\r
+            $usersBonus->date = date('Y-m-d H:i:s');\r
+            $usersBonus->tip = 'minus';\r
+            $usersBonus->tip_sale = $tipSaleForWriteOff;\r
+            $usersBonus->phone = $phone;\r
+            $usersBonus->name = $name_b;\r
+            $usersBonus->check_id = $check_id;\r
+            $usersBonus->store_id = $store_id;\r
+            $usersBonus->ip = $ip;\r
+            $usersBonus->site_id = $site_id; // ???\r
+            $usersBonus->referal_id = 0;// $referal_id;\r
+            $usersBonus->admin_id = $admin_id;\r
+            $usersBonus->price = $summa_chek;\r
+            $usersBonus->price_skidka = $write_off_bonuses;\r
+            $usersBonus->bonus = $write_off_bonuses;\r
+            $usersBonus->store_id_1c = $store_id_1c;\r
+            $usersBonus->seller_id_1c = $seller_id;\r
+            $usersBonus->user_id = $user_id;             // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+            $usersBonus->lid_id = $lid_id;               // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+            $usersBonus->date_start = $usersBonus->date; // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+            $usersBonus->date_end = $usersBonus->date;   // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+            $usersBonus->date_dell = $usersBonus->date;  // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+\r
+            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+\r
+            $usersBonus->save(); // иначе не пройдём валидацию, т.к. множество полей в бд не заполнены.\r
+\r
+            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+\r
+            if ($user->first_minus_balance === null) {\r
+\r
+                file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+                $user->first_minus_balance = $usersBonus->date;\r
+                $user->save();\r
+\r
+                file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            }\r
+\r
+            if ($usersBonus->getErrors()) {\r
+\r
+                file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+\r
+                LogService::apiErrorLog(json_encode(["error_id" => 4, "error" => $usersBonus->getErrors()], JSON_UNESCAPED_UNICODE));\r
+\r
+                file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+                return $this->asJson(["error_id" => 4, "error" => $usersBonus->getErrors()]);\r
+            }\r
+\r
+            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            Yii::info("MINUS write_off_bonuses={$write_off_bonuses}", self::LOG_CATEGORY_BONUS);\r
+        }\r
+        // TO8-22: При промо-списании кэшбек НЕ начисляется\r
+        if ($usePromoWriteOff) {\r
+            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--promo_writeoff_no_cashback--' . __LINE__, FILE_APPEND);\r
+            Yii::info("PROMO write_off={$write_off_bonuses}, no cashback for {$phone}", self::LOG_CATEGORY_BONUS);\r
+\r
+            // Сохраняем продажу и обновляем уровень\r
+            $userFound = Users::find()->where(['phone' => $result['phone']])->one();\r
+            /** @var $userFound Users */\r
+            if ($userFound) {\r
+                $sale_price_new = ($userFound->sale_price ?? 0) + $amount_all;\r
+                $sale_cnt_new = ($userFound->sale_cnt ?? 0) + 1;\r
+                $userFound->sale_price = $sale_price_new;\r
+                $userFound->sale_cnt = $sale_cnt_new;\r
+                $userFound->save(false);\r
+                $this->updateUserBonusLevel($userFound, $sale_price_new, $check_id, $check_name);\r
+            }\r
+\r
+            $mess["write_off_bonuses"] = $write_off_bonuses;\r
+            $mess["summa_chek"] = $summa_chek;\r
+            $mess["bonus_back"] = 0;\r
+            $mess["user_balans"] = $user_balans_new;\r
+            $mess["promo_writeoff"] = true;\r
+\r
+            return $this->asJson($mess);\r
+        }\r
+\r
+        //начисляем кэшбек клиенту 10% от покупки - с базы за вычитом бонусов которые он списывает\r
+\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        $userFound = Users::find()->where(['phone' => $result['phone']])->one();\r
+        /** @var $userFound Users */\r
+        $salesCount = -1; /* Из-за нулевого значения по умолчанию куча клиентов получило бонус 20% за покупку */\r
+\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        if ($userFound && $userFound->telegram_created_at) {\r
+            $salesCount = intval(Sales::find()->where(['phone' => $result['phone'], 'operation' => Sales::OPERATION_SALE])\r
+                ->andWhere(['>=', 'date', $userFound->telegram_created_at])->count());\r
+\r
+            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        }\r
+        $credit_procent_index = $userFound && $userFound->source > 0 && $salesCount == 0 ? 1 : 0;\r
+\r
+\r
+        $back10 = $back20 = 0;\r
+        $back1 = $back = round($baza_back * $cashback_rate);\r
+        $nm = "Возврат с покупки " . (100 * $cashback_rate) . "% $check_name сумма чека $check_amount";\r
+\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        $userBonus2 = UsersBonus::find()->where(['phone' => $phone])->andWhere(['check_id' => $check_id])->andWhere(['site_id' => $site_id])\r
+            ->andWhere(['store_id' => $store_id])->andWhere(['tip' => 'plus'])->andWhere(['bonus' => $back])->andWhere(['name' => $nm])->one();\r
+        if (!$userBonus2) {\r
+\r
+            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            $user_balans_new += $back;\r
+\r
+            $userBonus2 = new UsersBonus;\r
+            $userBonus2->tip = 'plus';\r
+            $userBonus2->tip_sale = 'sale';\r
+            $userBonus2->date = date('Y-m-d H:i:s');\r
+            $userBonus2->date_start = date('Y-m-d H:i:s', strtotime('+1 day', time()));\r
+            $userBonus2->date_end = date('Y-m-d H:i:s', strtotime('+' . self::$YEAR_PERIOD . ' day', time()));\r
+            $userBonus2->phone = $phone;\r
+            $userBonus2->name = $nm;\r
+            $userBonus2->check_id = $check_id;\r
+            $userBonus2->store_id = $store_id;\r
+            $userBonus2->bonus = $back;\r
+            $userBonus2->ip = $ip;\r
+            $userBonus2->site_id = $site_id;\r
+            $userBonus2->referal_id = 0; // $referal_id;\r
+            $userBonus2->admin_id = $admin_id;\r
+            $userBonus2->price = $summa_chek;\r
+            $userBonus2->store_id_1c = $store_id_1c;\r
+            $userBonus2->seller_id_1c = $seller_id;\r
+            $userBonus2->user_id = $user_id;\r
+            $userBonus2->lid_id = $lid_id;\r
+            $userBonus2->price_skidka = 0;\r
+            $userBonus2->date_dell = $userBonus2->date_end;\r
+\r
+            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            $userBonus2->save();\r
+\r
+            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            if ($userBonus2->getErrors()) {\r
+\r
+                file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+\r
+                LogService::apiErrorLog(json_encode(["error_id" => 5, "error" => $userBonus2->getErrors()], JSON_UNESCAPED_UNICODE));\r
+\r
+                return $this->asJson(["error_id" => 5, "error" => $userBonus2->getErrors()]);\r
+            } else {\r
+                $back10 = $back;\r
+            }\r
+\r
+            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            Yii::info("PLUS bonus={$back}", self::LOG_CATEGORY_BONUS);\r
+\r
+            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            if ($credit_procent_index) {\r
+                file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+                $back = round($baza_back * self::$CREDIT_HIGH_PROCENT_PART20);\r
+                $nm = "Возврат с покупки " . (100 * self::$CREDIT_HIGH_PROCENT_PART20) . "% $check_name сумма чека $check_amount";\r
+\r
+                $user_balans_new += $back;\r
+\r
+                $userBonus2 = new UsersBonus();\r
+                $userBonus2->tip = 'plus';\r
+                $userBonus2->tip_sale = 'sale';\r
+                $userBonus2->date = date('Y-m-d H:i:s');\r
+                $userBonus2->date_start = date('Y-m-d H:i:s', strtotime('+1 day', time()));\r
+                $userBonus2->date_end = date('Y-m-d H:i:s', strtotime('+3 month', time()));\r
+                $userBonus2->phone = $phone;\r
+                $userBonus2->name = $nm;\r
+                $userBonus2->check_id = $check_id;\r
+                $userBonus2->store_id = $store_id;\r
+                $userBonus2->bonus = $back;\r
+                $userBonus2->ip = $ip;\r
+                $userBonus2->site_id = $site_id;\r
+                $userBonus2->referal_id = 0;\r
+                $userBonus2->admin_id = $admin_id;\r
+                $userBonus2->price = $summa_chek;\r
+                $userBonus2->store_id_1c = $store_id_1c;\r
+                $userBonus2->seller_id_1c = $seller_id;\r
+                $userBonus2->user_id = $user_id;\r
+                $userBonus2->lid_id = $lid_id;\r
+                $userBonus2->price_skidka = 0;\r
+                $userBonus2->date_dell = $userBonus2->date_end;\r
+\r
+                file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+                $userBonus2->save();\r
+\r
+                file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+                if ($userBonus2->getErrors()) {\r
+\r
+                    file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+\r
+                    LogService::apiErrorLog(json_encode(["error_id" => 5.2, "error" => $userBonus2->getErrors()], JSON_UNESCAPED_UNICODE));\r
+\r
+                    return $this->asJson(["error_id" => 5.2, "error" => $userBonus2->getErrors()]);\r
+                } else {\r
+                    $back20 = $back;\r
+                }\r
+                if ($userFound->telegram_created_at == null) {\r
+                    $userFound->telegram_created_at = date("Y-m-d H:i:s");\r
+\r
+                    file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+                    $userFound->save();\r
+                    if ($userFound->getErrors()) {\r
+\r
+                        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+\r
+                        LogService::apiErrorLog(json_encode(["error_id" => 5.3, "error" => $userFound->getErrors()], JSON_UNESCAPED_UNICODE));\r
+\r
+                        return $this->asJson(["error_id" => 5.3, "error" => $userFound->getErrors()]);\r
+                    }\r
+                }\r
+\r
+                $notifiableUser = new NotifiableUser;\r
+                $notifiableUser->phone = $phone;\r
+                $notifiableUser->type = "first_given_bonus";\r
+                $notifiableUser->data = "" . ($back1 + $back);\r
+\r
+                file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+                $notifiableUser->save();\r
+\r
+                file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+                if ($notifiableUser->getErrors()) {\r
+\r
+                    file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+                    return $this->asJson(["error_id" => 5.4, "error" => $notifiableUser->getErrors()]);\r
+                }\r
+            }\r
+        }\r
+\r
+//        /////// Добавляем бонусов рефералу\r
+//        if ($referal_id && $back) {\r
+//            $name = "Вознаграждение за приведенного друга";\r
+//            $referalBonus = UsersBonus::find()->where(['phone' => $phone])->andWhere(['referal_id' => $referal_id])->andWhere(['tip' => 'plus'])->one(); // phone = referal_id ???\r
+//            if (!$referalBonus) {\r
+//                $referalBonus = new UsersBonus;\r
+//                $referalBonus->tip = 'plus';\r
+//                $referalBonus->tip_sale = 'referal';\r
+//                $referalBonus->date = date('Y-m-d H:i:s');\r
+//                $referalBonus->date_start = date('Y-m-d H:i:s', strtotime('+1 day', time()));\r
+//                $referalBonus->date_end = date('Y-m-d H:i:s', strtotime('+' . self::$YEAR_PERIOD . ' day', time()));\r
+//                $referalBonus->phone = $phone;\r
+//                $referalBonus->name = $name;\r
+//                $referalBonus->check_id = $check_id;\r
+//                $referalBonus->store_id = $store_id;\r
+//                $referalBonus->bonus = $back;\r
+//                $referalBonus->ip = $ip;\r
+//                $referalBonus->site_id = $site_id; // ??? $user_id_referal\r
+//                $referalBonus->referal_id = $referal_id; // ???\r
+//                $referalBonus->admin_id = $admin_id;\r
+//                $referalBonus->price = $summa_chek;\r
+//                $referalBonus->store_id_1c = $store_id_1c;\r
+//                $referalBonus->seller_id_1c = $seller_id;\r
+//                $referalBonus->save();\r
+//                if ($referalBonus->getErrors()) {\r
+//                    return $this->asJson(["error_id" => 3, "error" => $referalBonus->getErrors()]);\r
+//                }\r
+//            }\r
+//        }\r
+\r
+        ///////\r
+//        $itogo = 0;\r
+//        foreach ($result["items"] as $k => $mass) {\r
+//            $seller_id_item = $mass["seller_id"];\r
+//            $product_id = $mass["product_id"];\r
+//            $price = $mass["price"];\r
+//            $quantity = $mass["quantity"];\r
+//            $sm = $price * $quantity;\r
+//            //$info .=" id=$product_id ($quantity шт. x $price руб.)  = $sm руб.,";\r
+//            $itogo += $sm;\r
+//\r
+//            //получаем внутренний ID товара\r
+//            $item_id = ClientHelper::get_export_id($product_id, "products",1);\r
+//\r
+//            //товары к продаже\r
+//            $salesItem = new SalesItems;\r
+//            $salesItem->date = date('Y-m-d H:i:s');\r
+//            $salesItem->phone = $phone;\r
+//            $salesItem->check_id = $check_id;\r
+//            $salesItem->store_id = $store_id;\r
+//            $salesItem->store_id_1c = $store_id_1c;\r
+//            $salesItem->seller_id = $seller_id_item;\r
+//            $salesItem->admin_id = $admin_id;\r
+//            $salesItem->id_1c = $product_id;\r
+//            $salesItem->item_id = $item_id;\r
+//            $salesItem->kol = $quantity;\r
+//            $salesItem->summa = $sm;\r
+//            $salesItem->referal_id = 0; // $referal_id;\r
+//            $salesItem->color_id = 0; // $color_id ???\r
+//            $salesItem->lid_id = $lid_id;\r
+//            $salesItem->complect_id = 0;                 // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+//            $salesItem->name = '???';                    // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+//            $salesItem->skidka = $mass['discount'] ?? 0; // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+//            $salesItem->vozvrat = 0;                     // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+//            $salesItem->save();\r
+//            if ($salesItem->getErrors()) {\r
+//                return $this->asJson(["error_id" => 4, "error" => $salesItem->getErrors()]);\r
+//            }\r
+//        }\r
+        // sale_avg_price sale_price\r
+        $sale_price += $check_amount;\r
+        $sale_avg_price = round($sale_price / ($sale_cnt + 1));\r
+\r
+        $user->keycode = "" . rand(1000, 9999);\r
+        $user->password = ClientHelper::generatePassword(8);\r
+        $user->date_last_sale = date('Y-m-d H:i:s');\r
+        $user->sale_cnt = $sale_cnt + 1;\r
+        if ($user->sale_cnt == 1) {\r
+            $user->date_first_sale = $user->date_last_sale;\r
+        }\r
+        $user->sale_store_id = $store_id;\r
+        $user->sale_price = $sale_price;\r
+        $user->sale_avg_price = $sale_avg_price;\r
+        $user->check_id_last_sale = $check_id;\r
+        if (!$user->date) {\r
+            $user->date = (new DateTime('now', new DateTimeZone('Europe/Moscow')))->format('Y-m-d H:i:sP');\r
+        }\r
+        $user->balans = ClientHelper::getBonusBalance($phone);\r
+\r
+//        $user->email_old = "example@example.ru"; // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+//        $user->phone_old = "71111111111";        // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+//        $user->check_id_forgot = "???";          // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+//        $user->sid_forgot = "???";               // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+//        $user->alerts_balans = "???";            // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+//        $user->alerts_date = "???";              // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+//        $user->alerts_reklama = "???";           // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+//        $user->seller_id = "???";                // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        $user->save();\r
+        if ($user->getErrors()) {\r
+\r
+\r
+            file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            LogService::apiErrorLog(json_encode(["error_id" => 6, "error" => $user->getErrors()], JSON_UNESCAPED_UNICODE));\r
+\r
+            Yii::info("BEFORE END errors=" . json_encode($user->getErrors(), JSON_UNESCAPED_UNICODE), self::LOG_CATEGORY_BONUS);\r
+\r
+            return $this->asJson(["error_id" => 6, "error" => $user->getErrors()]);\r
+        } else {\r
+            $this->updateUserBonusLevel($user, $sale_price, $check_id, $check_name);\r
+        }\r
+        Yii::info("BEFORE END", self::LOG_CATEGORY_BONUS);\r
+//        $itogo -= $write_off_bonuses;\r
+\r
+//        // продажа заносим в таблицу\r
+//        $sale = new Sales;\r
+//        $sale->date = date("Y-m-d H:i:s");\r
+//        $sale->phone = $phone;\r
+//        $sale->operation = 'Продажа';\r
+//        $sale->store_id = $store_id;\r
+//        $sale->admin_id = $admin_id;\r
+//        $sale->seller_id = $seller_id;\r
+//        $sale->store_id_1c = $store_id_1c;\r
+//        $sale->id = $check_id;\r
+//        $sale->number = $check_name;\r
+//        $sale->summ = $amount_all;\r
+//        $sale->skidka = $write_off_bonuses;\r
+//        $sale->status = "???";        // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+//        $sale->payments = "???";      // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+//        $sale->pay_arr = "???";       // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+//        $sale->sales_check = "???";   // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+//        $sale->order_id = "";         // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+//        $sale->terminal_id = "???";   // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+//        $sale->terminal = "???";      // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+//        $sale->kkm_id = "???";        // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+//        $sale->held = 0;              // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+//        $sale->date_up = $sale->date; // Поле не заполнялось в старом апи, но без него бд выдаёт ошибку при сохранении\r
+//        $sale->save();\r
+//        if ($sale->getErrors()) {\r
+//            return $this->asJson(["error_id" => 6, "error" => $sale->getErrors()]);\r
+//        }\r
+\r
+        $mess["result"] = true;\r
+        $mess["message_cashier"] = "Бонусы списаны";\r
+        $mess["user_balans_old"] = $user_balans;\r
+        $mess["user_balans_new"] = $user_balans_new;\r
+        $mess["user_balans_actual"] = $user->balans;\r
+        LogService::apiLogs(1, json_encode($mess, JSON_UNESCAPED_UNICODE));\r
+        file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__ . ' OK ', FILE_APPEND);\r
+\r
+        $totalBonus = $back10 + $back20;\r
+\r
+        $input = [\r
+            'phone' => $phone,\r
+            'bonusCount' => $totalBonus,\r
+            'purchaseDate' => date("Y-m-d H:i:s"),\r
+            'orderId' => $check_id,\r
+        ];\r
+\r
+        $userBonusSendToTgLogs = new UserBonusSendToTgLogs;\r
+        $userBonusSendToTgLogs->input_hash = md5(Json::encode($input));\r
+        $userBonusSendToTgLogs->input = Json::encode($input);\r
+        $userBonusSendToTgLogs->check_id = $check_id;\r
+        $userBonusSendToTgLogs->phone = $phone;\r
+        $userBonusSendToTgLogs->bonusCount = $totalBonus;\r
+        $userBonusSendToTgLogs->status = 1;\r
+        $userBonusSendToTgLogs->date = date('Y-m-d H:I:s');\r
+        $userBonusSendToTgLogs->save();\r
+        if ($userBonusSendToTgLogs->getErrors()) {\r
+            LogService::apiErrorLog(json_encode(["error_id" => 100.001, "error" => $userBonusSendToTgLogs->getErrors()], JSON_UNESCAPED_UNICODE));\r
+        }\r
+        Yii::$app->queue->push(new SendBonusInfoToSiteJob($input));\r
+\r
+//        SiteService::notifySiteAboutBonuses($phone, $totalBonus, date("Y-m-d H:i:s"), $check_id);\r
+\r
+        return $this->asJson($mess);\r
+    }\r
+\r
+    /**\r
+     * Создаёт новую запись в таблице UsersBonusLevels.\r
+     *\r
+     * @param Users $user\r
+     * @param string $bonusLevel\r
+     * @param string $check_id\r
+     * @param string $check_name\r
+     * @param string $createdAt\r
+     * @return bool\r
+     */\r
+    protected function createBonusHistoryRecord($user, $bonusLevel, $check_id, $check_name, $createdAt)\r
+    {\r
+        $bonusRecord = new UsersBonusLevels();\r
+        $bonusRecord->phone = $user->phone;\r
+        $bonusRecord->user_id = $user->id;\r
+        $bonusRecord->bonus_level = $bonusLevel;\r
+        $bonusRecord->date_from = $createdAt;\r
+        $bonusRecord->check_id = $check_id;\r
+        $bonusRecord->check_name = $check_name;\r
+        $bonusRecord->active = 1;\r
+\r
+        if (!$bonusRecord->save()) {\r
+            LogService::apiErrorLog(\r
+                json_encode(["error_id" => 100, "error" => $bonusRecord->getErrors()], JSON_UNESCAPED_UNICODE)\r
+            );\r
+            return false;\r
+        }\r
+        return true;\r
+    }\r
+\r
+    /**\r
+     * Обновляет бонусный уровень пользователя.\r
+     *\r
+     * @param Users  $user       Модель пользователя.\r
+     * @param float  $sale_price Текущая сумма покупок.\r
+     * @param string $check_id   Идентификатор чека.\r
+     * @param string $check_name Имя (номер) чека.\r
+     */\r
+    protected function updateUserBonusLevel($user, $sale_price, $check_id, $check_name)\r
+    {\r
+        $bonusLevels = BonusLevels::find()\r
+            ->where(['active' => 1])\r
+            ->orderBy(['threshold' => SORT_ASC])\r
+            ->all();\r
+\r
+        $computedBonusLevel = null;\r
+        foreach ($bonusLevels as $level) {\r
+            if ($sale_price > $level->threshold) {\r
+                $computedBonusLevel = $level->alias;\r
+            }\r
+        }\r
+        $newBonusLevel = $computedBonusLevel ?? 'silver';\r
+\r
+        $existingHistoryLevel = UsersBonusLevels::find()\r
+            ->where(['or', ['phone' => $user->phone], ['user_id' => $user->id]])\r
+            ->andWhere(['active' => 1])\r
+            ->one();\r
+\r
+        $now = date('Y-m-d H:i:s');\r
+\r
+        if (empty($user->bonus_level) || $user->bonus_level !== $newBonusLevel) {\r
+            $user->bonus_level = $newBonusLevel;\r
+            if (!$user->save()) {\r
+                LogService::apiErrorLog(\r
+                    json_encode(["error_id" => 6.1, "error" => $user->getErrors()], JSON_UNESCAPED_UNICODE)\r
+                );\r
+            }\r
+\r
+            if ($existingHistoryLevel) {\r
+                $existingHistoryLevel->active = 0;\r
+                $existingHistoryLevel->date_to = $now;\r
+                if (!$existingHistoryLevel->save()) {\r
+                    LogService::apiErrorLog(\r
+                        json_encode(\r
+                            ["error_id" => 6.2, "error" => $existingHistoryLevel->getErrors()],\r
+                            JSON_UNESCAPED_UNICODE\r
+                        )\r
+                    );\r
+                }\r
+            }\r
+\r
+            $this->createBonusHistoryRecord($user, $newBonusLevel, $check_id, $check_name, $now);\r
+        }\r
+    }\r
+\r
+    public function actionGetClientInfo()\r
+    {\r
+        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;\r
+        $data = file_get_contents('php://input');\r
+        $result = json_decode($data, true);\r
+\r
+        $__API_PARAMS = ['phone'];\r
+\r
+        foreach ($__API_PARAMS as $paramName) {\r
+            if (empty($result[$paramName])) {\r
+\r
+                if ($paramName != 'phone') {\r
+                    LogService::apiErrorLog(json_encode(["error_id" => 1, "error" => "$paramName is required"], JSON_UNESCAPED_UNICODE));\r
+                }\r
+\r
+                return $this->asJson(["error_id" => 1, "error" => "$paramName is required"]);\r
+            }\r
+        }\r
+\r
+        $phone = ClientHelper::phoneClear($result['phone']);\r
+        if (!ClientHelper::phoneVerify($phone)) {\r
+            return $this->asJson(["error_id" => 1.2, "error" => "phone is required"]);\r
+        }\r
+\r
+        $mess = [];\r
+        $user = Users::find()->select(['id', 'keycode', 'bonus_level', 'burn_balans', 'name', 'referral_id', 'bdate', 'comment', 'pol', 'extract(epoch FROM  date) as date'])\r
+            ->where(['phone' => $phone])->one();\r
+        if (!$user) {\r
+            $mess["error"] = "Покупателя " . $phone . " нет в бонусной программе!";\r
+\r
+            return $this->asJson($mess);\r
+        }\r
+        $name = explode(" ", $user->name);\r
+        $birth_day = $user->bdate;\r
+        $first_name = $name[0] ?? '';\r
+        $second_name = $name[1] ?? '';\r
+        $comment = $user->comment;\r
+        $pol = "male";\r
+        if ($user->pol == "women") {\r
+            $pol = "female";\r
+        }\r
+        // если с момента добавления клиента прошло не более 5 часов позволяем редактировать даты иначе запрещаем редактирование\r
+        if ($user->date > time() - 3600 * 5) {\r
+            $mess["birth_day_readonly"] = true;\r
+            $mess["events_readonly"] = false;\r
+        }\r
+        if ($birth_day) {\r
+            $mess["birth_day_readonly"] = true;\r
+        }\r
+\r
+        $data = UsersEvents::find()->where(['phone' => $phone])->orderBy(['date' => SORT_DESC])->all();\r
+        foreach ($data as $row) {\r
+            if (strlen($row->date_day) == 1) {\r
+                $row->date_day = "0" . $row->date_day;\r
+            }\r
+            if (strlen($row->date_month) == 1) {\r
+                $row->date_month = "0" . $row->date_month;\r
+            }\r
+            if (!isset($mess["events"])) {\r
+                $mess["events"] = [];\r
+            }\r
+            $mess["events"][] = ["date" => $row->date, "event_id" => $row->tip_id];\r
+        }\r
+\r
+        $user_balance = ClientHelper::getBonusBalance($phone);\r
+\r
+        $mess["result"] = true;\r
+        $mess["sex"] = $pol;\r
+        $mess["first_name"] = $first_name;\r
+        $mess["second_name"] = $second_name;\r
+        $mess["birth_day"] = $birth_day;\r
+        $mess["comment"] = $comment;\r
+        $mess["balance"] = $user_balance;\r
+        $mess["bonus_level"] = $user->bonus_level;\r
+        $mess["burn_balans"] = $user->burn_balans;\r
+\r
+        LogService::apiLogs(1, json_encode($mess, JSON_UNESCAPED_UNICODE));\r
+\r
+        return $this->asJson($mess);\r
+    }\r
+\r
+    public function actionReturn()\r
+    {\r
+        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;\r
+        $data = file_get_contents('php://input');\r
+        $result = json_decode($data, true);\r
+\r
+        $__API_PARAMS = ['store_id', 'check_id']; // check_name, seller_id\r
+\r
+        foreach ($__API_PARAMS as $paramName) {\r
+            if (!isset($result[$paramName])) {\r
+\r
+                LogService::apiErrorLog(json_encode(["error_id" => 1, "error" => "$paramName is required"], JSON_UNESCAPED_UNICODE));\r
+\r
+                return $this->asJson(["error_id" => 1, "error" => "$paramName is required"]);\r
+            }\r
+        }\r
+\r
+        $store_id = $result["store_id"];\r
+//        $seller_id = $result["seller_id"] ?? '';\r
+        $check_id = $result["check_id"];\r
+//        $check_name = $result["check_name"] ?? '';\r
+\r
+        UsersBonus::deleteAll(['and', ['check_id' => $check_id],\r
+            ['>', 'date', date('Y-m-d H:i:s', strtotime('-3 day', time()))]]);\r
+\r
+        // api_logs... event: return, seller_id, when, check\r
+\r
+        LogService::apiLogs(1, json_encode(['Удачный возврат'], JSON_UNESCAPED_UNICODE));\r
+\r
+        return $this->asJson(['ok']);\r
+    }\r
+\r
+    public function actionAuthCodeFail()\r
+    {\r
+        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;\r
+        $data = file_get_contents('php://input');\r
+        $result = json_decode($data, true);\r
+\r
+        $__API_PARAMS = [/*'store_id', 'seller_id',*/\r
+            'phone'];\r
+\r
+        foreach ($__API_PARAMS as $paramName) {\r
+            if (empty($result[$paramName])) {\r
+\r
+                if ($paramName != 'phone') {\r
+                    LogService::apiErrorLog(json_encode(["error_id" => 1, "error" => "$paramName is required"], JSON_UNESCAPED_UNICODE));\r
+                }\r
+\r
+                return $this->asJson(["error_id" => 1, "error" => "$paramName is required"]);\r
+            }\r
+        }\r
+\r
+        $phone = ClientHelper::phoneClear($result['phone']);\r
+        if (!ClientHelper::phoneVerify($phone)) {\r
+            return $this->asJson(["error_id" => 1.2, "error" => "phone is required"]);\r
+        }\r
+        $result['phone'] = $phone;\r
+\r
+//        $seller_id = $result['seller_id'];\r
+//        $store_id = $result['store_id'];\r
+\r
+        $user = Users::find()->where(['phone' => $result['phone']])->andWhere(['phone_true' => '1'])->one();\r
+        if (!$user) {\r
+            $mess["error"] = "Покупателя $phone нет в бонусной программе!";\r
+\r
+            return $this->asJson($mess);\r
+        }\r
+\r
+        $user->keycode = "" . rand(1000, 9999);\r
+        $user->save();\r
+\r
+        if ($user->getErrors()) {\r
+\r
+            LogService::apiErrorLog(json_encode(["error_id" => 3, "error" => $user->getErrors()], JSON_UNESCAPED_UNICODE));\r
+\r
+            return $this->asJson(["error_id" => 3, "error" => $user->getErrors()]);\r
+        }\r
+\r
+        $mess = [];\r
+        $mess["result"] = true;\r
+        // api_logs seller, store\r
+\r
+        LogService::apiLogs(1, json_encode($mess, JSON_UNESCAPED_UNICODE));\r
+\r
+        return $this->asJson($mess);\r
+    }\r
+\r
+    public function actionCurrentItems()\r
+    {\r
+        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;\r
+        $data = file_get_contents('php://input');\r
+        $result = json_decode($data, true);\r
+\r
+        $__API_PARAMS = ['store_id', 'seller_id', 'phone', 'amount_no_discount', 'amount_to_pay'];\r
+\r
+        foreach ($__API_PARAMS as $paramName) {\r
+            if (empty($result[$paramName])) {\r
+\r
+                if ($paramName != 'phone') {\r
+                    LogService::apiErrorLog(json_encode(["error_id" => 1, "error" => "$paramName is required"], JSON_UNESCAPED_UNICODE));\r
+                }\r
+\r
+                return $this->asJson(["error_id" => 1, "error" => "$paramName is required"]);\r
+            }\r
+        }\r
+\r
+        $phone = ClientHelper::phoneClear($result['phone']);\r
+        if (!ClientHelper::phoneVerify($phone)) {\r
+            return $this->asJson(["error_id" => 1.2, "error" => "phone is required"]);\r
+        }\r
+        $result['phone'] = $phone;\r
+\r
+//        $store_id = $result["store_id"];\r
+//        $seller_id = $result["seller_id"];\r
+        $phone = $result["phone"];\r
+//        $check_amount = intval($result["check_amount"] ?? 0);\r
+//        $check_id = $result["check_id"] ?? '';\r
+//        $check_name = $result["check_name"] ?? '';\r
+//        $items = $result['items'] ?? [];\r
+\r
+        $mess = [];\r
+\r
+        $user = Users::find()->where(['phone' => $result['phone']])->andWhere(['phone_true' => '1'])->one();\r
+        if (!$user) {\r
+            $mess["error"] = "Покупателя $phone нет в бонусной программе!";\r
+\r
+            return $this->asJson($mess);\r
+        }\r
+\r
+//        $user_balans = ClientHelper::getBonusBalance($phone);\r
+//        $max = $itogo * self::$MAX_PROCENT;  // максимально можем разрешить списывать до 30 процентов от суммы заказа\r
+//        $max = ceil($max);\r
+//        $available_bonus = $user_balans;\r
+//        if ($available_bonus > $max) { // если баллов бонусов больше чем 30 процентов списываем по максимуму 30\r
+//            $available_bonus = $max;\r
+//        }\r
+//        $baza = $check_amount - $available_bonus;\r
+//        $back = ceil(self::$CREDIT_PROCENT * $baza);\r
+\r
+\r
+//        foreach ($items as $k => $mass) {\r
+//            $seller_id_item = $mass["seller_id"];\r
+//            $product_id = $mass["product_id"];\r
+//            $price = $mass["price"];\r
+//            $quantity = $mass["quantity"];\r
+//            $sm = $price * $quantity;\r
+//            $info .=" id=$product_id ($quantity шт. x $price руб.)  = $sm руб.,";\r
+//            $itogo += $sm;\r
+//\r
+//            //получаем внутренний ID товара\r
+//            $item_id = ClientHelper::getExportId($product_id, "products",1);\r
+//        }\r
+\r
+        LogService::apiLogs(1, json_encode($mess, JSON_UNESCAPED_UNICODE));\r
+\r
+        return $this->asJson(["NOT IMPLEMENTED"]);\r
+    }\r
+\r
+    public function actionGetSettings()\r
+    {\r
+        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;\r
+        $data = file_get_contents('php://input');\r
+        $result = json_decode($data, true);\r
+\r
+        $__API_PARAMS = ['store_id', 'seller_id'];\r
+\r
+        foreach ($__API_PARAMS as $paramName) {\r
+            if (!isset($result[$paramName])) {\r
+\r
+                LogService::apiErrorLog(json_encode(["error_id" => 1, "error" => "$paramName is required"], JSON_UNESCAPED_UNICODE));\r
+\r
+                return $this->asJson(["error_id" => 1, "error" => "$paramName is required"]);\r
+            }\r
+        }\r
+\r
+        $store_id = $result['store_id'];\r
+        $seller_id = $result['seller_id'];\r
+\r
+        $mess = [];\r
+        $mess["result"] = true;\r
+        $mess["attempts_auth_code"] = 5; // Количество возможных попыток ввода кода подтверждения\r
+        $mess["list_events"] = [\r
+            ["id" => 1, "name" => "День рождения"],\r
+            ["id" => 2, "name" => "8 марта"],\r
+            ["id" => 3, "name" => "День матери"],\r
+            ["id" => 4, "name" => "День влюбленных"],\r
+            ["id" => 5, "name" => "День свадьбы"],\r
+            ["id" => 6, "name" => "Другое"]\r
+        ];\r
+        $mess["send_current_items"] = false; // Отправлять или нет текущие позиции чека\r
+\r
+        LogService::apiLogs(1, json_encode($mess, JSON_UNESCAPED_UNICODE));\r
+\r
+        return $this->asJson($mess);\r
+    }\r
+\r
+    public function actionGetUnusedNumenclatur() {\r
+        $items_arr_no = array_values(ArrayHelper::map(\r
+            UniversalCatalogItem::find()->where(['catalog_alias' => 'unused_nomenclature'])->all(), 'guid', 'guid'));\r
+        return $this->asJson(['unused_nomenclature' => $items_arr_no]);\r
+    }\r
+\r
+    public function actionGetContest001Participant() {\r
+        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;\r
+\r
+        $request = Yii::$app->request->getRawBody();\r
+\r
+        try {\r
+            $result = Json::decode($request);\r
+        } catch (\Exception $ex) {\r
+            return $this->asJson(['error' => ['code' => 400, 'message' => 'Json body invalid']]);\r
+        }\r
+\r
+        if (!isset($result['phone'])) {\r
+            return $this->asJson(["error_id" => 1, "error" => "phone is required"]);\r
+        }\r
+\r
+        $phone = ClientHelper::phoneClear($result['phone']);\r
+        if (!ClientHelper::phoneVerify($phone)) {\r
+            return $this->asJson(["error_id" => 1.2, "error" => "phone is required"]);\r
+        }\r
+        $result['phone'] = $phone;\r
+\r
+        $mess = [];\r
+\r
+        $contestants = Contest001::find()->where(['phone' => $phone])->all();\r
+        if (count($contestants) > 0) {\r
+            $mess['is_participant'] = true;\r
+            $raffle_numbers = [];\r
+            foreach ($contestants as $contestant) {\r
+                $raffle_numbers[] = $contestant->number;\r
+            }\r
+            $mess['raffle_numbers'] = implode(', ', $raffle_numbers);\r
+        } else {\r
+            $mess['is_participant'] = false;\r
+        }\r
+\r
+        LogService::apiLogs(1, json_encode($mess, JSON_UNESCAPED_UNICODE));\r
+\r
+        return $this->asJson(['response' => $mess]);\r
+    }\r
+\r
+    public function actionAdd() {\r
+        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;\r
+\r
+        $request = Yii::$app->request->getRawBody();\r
+\r
+        $requestTest = json_decode(\Yii::$app->getRequest()->getRawBody(), true);\r
+\r
+        $fl = date('_Y_m_d__H_i_s_');\r
+\r
+        file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', implode(',', $requestTest));\r
+        file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        try {\r
+            $result = Json::decode($request);\r
+            file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        } catch (\Exception $ex) {\r
+            file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            return $this->asJson(['error' => ['code' => 400, 'message' => 'Json body invalid']]);\r
+        }\r
+\r
+        $__API_PARAMS = ['phone', 'description', 'tip_sale', 'bonus', 'date_end'];\r
+\r
+        foreach ($__API_PARAMS as $paramName) {\r
+            if (!isset($result[$paramName])) {\r
+                file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+                if ($paramName != 'phone') {\r
+                    file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+                    LogService::apiErrorLog(json_encode(["error_id" => 1, "error" => "$paramName is required"], JSON_UNESCAPED_UNICODE));\r
+                }\r
+                file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+                return $this->asJson(["error_id" => 1, "error" => "$paramName is required"]);\r
+            }\r
+        }\r
+        file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        //\r
+        if (!in_array($result['tip_sale'], ['podarok', 'senat', 'nino802', 'sale', '14feb', '23feb', '8mar', 'quest001'])) {\r
+            file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            return $this->asJson(["error_id" => 1.1, "error" => "tip_sale не разрешён (podarok, senat, nino802)"]);\r
+        }\r
+\r
+        $phone = ClientHelper::phoneClear($result['phone']);\r
+        file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        if (!ClientHelper::phoneVerify($phone)) {\r
+            file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            return $this->asJson(["error_id" => 1.2, "error" => "phone is required"]);\r
+        }\r
+        $result['phone'] = $phone;\r
+\r
+        $stop = UsersStopList::find()->select(['phone'])->where(['phone' => $result['phone']])->one();\r
+        file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        if ($stop) {\r
+            file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            return $this->asJson(["error_id" => 4, "error" => 'Номер телефона числится в стоп листе']);\r
+        }\r
+\r
+        $bonus = min((int)$result['bonus'], 1000);\r
+        file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        $found = UsersBonus::find()->where(['phone' => $phone])->andWhere(['>=', 'date_start', date('Y-m-d H:i:s', time() - self::$YEAR_PERIOD * 86400)])\r
+            ->andWhere(['tip_sale' => $result['tip_sale']])->andWhere(['tip' => 'plus'])->andWhere(['bonus' => $bonus])->one();\r
+        file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        if ($found) {\r
+            file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            return $this->asJson(["error_id" => 3, "error" => 'Бонусы уже начисляли']);\r
+        }\r
+        file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        $userBonus = new UsersBonus;\r
+        $userBonus->phone = $phone;\r
+        $userBonus->name = $result['description'];\r
+        $userBonus->date = date('Y-m-d H:i:s');\r
+        $userBonus->site_id = 1;\r
+        $userBonus->setka_id = 1;\r
+        $userBonus->tip = 'plus';\r
+        $userBonus->tip_sale = $result['tip_sale'];\r
+        $userBonus->bonus = $bonus;\r
+\r
+        $userBonusDateStart = $result['date_start'] ?? $userBonus->date;\r
+        $userBonus->date_start = date('Y-m-d H:i:s', strtotime($userBonusDateStart));\r
+\r
+        $userBonusDateEnd = $result['date_end'];\r
+        $userBonus->date_end = date('Y-m-d H:i:s', strtotime($userBonusDateEnd));\r
+\r
+        $userBonus->save();\r
+        file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+        if ($userBonus->getErrors()) {\r
+            file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\r
+            LogService::apiErrorLog(json_encode(["error_id" => 2, "error" => $userBonus->getErrors()], JSON_UNESCAPED_UNICODE));\r
+            return $this->asJson(["error_id" => 2, "error" => $userBonus->getErrors()]);\r
+        }\r
+        file_put_contents(self::OUT_DIR . '/add_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__ . ' OK', FILE_APPEND);\r
+        return $this->asJson(['response' => true]);\r
+    }\r
+\r
+    /**\r
+     * TO8-22: Активация промокода БЛАГО.\r
+     * POST /bonus/activate-promocode\r
+     * Параметры: phone, code\r
+     *\r
+     * Логика:\r
+     * 1. Найти промокод по коду\r
+     * 2. Проверить isActivatable()\r
+     * 3. Найти клиента по телефону\r
+     * 4. В транзакции: начислить 350 промо-бонусов (tip_sale='promobonus'), пометить промокод used=1\r
+     */\r
+    public function actionActivatePromocode()\r
+    {\r
+        Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;\r
+        $data = file_get_contents('php://input');\r
+        $result = json_decode($data, true);\r
+\r
+        if (empty($result['code'])) {\r
+            return $this->asJson(["error_id" => 1, "error" => "code is required"]);\r
+        }\r
+        if (empty($result['phone'])) {\r
+            return $this->asJson(["error_id" => 4, "error" => "phone is required"]);\r
+        }\r
+\r
+        $phone = ClientHelper::phoneClear($result['phone']);\r
+        if (!ClientHelper::phoneVerify($phone)) {\r
+            return $this->asJson(["error_id" => 4, "error" => "phone is not valid"]);\r
+        }\r
+\r
+        $promocode = Promocode::find()\r
+            ->where(['code' => $result['code']])\r
+            ->one();\r
+\r
+        if (!$promocode) {\r
+            return $this->asJson(["error_id" => 1, "error" => "Промокод не найден"]);\r
+        }\r
+\r
+        $activatable = $promocode->isActivatable();\r
+        if ($activatable !== true) {\r
+            $errorMessages = [\r
+                1 => "Промокод неактивен",\r
+                2 => "Промокод уже использован",\r
+                3 => "Срок действия промокода истёк",\r
+            ];\r
+            return $this->asJson([\r
+                "error_id" => $activatable,\r
+                "error" => $errorMessages[$activatable] ?? "Промокод недоступен",\r
+            ]);\r
+        }\r
+\r
+        $user = Users::find()\r
+            ->where(['phone' => $phone])\r
+            ->andWhere(['phone_true' => '1'])\r
+            ->one();\r
+\r
+        if (!$user) {\r
+            return $this->asJson(["error_id" => 4, "error" => "Пользователь с телефоном $phone не найден"]);\r
+        }\r
+\r
+        $transaction = Yii::$app->db->beginTransaction();\r
+        try {\r
+            $bonusAmount = $promocode->bonus ?: 350;\r
+\r
+            $usersBonus = new UsersBonus();\r
+            $usersBonus->phone = $phone;\r
+            $usersBonus->name = "Активация промокода {$promocode->code}";\r
+            $usersBonus->date = date('Y-m-d H:i:s');\r
+            $usersBonus->tip = 'plus';\r
+            $usersBonus->tip_sale = Promocode::TIP_SALE_PROMOBONUS;\r
+            $usersBonus->bonus = $bonusAmount;\r
+            $usersBonus->price = 0;\r
+            $usersBonus->price_skidka = 0;\r
+            $usersBonus->user_id = $user->id;\r
+            $usersBonus->store_id = 0;\r
+            $usersBonus->site_id = 0;\r
+            $usersBonus->setka_id = 0;\r
+            $usersBonus->referal_id = 0;\r
+            $usersBonus->admin_id = 0;\r
+            $usersBonus->lid_id = 0;\r
+            $usersBonus->date_start = date('Y-m-d H:i:s');\r
+            $usersBonus->date_end = date('Y-m-d H:i:s', strtotime('+' . self::$YEAR_PERIOD . ' day'));\r
+            $usersBonus->date_dell = $usersBonus->date_end;\r
+            $usersBonus->ip = $_SERVER['REMOTE_ADDR'] ?? '';\r
+\r
+            if (!$usersBonus->save()) {\r
+                throw new \Exception('Ошибка сохранения бонуса: ' . json_encode($usersBonus->getErrors()));\r
+            }\r
+\r
+            $promocode->used = Promocode::USED_YES;\r
+            $promocode->activated_by = $user->id;\r
+            $promocode->activated_at = date('Y-m-d H:i:s');\r
+            if (!$promocode->save(false)) {\r
+                throw new \Exception('Ошибка обновления промокода: ' . json_encode($promocode->getErrors()));\r
+            }\r
+\r
+            $transaction->commit();\r
+\r
+            Yii::info("Промокод {$promocode->code} активирован для {$phone}, бонус={$bonusAmount}", self::LOG_CATEGORY_BONUS);\r
+\r
+            return $this->asJson([\r
+                "success" => true,\r
+                "bonus" => $bonusAmount,\r
+                "message" => "Промокод активирован, начислено {$bonusAmount} бонусов",\r
+            ]);\r
+        } catch (\Exception $e) {\r
+            $transaction->rollBack();\r
+            LogService::apiErrorLog(json_encode([\r
+                "error_id" => 10,\r
+                "error" => $e->getMessage(),\r
+                "phone" => $phone,\r
+                "code" => $result['code'],\r
+            ], JSON_UNESCAPED_UNICODE));\r
+            return $this->asJson(["error_id" => 10, "error" => "Ошибка активации промокода"]);\r
+        }\r
+    }\r
+}\r
diff --git a/erp24/migrations/m260306_100000_add_activated_fields_to_promocode.php b/erp24/migrations/m260306_100000_add_activated_fields_to_promocode.php
new file mode 100644 (file)
index 0000000..9abfc78
--- /dev/null
@@ -0,0 +1,29 @@
+<?php\r
+\r
+use yii\db\Migration;\r
+\r
+/**\r
+ * TO8-22: Поля трекинга активации промокода.\r
+ */\r
+class m260306_100000_add_activated_fields_to_promocode extends Migration\r
+{\r
+    public function safeUp()\r
+    {\r
+        $this->addColumn(\r
+            '{{%erp24.promocode}}',\r
+            'activated_by',\r
+            $this->integer()->null()->comment('ID клиента, активировавшего промокод')\r
+        );\r
+        $this->addColumn(\r
+            '{{%erp24.promocode}}',\r
+            'activated_at',\r
+            $this->dateTime()->null()->comment('Дата и время активации промокода')\r
+        );\r
+    }\r
+\r
+    public function safeDown()\r
+    {\r
+        $this->dropColumn('{{%erp24.promocode}}', 'activated_at');\r
+        $this->dropColumn('{{%erp24.promocode}}', 'activated_by');\r
+    }\r
+}\r
index 134b3245468022cd3918c521410f3af46aaa5d6b..36e4b352a4eb8b9246a388e2061bf90f8f988f85 100644 (file)
-<?php
-
-namespace yii_app\records;
-
-use Yii;
-
-/**
- * This is the model class for table "promocode".
- *
- * @property int $id
- * @property string $code Промокод
- * @property int $bonus Количество бонусов получаемых по промокоду
- * @property int $duration Продолжительность действия бонуса
- * @property int $active 0 - не активный, 1 - активный
- * @property int $used 0 - не использован, 1 - использован
- * @property int $base 0 - многоразовый промокод, 1 - база для одноразовых, 2 - одноразовый промокод
- * @property int|null $parent_id ID промокода базы
- * @property string $date_start Дата начала действия промокода
- * @property string $date_end Дата окончания действия промокода
- * @property int $created_by ID создателя записи
- * @property int|null $updated_by ID редактора записи
- * @property string $created_at Дата создания
- * @property string|null $updated_at Дата изменения
- * @property bool $is_promo_balance Начислять в промо-баланс (а не в обычные бонусы)
- */
-class Promocode extends \yii\db\ActiveRecord
-{
-    const ACTIVE_ON = 1;
-    const ACTIVE_OFF = 0;
-    const USED_YES = 1;
-    const USED_NO = 0;
-    const BASE_SHARED = 0;
-    const BASE_BASE = 1;
-    const BASE_SINGLE_USE = 2;
-
-    const FORMAT_DIGITS = 'digits';
-    const FORMAT_ALPHANUMERIC = 'alphanumeric';
-
-    public $generatePromocodeCount;
-    public $generateFormat = self::FORMAT_DIGITS;
-
-    public static function tableName() { return 'promocode'; }
-
-    public function rules() {
-        return [
-            [['code', 'bonus', 'duration', 'active', 'date_start', 'date_end', 'created_by', 'created_at'], 'required'],
-            [['code'], 'string', 'max' => 20],
-            [['bonus', 'duration', 'active', 'used', 'base', 'parent_id', 'created_by', 'updated_by', 'is_promo_balance'], 'integer'],
-            [['date_start', 'date_end', 'created_at', 'updated_at'], 'datetime', 'format' => 'php:Y-m-d H:i:s'],
-            [['updated_by', 'updated_at', 'generatePromocodeCount', 'generateFormat'], 'safe'],
-            ['generateFormat', 'in', 'range' => [self::FORMAT_DIGITS, self::FORMAT_ALPHANUMERIC]],
-        ];
-    }
-
-    public function attributeLabels() {
-        return [
-            'id' => 'ID',
-            'code' => 'Code',
-            'bonus' => 'Bonus',
-            'duration' => 'Duration',
-            'active' => 'Active',
-            'used' => 'Used',
-            'base' => 'Base',
-            'parent_id' => 'Parent ID',
-            'date_start' => 'Date Start',
-            'date_end' => 'Date End',
-            'created_by' => 'Created By',
-            'updated_by' => 'Updated By',
-            'created_at' => 'Created At',
-            'updated_at' => 'Updated At',
-        ];
-    }
-
-    public function getCreatedBy() {
-        return $this->hasOne(Admin::class, ['id' => 'created_by']);
-    }
-
-    public function getUpdatedBy() {
-        return $this->hasOne(Admin::class, ['id' => 'updated_by']);
-    }
-
-    public function getParent() {
-        return $this->hasOne(Promocode::class, ['id' => 'parent_id']);
-    }
-}
+<?php\r
+\r
+namespace yii_app\records;\r
+\r
+use Yii;\r
+\r
+/**\r
+ * This is the model class for table "promocode".\r
+ *\r
+ * @property int $id\r
+ * @property string $code Промокод\r
+ * @property int $bonus Количество бонусов получаемых по промокоду\r
+ * @property int $duration Продолжительность действия бонуса\r
+ * @property int $active 0 - не активный, 1 - активный\r
+ * @property int $used 0 - не использован, 1 - использован\r
+ * @property int $base 0 - многоразовый промокод, 1 - база для одноразовых, 2 - одноразовый промокод\r
+ * @property int|null $parent_id ID промокода базы\r
+ * @property string $date_start Дата начала действия промокода\r
+ * @property string $date_end Дата окончания действия промокода\r
+ * @property int $created_by ID создателя записи\r
+ * @property int|null $updated_by ID редактора записи\r
+ * @property string $created_at Дата создания\r
+ * @property string|null $updated_at Дата изменения\r
+ * @property bool $is_promo_balance Начислять в промо-баланс (а не в обычные бонусы)\r
+ * @property int|null $activated_by ID клиента, активировавшего промокод\r
+ * @property string|null $activated_at Дата и время активации промокода\r
+ */\r
+class Promocode extends \yii\db\ActiveRecord\r
+{\r
+    const ACTIVE_ON = 1;\r
+    const ACTIVE_OFF = 0;\r
+    const USED_YES = 1;\r
+    const USED_NO = 0;\r
+    const BASE_SHARED = 0;\r
+    const BASE_BASE = 1;\r
+    const BASE_SINGLE_USE = 2;\r
+\r
+    const FORMAT_DIGITS = 'digits';\r
+    const FORMAT_ALPHANUMERIC = 'alphanumeric';\r
+\r
+    const TIP_SALE_PROMOBONUS = 'promobonus';\r
+\r
+    public $generatePromocodeCount;\r
+    public $generateFormat = self::FORMAT_DIGITS;\r
+\r
+    public static function tableName() { return 'promocode'; }\r
+\r
+    public function rules() {\r
+        return [\r
+            [['code', 'bonus', 'duration', 'active', 'date_start', 'date_end', 'created_by', 'created_at'], 'required'],\r
+            [['code'], 'string', 'max' => 20],\r
+            [['bonus', 'duration', 'active', 'used', 'base', 'parent_id', 'created_by', 'updated_by', 'is_promo_balance'], 'integer'],\r
+            [['date_start', 'date_end', 'created_at', 'updated_at'], 'datetime', 'format' => 'php:Y-m-d H:i:s'],\r
+            [['activated_by'], 'integer'],\r
+            [['activated_at'], 'datetime', 'format' => 'php:Y-m-d H:i:s'],\r
+            [['updated_by', 'updated_at', 'activated_by', 'activated_at', 'generatePromocodeCount', 'generateFormat'], 'safe'],\r
+            ['generateFormat', 'in', 'range' => [self::FORMAT_DIGITS, self::FORMAT_ALPHANUMERIC]],\r
+        ];\r
+    }\r
+\r
+    public function attributeLabels() {\r
+        return [\r
+            'id' => 'ID',\r
+            'code' => 'Code',\r
+            'bonus' => 'Bonus',\r
+            'duration' => 'Duration',\r
+            'active' => 'Active',\r
+            'used' => 'Used',\r
+            'base' => 'Base',\r
+            'parent_id' => 'Parent ID',\r
+            'date_start' => 'Date Start',\r
+            'date_end' => 'Date End',\r
+            'created_by' => 'Created By',\r
+            'updated_by' => 'Updated By',\r
+            'created_at' => 'Created At',\r
+            'updated_at' => 'Updated At',\r
+        ];\r
+    }\r
+\r
+    public function getCreatedBy() {\r
+        return $this->hasOne(Admin::class, ['id' => 'created_by']);\r
+    }\r
+\r
+    public function getUpdatedBy() {\r
+        return $this->hasOne(Admin::class, ['id' => 'updated_by']);\r
+    }\r
+\r
+    public function getParent() {\r
+        return $this->hasOne(Promocode::class, ['id' => 'parent_id']);\r
+    }\r
+\r
+    /**\r
+     * Проверяет, можно ли активировать промокод.\r
+     * @return int|true true если можно, иначе error_id:\r
+     *   1 — не найден/неактивен\r
+     *   2 — уже использован\r
+     *   3 — просрочен\r
+     */\r
+    public function isActivatable()\r
+    {\r
+        if ($this->active != self::ACTIVE_ON) {\r
+            return 1;\r
+        }\r
+        if ($this->used == self::USED_YES) {\r
+            return 2;\r
+        }\r
+        $now = date('Y-m-d H:i:s');\r
+        if ($this->date_start > $now || $this->date_end < $now) {\r
+            return 3;\r
+        }\r
+        return true;\r
+    }\r
+}\r
diff --git a/erp24/tests/unit/controllers/BonusControllerPromoTest.php b/erp24/tests/unit/controllers/BonusControllerPromoTest.php
new file mode 100644 (file)
index 0000000..27669f8
--- /dev/null
@@ -0,0 +1,215 @@
+<?php\r
+\r
+namespace app\tests\unit\controllers;\r
+\r
+use Codeception\Test\Unit;\r
+use yii_app\records\Promocode;\r
+\r
+/**\r
+ * TO8-22: Тесты логики промо-списания БЛАГО.\r
+ *\r
+ * Тестируем алгоритм выбора: стандартное списание vs промо-списание.\r
+ * Алгоритм из actionSale():\r
+ *   1. standard_max = amount_real * bonus_rate\r
+ *   2. promo_balance >= 350 AND check_amount >= 1700 AND 350 > standard_max => промо\r
+ *   3. Иначе => стандартное списание\r
+ */\r
+class BonusControllerPromoTest extends Unit\r
+{\r
+    /**\r
+     * Промо-списание применяется: покупка 2000р, промо-баланс 350р, стандарт 200р (10%).\r
+     * 350 > 200 => промо выгоднее.\r
+     */\r
+    public function testPromoAppliedWhenBetter()\r
+    {\r
+        $result = $this->calculateWriteOffChoice(\r
+            checkAmount: 2000,\r
+            amountReal: 2000,\r
+            bonusRate: 0.10,\r
+            promoBalance: 350\r
+        );\r
+\r
+        $this->assertTrue($result['usePromoWriteOff']);\r
+        $this->assertSame(350, $result['writeOffBonuses']);\r
+    }\r
+\r
+    /**\r
+     * Стандартное списание лучше: покупка 5000р, стандарт 500р (10%).\r
+     * 350 < 500 => стандартное выгоднее.\r
+     */\r
+    public function testStandardBetterThanPromo()\r
+    {\r
+        $result = $this->calculateWriteOffChoice(\r
+            checkAmount: 5000,\r
+            amountReal: 5000,\r
+            bonusRate: 0.10,\r
+            promoBalance: 350\r
+        );\r
+\r
+        $this->assertFalse($result['usePromoWriteOff']);\r
+        $this->assertSame(500, $result['writeOffBonuses']);\r
+    }\r
+\r
+    /**\r
+     * Покупка слишком маленькая (< 1700р) — промо-списание не применяется.\r
+     */\r
+    public function testCheckTooSmallForPromo()\r
+    {\r
+        $result = $this->calculateWriteOffChoice(\r
+            checkAmount: 1500,\r
+            amountReal: 1500,\r
+            bonusRate: 0.10,\r
+            promoBalance: 350\r
+        );\r
+\r
+        $this->assertFalse($result['usePromoWriteOff']);\r
+        $this->assertSame(150, $result['writeOffBonuses']);\r
+    }\r
+\r
+    /**\r
+     * Недостаточный промо-баланс (< 350р) — стандартное списание.\r
+     */\r
+    public function testInsufficientPromoBalance()\r
+    {\r
+        $result = $this->calculateWriteOffChoice(\r
+            checkAmount: 2000,\r
+            amountReal: 2000,\r
+            bonusRate: 0.10,\r
+            promoBalance: 100\r
+        );\r
+\r
+        $this->assertFalse($result['usePromoWriteOff']);\r
+        $this->assertSame(200, $result['writeOffBonuses']);\r
+    }\r
+\r
+    /**\r
+     * При промо-списании кэшбек НЕ начисляется.\r
+     */\r
+    public function testNoCashbackWithPromoWriteOff()\r
+    {\r
+        $result = $this->calculateWriteOffChoice(\r
+            checkAmount: 2000,\r
+            amountReal: 2000,\r
+            bonusRate: 0.10,\r
+            promoBalance: 350\r
+        );\r
+\r
+        $this->assertTrue($result['usePromoWriteOff']);\r
+        // При промо-списании cashback = 0\r
+        $this->assertSame(0, $result['cashback']);\r
+    }\r
+\r
+    /**\r
+     * При стандартном списании кэшбек начисляется нормально.\r
+     */\r
+    public function testCashbackWithStandardWriteOff()\r
+    {\r
+        $result = $this->calculateWriteOffChoice(\r
+            checkAmount: 5000,\r
+            amountReal: 5000,\r
+            bonusRate: 0.10,\r
+            promoBalance: 350,\r
+            cashbackRate: 0.10\r
+        );\r
+\r
+        $this->assertFalse($result['usePromoWriteOff']);\r
+        $this->assertGreaterThan(0, $result['cashback']);\r
+    }\r
+\r
+    /**\r
+     * Граничный случай: покупка ровно 1700р — промо применяется (>=).\r
+     */\r
+    public function testExactMinCheckAmount()\r
+    {\r
+        $result = $this->calculateWriteOffChoice(\r
+            checkAmount: 1700,\r
+            amountReal: 1700,\r
+            bonusRate: 0.10,\r
+            promoBalance: 350\r
+        );\r
+\r
+        // standard_max = 1700 * 0.10 = 170, 350 > 170 => промо\r
+        $this->assertTrue($result['usePromoWriteOff']);\r
+    }\r
+\r
+    /**\r
+     * Граничный случай: промо-баланс ровно 350 — промо применяется (>=).\r
+     */\r
+    public function testExactPromoBalance()\r
+    {\r
+        $result = $this->calculateWriteOffChoice(\r
+            checkAmount: 2000,\r
+            amountReal: 2000,\r
+            bonusRate: 0.10,\r
+            promoBalance: 350\r
+        );\r
+\r
+        $this->assertTrue($result['usePromoWriteOff']);\r
+    }\r
+\r
+    /**\r
+     * При промо-списании tip_sale должен быть 'promobonus'.\r
+     */\r
+    public function testPromoTipSaleValue()\r
+    {\r
+        $result = $this->calculateWriteOffChoice(\r
+            checkAmount: 2000,\r
+            amountReal: 2000,\r
+            bonusRate: 0.10,\r
+            promoBalance: 350\r
+        );\r
+\r
+        $this->assertSame(Promocode::TIP_SALE_PROMOBONUS, $result['tipSale']);\r
+    }\r
+\r
+    /**\r
+     * Воспроизводит алгоритм выбора списания из actionSale().\r
+     * Это чистая логика без БД — тестируем алгоритм.\r
+     */\r
+    private function calculateWriteOffChoice(\r
+        int $checkAmount,\r
+        int $amountReal,\r
+        float $bonusRate,\r
+        float $promoBalance,\r
+        float $cashbackRate = 0.10,\r
+        int $userBalance = 10000,\r
+        int $requestedWriteOff = 0\r
+    ): array {\r
+        $promoWriteOffAmount = 350;\r
+        $promoMinCheckAmount = 1700;\r
+\r
+        // Стандартный расчёт\r
+        $writeOffBonusesTheory = (int) round($amountReal * $bonusRate);\r
+        $writeOffBonuses = $requestedWriteOff ?: $writeOffBonusesTheory;\r
+        if ($writeOffBonuses > $writeOffBonusesTheory) {\r
+            $writeOffBonuses = $writeOffBonusesTheory;\r
+        }\r
+        if ($userBalance < $writeOffBonuses) {\r
+            $writeOffBonuses = $userBalance;\r
+        }\r
+\r
+        // Промо-проверка\r
+        $usePromoWriteOff = false;\r
+        if ($promoBalance >= $promoWriteOffAmount\r
+            && $checkAmount >= $promoMinCheckAmount\r
+            && $promoWriteOffAmount > $writeOffBonusesTheory\r
+        ) {\r
+            $usePromoWriteOff = true;\r
+            $writeOffBonuses = $promoWriteOffAmount;\r
+        }\r
+\r
+        // Кэшбек\r
+        $bazaBack = $amountReal - $writeOffBonuses;\r
+        $cashback = $usePromoWriteOff ? 0 : (int) round($bazaBack * $cashbackRate);\r
+\r
+        // tip_sale\r
+        $tipSale = $usePromoWriteOff ? Promocode::TIP_SALE_PROMOBONUS : 'sale';\r
+\r
+        return [\r
+            'usePromoWriteOff' => $usePromoWriteOff,\r
+            'writeOffBonuses' => $writeOffBonuses,\r
+            'cashback' => $cashback,\r
+            'tipSale' => $tipSale,\r
+        ];\r
+    }\r
+}\r
diff --git a/erp24/tests/unit/records/PromocodePromoTest.php b/erp24/tests/unit/records/PromocodePromoTest.php
new file mode 100644 (file)
index 0000000..ce67c6a
--- /dev/null
@@ -0,0 +1,90 @@
+<?php\r
+\r
+namespace app\tests\unit\records;\r
+\r
+use Codeception\Test\Unit;\r
+use yii_app\records\Promocode;\r
+\r
+/**\r
+ * TO8-22: Тесты модели Promocode — метод isActivatable() и константы.\r
+ */\r
+class PromocodePromoTest extends Unit\r
+{\r
+    /**\r
+     * Активный, неиспользованный промокод в пределах дат — можно активировать.\r
+     */\r
+    public function testIsActivatableSuccess()\r
+    {\r
+        $promo = new Promocode();\r
+        $promo->active = Promocode::ACTIVE_ON;\r
+        $promo->used = Promocode::USED_NO;\r
+        $promo->date_start = date('Y-m-d H:i:s', strtotime('-1 day'));\r
+        $promo->date_end = date('Y-m-d H:i:s', strtotime('+30 day'));\r
+\r
+        $this->assertTrue($promo->isActivatable());\r
+    }\r
+\r
+    /**\r
+     * Уже использованный промокод — error_id=2.\r
+     */\r
+    public function testIsActivatableAlreadyUsed()\r
+    {\r
+        $promo = new Promocode();\r
+        $promo->active = Promocode::ACTIVE_ON;\r
+        $promo->used = Promocode::USED_YES;\r
+        $promo->date_start = date('Y-m-d H:i:s', strtotime('-1 day'));\r
+        $promo->date_end = date('Y-m-d H:i:s', strtotime('+30 day'));\r
+\r
+        $this->assertSame(2, $promo->isActivatable());\r
+    }\r
+\r
+    /**\r
+     * Неактивный промокод — error_id=1.\r
+     */\r
+    public function testIsActivatableInactive()\r
+    {\r
+        $promo = new Promocode();\r
+        $promo->active = Promocode::ACTIVE_OFF;\r
+        $promo->used = Promocode::USED_NO;\r
+        $promo->date_start = date('Y-m-d H:i:s', strtotime('-1 day'));\r
+        $promo->date_end = date('Y-m-d H:i:s', strtotime('+30 day'));\r
+\r
+        $this->assertSame(1, $promo->isActivatable());\r
+    }\r
+\r
+    /**\r
+     * Просроченный промокод (date_end в прошлом) — error_id=3.\r
+     */\r
+    public function testIsActivatableExpired()\r
+    {\r
+        $promo = new Promocode();\r
+        $promo->active = Promocode::ACTIVE_ON;\r
+        $promo->used = Promocode::USED_NO;\r
+        $promo->date_start = date('Y-m-d H:i:s', strtotime('-30 day'));\r
+        $promo->date_end = date('Y-m-d H:i:s', strtotime('-1 day'));\r
+\r
+        $this->assertSame(3, $promo->isActivatable());\r
+    }\r
+\r
+    /**\r
+     * Промокод ещё не начал действовать (date_start в будущем) — error_id=3.\r
+     */\r
+    public function testIsActivatableNotStartedYet()\r
+    {\r
+        $promo = new Promocode();\r
+        $promo->active = Promocode::ACTIVE_ON;\r
+        $promo->used = Promocode::USED_NO;\r
+        $promo->date_start = date('Y-m-d H:i:s', strtotime('+1 day'));\r
+        $promo->date_end = date('Y-m-d H:i:s', strtotime('+30 day'));\r
+\r
+        $this->assertSame(3, $promo->isActivatable());\r
+    }\r
+\r
+    /**\r
+     * Константа TIP_SALE_PROMOBONUS имеет правильное значение.\r
+     */\r
+    public function testTipSalePromobonusConstant()\r
+    {\r
+        $this->assertSame('promobonus', Promocode::TIP_SALE_PROMOBONUS);\r
+    }\r
+}\r
diff --git a/plan_email.md b/plan_email.md
new file mode 100644 (file)
index 0000000..fc1ac92
--- /dev/null
@@ -0,0 +1,293 @@
+План: Улучшение системы регистрации и обработки писем Flowwow
+
+Контекст
+
+Крон-задача php yii marketplace/get-flowwow-orders читает письма от Flowwow через IMAP и создаёт/обновляет заказы в системе. Текущие проблемы:
+
+1. Нет разделения "регистрация" / "обработка" — email_status=1 ставится ДО processMessage() (строка 2203-2205), поэтому при сбое обработки письмо уже помечено как       
+   обработанное
+2. Повторная обработка невозможна — если saveEmailIfNotExists вернул null (письмо есть), processMessage() всё равно вызывается, но нет контроля завершённости обработки
+3. SEEN ставится только для новых заказов — processFlowwowOrders возвращает счётчик только для NEW (строка 2721), а SEEN зависит от $output > 0 (строка 2218). Для       
+   APPROVED/CHANGED/CANCELLED/DELIVERED SEEN не ставится
+4. Нет индексов на таблице marketplace_flowwow_emails для проверки дубликатов
+5. Нет отслеживания ошибок — если обработка упала, нет записи почему и сколько раз пытались
+6. Нет связки письмо↔заказ — невозможно из интерфейса писем найти, какие письма относятся к конкретному заказу
+
+Файлы для изменения
+┌──────────────────────────────────────────────────────────┬──────────────────────────────────────────────────────────────────────────────────────┐
+│                           Файл                           │                                      Изменение                                       │
+├──────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────┤
+│ erp24/migrations/m260218_*_improve_flowwow_emails.php    │ Новый — миграция: колонки + индексы                                                  │
+├──────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────┤
+│ erp24/records/MarketplaceFlowwowEmails.php               │ Константы статусов, новые поля, relation, helper-методы                              │
+├──────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────┤
+│ erp24/records/MarketplaceFlowwowEmailsSearch.php         │ Фильтрация по новым полям + поиск по заказу                                          │
+├──────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────┤
+│ erp24/services/MarketplaceService.php                    │ Рефакторинг getFlowwowOrdersFromMail, saveEmailIfNotExists, fix processFlowwowOrders │
+├──────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────┤
+│ erp24/commands/MarketplaceController.php                 │ Новый action actionRetryFlowwowEmails                                                │
+├──────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────┤
+│ erp24/media/controllers/FlowwowController.php            │ Fix actionCheckMail (несовместимый вызов)                                            │
+├──────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────┤
+│ erp24/views/marketplace-flowwow-emails/index.php         │ Обновление GridView: статусы, связка с заказом, поиск                                │
+├──────────────────────────────────────────────────────────┼──────────────────────────────────────────────────────────────────────────────────────┤
+│ erp24/controllers/MarketplaceFlowwowEmailsController.php │ Мелкие правки (если нужны для view)                                                  │
+└──────────────────────────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────────────────┘
+ ---
+Шаг 1. Миграция БД
+
+Файл: erp24/migrations/m260218_000001_improve_marketplace_flowwow_emails.php
+
+Добавить в таблицу marketplace_flowwow_emails:
+┌──────────────────────┬────────────────────┬─────────────────────────────────────────────────────────────────────┐
+│       Колонка        │        Тип         │                              Описание                               │
+├──────────────────────┼────────────────────┼─────────────────────────────────────────────────────────────────────┤
+│ subject_type         │ smallint, NULL     │ Тип письма (1=NEW, 2=APPROVED, 3=CHANGED, 4=CANCELLED, 5=DELIVERED) │
+├──────────────────────┼────────────────────┼─────────────────────────────────────────────────────────────────────┤
+│ processing_attempts  │ integer, default 0 │ Счётчик попыток обработки                                           │
+├──────────────────────┼────────────────────┼─────────────────────────────────────────────────────────────────────┤
+│ processed_at         │ timestamp, NULL    │ Время успешной обработки                                            │
+├──────────────────────┼────────────────────┼─────────────────────────────────────────────────────────────────────┤
+│ error_message        │ text, NULL         │ Текст последней ошибки                                              │
+├──────────────────────┼────────────────────┼─────────────────────────────────────────────────────────────────────┤
+│ marketplace_order_id │ varchar(50), NULL  │ ID заказа (связка с marketplace_orders)                             │
+└──────────────────────┴────────────────────┴─────────────────────────────────────────────────────────────────────┘
+Индексы:
+- idx_flowwow_emails_dedup на (subject, "from", date) — ускорение проверки дубликатов
+- idx_flowwow_emails_status на (email_status) — выборка необработанных
+- idx_flowwow_emails_order на (marketplace_order_id) — связка с заказами
+
+Важно: from — зарезервированное слово PostgreSQL, использовать "from" в кавычках через raw SQL.
+
+ ---
+Шаг 2. Модель MarketplaceFlowwowEmails
+
+Файл: erp24/records/MarketplaceFlowwowEmails.php
+
+2.1 Константы статусов
+public const STATUS_NEW = 0;        // Зарегистрировано, ожидает обработки
+public const STATUS_PROCESSED = 1;  // Успешно обработано
+public const STATUS_ERROR = 2;      // Ошибка (исчерпаны попытки)
+public const STATUS_RETRY = 3;      // Ожидает повторной обработки
+
+public const MAX_PROCESSING_ATTEMPTS = 5;
+
+2.2 Relation с MarketplaceOrders (связка письмо↔заказ)
+/**
+* Связь с заказом маркетплейса по marketplace_order_id.
+* Поле marketplace_order_id в emails хранит ID заказа из маркетплейса (например "123456"),
+* которому соответствует marketplace_orders.marketplace_order_id.
+  */
+  public function getOrder(): \yii\db\ActiveQuery
+  {
+  return $this->hasOne(MarketplaceOrders::class, ['marketplace_order_id' => 'marketplace_order_id'])
+  ->andWhere(['marketplace_id' => \yii_app\records\MarketplaceStore::FLOWWOW_WAREHOUSE_ID]);
+  }
+
+Это позволит:
+- Из письма перейти к заказу: $email->order
+- Из GridView показать ссылку на заказ
+
+2.3 Helper-методы
+
+- markAsProcessed(): bool — ставит STATUS_PROCESSED + processed_at
+- markAsError(string $errorMessage): bool — ставит STATUS_ERROR + error_message + инкремент attempts
+- markForRetry(string $reason): bool — ставит STATUS_RETRY + инкремент attempts
+- isRetryAllowed(): bool — processing_attempts < MAX_PROCESSING_ATTEMPTS
+- static findUnprocessed(): ActiveQuery — WHERE email_status IN (0, 3) AND processing_attempts < MAX
+- static statusLabels(): array — массив текстовых меток статусов
+- static subjectTypeLabels(): array — [1 => 'Новый заказ', 2 => 'Принят', 3 => 'Изменён', 4 => 'Отменён', 5 => 'Доставлен']
+
+2.4 Обновить rules() и attributeLabels()
+
+Добавить правила валидации и метки для новых полей.
+
+ ---
+Шаг 3. Рефакторинг MarketplaceService
+
+3.1 Новый метод detectSubjectType
+private static function detectSubjectType(string $subject): ?int
+
+Определяет тип письма по теме через SUBJECT_INDEX regex patterns.
+
+3.2 Изменение saveEmailIfNotExists (строка 2260)
+
+При создании нового письма дополнительно заполнять subject_type через detectSubjectType(). Остальная логика без изменений — метод по-прежнему возвращает объект если     
+создано, null если уже существует.
+
+3.3 Рефакторинг getFlowwowOrdersFromMail (строки 2198-2242)
+
+Ключевое изменение — логика цикла обработки каждого письма:
+ДЛЯ КАЖДОГО ПИСЬМА ИЗ IMAP:
+├── saveEmailIfNotExists() → регистрация
+├── Если письмо новое (savedEmail != null):
+│   └── emailRecord = savedEmail
+├── Иначе: загружаем из БД:
+│   └── emailRecord = MarketplaceFlowwowEmails::find()->where(...)
+│
+├── Если emailRecord.email_status == STATUS_PROCESSED:
+│   ├── Только ставим SEEN на IMAP (чтобы не читать повторно)
+│   └── continue (пропускаем обработку)
+│
+├── Определяем паттерн темы → subject_index
+├── try:
+│   ├── processMessage($message) → обработка заказа
+│   ├── emailRecord->markAsProcessed() → статус ПОСЛЕ успешной обработки
+│   ├── emailRecord->marketplace_order_id = key($order)
+│   ├── imap_setflag_full(SEEN) → для ВСЕХ успешных, не только NEW
+│   └── countProcessedMessages++
+├── catch (Throwable):
+│   ├── Логирование ошибки
+│   ├── Если isRetryAllowed() → markForRetry()
+│   └── Иначе → markAsError()
+
+Что убираем:
+- Строки 2203-2205: преждевременная установка email_status = 1 ДО обработки
+- Строки 2218: условие if ($output > 0) для SEEN — теперь ставится безусловно
+
+3.4 Fix processFlowwowOrders (строка 2677)
+
+Добавить переменную $processingSuccess = false. Устанавливать в true при:
+- Создании нового заказа (строка 2719, $marketplaceOrder->save())
+- Успешном обновлении статуса (строки 2749, 2783, 2798)
+
+Изменить return (строка 2808):
+return $processingSuccess ? max($newOrdersCount, 1) : 0;
+
+Это обеспечит что $output > 0 для всех типов писем, а не только NEW.
+
+3.5 Новый метод processUnprocessedEmails
+public static function processUnprocessedEmails(?callable $progressCallback = null): array
+
+Выбирает из БД все письма со статусом STATUS_NEW или STATUS_RETRY (через findUnprocessed()), у которых заполнен subject_type, и обрабатывает каждое через
+processMessage().
+
+Возвращает: ['processed' => int, 'failed' => int, 'total' => int]
+
+Фильтр andWhere(['not', ['subject_type' => null]]) гарантирует, что старые записи (до миграции) не будут затронуты.
+
+ ---
+Шаг 4. Консольная команда для retry
+
+Файл: erp24/commands/MarketplaceController.php
+
+Новый action:
+php yii marketplace/retry-flowwow-emails
+
+Вызывает MarketplaceService::processUnprocessedEmails() с progress callback в консоль. Выводит итоговую статистику.
+
+ ---
+Шаг 5. Fix FlowwowController::actionCheckMail
+
+Файл: erp24/media/controllers/FlowwowController.php (строки 64-82)
+
+Текущий код несовместим: getFlowwowOrdersFromMail возвращает ['processed' => N, 'all' => M], а actionCheckMail вызывает count($messages) (вернёт 2) и
+processMessages($messages) (передаст массив с ключами 'processed'/'all' вместо писем).
+
+Исправить: убрать вызов processMessages, использовать возвращённый массив напрямую.
+
+ ---
+Шаг 6. Обновить Search-модель (поиск писем по заказу)
+
+Файл: erp24/records/MarketplaceFlowwowEmailsSearch.php
+
+6.1 Новые правила фильтрации
+
+- В rules(): добавить subject_type, processing_attempts как integer; processed_at, error_message, marketplace_order_id как safe
+
+6.2 Поиск в search()
+// Фильтрация по новым полям
+$query->andFilterWhere([
+'subject_type' => $this->subject_type,
+'processing_attempts' => $this->processing_attempts,
+]);
+$query->andFilterWhere(['ilike', 'error_message', $this->error_message]);
+
+// Поиск писем по ID заказа маркетплейса
+$query->andFilterWhere(['ilike', 'marketplace_order_id', $this->marketplace_order_id]);
+
+Это позволит:
+- Ввести номер заказа Flowwow в фильтр → увидеть все письма, связанные с этим заказом
+- Фильтровать по типу письма (новый, принят, отменён и т.д.)
+- Фильтровать по статусу обработки
+
+ ---
+Шаг 7. Обновить View (связка с заказами и поиск)
+
+Файл: erp24/views/marketplace-flowwow-emails/index.php
+
+7.1 Обновить статусы
+
+- Обновить отображение email_status — добавить статусы "Ошибка" (красный) и "Повтор" (синий)
+- Заменить текстовый фильтр email_status на dropdown:
+  'filter' => MarketplaceFlowwowEmails::statusLabels(),
+
+7.2 Добавить колонку "Тип письма"
+[
+'attribute' => 'subject_type',
+'value' => fn($model) => MarketplaceFlowwowEmails::subjectTypeLabels()[$model->subject_type] ?? '—',
+'filter' => MarketplaceFlowwowEmails::subjectTypeLabels(),
+],
+
+7.3 Добавить колонку "Заказ" со ссылкой
+[
+'attribute' => 'marketplace_order_id',
+'format' => 'raw',
+'value' => function ($model) {
+if (!$model->marketplace_order_id) {
+return '—';
+}
+$order = $model->order;
+if ($order) {
+return Html::a(
+'№' . $model->marketplace_order_id,
+['/marketplace-orders/view', 'id' => $order->id],
+['class' => 'btn btn-xs btn-outline-primary', 'target' => '_blank']
+);
+}
+return $model->marketplace_order_id . ' (не найден)';
+},
+'filter' => Html::input('text', 'MarketplaceFlowwowEmailsSearch[marketplace_order_id]',
+$searchModel->marketplace_order_id, ['class' => 'form-control', 'placeholder' => '№ заказа']),
+],
+
+Это даёт:
+- Поиск писем по заказу: ввести номер заказа в фильтр → увидеть ВСЕ связанные письма (создание, принятие, изменения, отмена, доставка)
+- Клик по номеру заказа → переход на страницу заказа в ERP
+- Фильтр по типу письма → например, увидеть все "отменённые" письма
+
+7.4 Добавить колонку "Попытки"
+'processing_attempts',
+
+7.5 Eager loading для оптимизации
+
+В контроллере MarketplaceFlowwowEmailsController::actionIndex добавить:
+$dataProvider->query->with(['order']);
+
+ ---
+Порядок реализации
+1.  Миграция БД                              (независимый)
+2.  Модель MarketplaceFlowwowEmails           (зависит от 1)
+3.  MarketplaceFlowwowEmailsSearch            (зависит от 2)
+4.  MarketplaceService::detectSubjectType     (независимый)
+5.  MarketplaceService::saveEmailIfNotExists  (зависит от 2, 4)
+6.  MarketplaceService::processFlowwowOrders  (независимый fix)
+7.  MarketplaceService::getFlowwowOrdersFromMail (зависит от 5, 6)
+8.  MarketplaceService::processUnprocessedEmails (зависит от 2)
+9.  MarketplaceController::actionRetryFlowwowEmails (зависит от 8)
+10. FlowwowController::actionCheckMail fix   (зависит от 7)
+11. MarketplaceFlowwowEmailsController       (eager loading)
+12. View index.php                           (зависит от 2, 3, 11)
+
+ ---
+Верификация
+
+1. Миграция: php yii migrate — проверить создание колонок и индексов
+2. Регистрация: запустить php yii marketplace/get-flowwow-orders, проверить что новые письма получили email_status=0, subject_type заполнен
+3. Обработка: проверить что после processMessage — email_status=1, processed_at заполнен, marketplace_order_id заполнен
+4. Повторный запуск: запустить команду снова — уже обработанные письма получают SEEN и пропускаются
+5. Retry: вручную UPDATE marketplace_flowwow_emails SET email_status=3 WHERE id=..., запустить php yii marketplace/retry-flowwow-emails — повторная обработка
+6. SEEN для всех типов: проверить что SEEN ставится для APPROVED, CANCELLED, DELIVERED (ранее не ставился)
+7. Поиск по заказу: в UI /marketplace-flowwow-emails/index ввести номер заказа Flowwow в фильтр → отображаются все связанные письма
+8. Ссылка на заказ: кликнуть по номеру заказа в колонке → переход на /marketplace-orders/view?id=...
+9. Фильтр по типу: выбрать "Отменён" в dropdown типа письма → только отменённые письма