$promoWriteOffAmount = 350;\r
$promoMinCheckAmount = 1700;\r
\r
+ $now = date('Y-m-d H:i:s');\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
+ ->andWhere(['<=', 'date_start', $now])\r
+ ->andWhere(['>=', 'date_end', $now])\r
->sum('bonus');\r
$promoMinusSum = (float) UsersBonus::find()\r
->where(['phone' => $phone, 'tip' => 'minus', 'tip_sale' => Promocode::TIP_SALE_PROMOBONUS])\r
\r
return $this->asJson($mess);\r
}\r
- $user->burn_balans = max(0, $user->burn_balans - $write_off_bonuses);\r
+ // TO8-22: При промо-списании burn_balans не трогаем — списываются промо-бонусы, а не обычные\r
+ if (!$usePromoWriteOff) {\r
+ $user->burn_balans = max(0, $user->burn_balans - $write_off_bonuses);\r
+ }\r
// [balans - burn_balance, burn_balans] - показать клиенту что мы сожгли сжигаемый баланс\r
\r
// старая точка проверки кода\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
+ $tipSaleForCheck = $usePromoWriteOff ? Promocode::TIP_SALE_PROMOBONUS : 'sale';\r
+ $writeOffAlready = UsersBonus::find()->where(['lid_id' => $lid_id, 'phone' => $phone, 'tip_sale' => $tipSaleForCheck, 'tip' => 'minus'])->one() != null;\r
}\r
\r
file_put_contents(self::OUT_DIR . '/sale_bonuses_' . $fl . '.json', PHP_EOL . '--' . __LINE__, FILE_APPEND);\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
+ // TO8-22: При промо-списании обычный баланс не уменьшается — списываются промо-бонусы\r
+ $user_balans_new = $usePromoWriteOff ? $user_balans : ($user_balans - $write_off_bonuses);\r
$tipSaleForWriteOff = $usePromoWriteOff ? Promocode::TIP_SALE_PROMOBONUS : 'sale';\r
$name_b = $usePromoWriteOff\r
? "Списание промо-бонусов БЛАГО по чеку $check_name"\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
+ $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
+ $user->save();\r
+\r
+ if ($user->getErrors()) {\r
+ LogService::apiErrorLog(json_encode(["error_id" => 6, "error" => $user->getErrors()], JSON_UNESCAPED_UNICODE));\r
+ return $this->asJson(["error_id" => 6, "error" => $user->getErrors()]);\r
}\r
\r
+ $this->updateUserBonusLevel($user, $sale_price, $check_id, $check_name);\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["user_balans_actual"] = $user->balans;\r
$mess["promo_writeoff"] = true;\r
\r
return $this->asJson($mess);\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
\r
$transaction = Yii::$app->db->beginTransaction();\r
try {\r
+ // SELECT FOR UPDATE — блокируем строку промокода от параллельной активации\r
+ $promocode = Promocode::find()\r
+ ->where(['code' => $result['code']])\r
+ ->forUpdate()\r
+ ->one();\r
+\r
+ if (!$promocode) {\r
+ $transaction->rollBack();\r
+ return $this->asJson(["error_id" => 1, "error" => "Промокод не найден"]);\r
+ }\r
+\r
+ $activatable = $promocode->isActivatable();\r
+ if ($activatable !== true) {\r
+ $transaction->rollBack();\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
$bonusAmount = $promocode->bonus ?: 350;\r
+ $duration = $promocode->duration ?: self::$YEAR_PERIOD;\r
\r
$usersBonus = new UsersBonus();\r
$usersBonus->phone = $phone;\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_end = date('Y-m-d H:i:s', strtotime('+' . $duration . ' day'));\r
$usersBonus->date_dell = $usersBonus->date_end;\r
$usersBonus->ip = $_SERVER['REMOTE_ADDR'] ?? '';\r
\r
$this->assertSame(Promocode::TIP_SALE_PROMOBONUS, $result['tipSale']);\r
}\r
\r
+ /**\r
+ * При промо-списании обычный баланс НЕ уменьшается.\r
+ */\r
+ public function testRegularBalanceUnchangedWithPromo()\r
+ {\r
+ $result = $this->calculateWriteOffChoice(\r
+ checkAmount: 2000,\r
+ amountReal: 2000,\r
+ bonusRate: 0.10,\r
+ promoBalance: 350,\r
+ userBalance: 500\r
+ );\r
+\r
+ $this->assertTrue($result['usePromoWriteOff']);\r
+ // Обычный баланс остаётся неизменным при промо-списании\r
+ $this->assertSame(500, $result['userBalansNew']);\r
+ }\r
+\r
+ /**\r
+ * При стандартном списании обычный баланс уменьшается.\r
+ */\r
+ public function testRegularBalanceDecreasedWithStandard()\r
+ {\r
+ $result = $this->calculateWriteOffChoice(\r
+ checkAmount: 5000,\r
+ amountReal: 5000,\r
+ bonusRate: 0.10,\r
+ promoBalance: 350,\r
+ userBalance: 1000\r
+ );\r
+\r
+ $this->assertFalse($result['usePromoWriteOff']);\r
+ // 1000 - 500 = 500\r
+ $this->assertSame(500, $result['userBalansNew']);\r
+ }\r
+\r
/**\r
* Воспроизводит алгоритм выбора списания из actionSale().\r
* Это чистая логика без БД — тестируем алгоритм.\r
+ *\r
+ * Параметры соответствуют переменным в actionSale():\r
+ * checkAmount -> amount_all\r
+ * amountReal -> amount_real\r
+ * summaNoWriteoffs -> summa_no_writeoffs (товары без списания)\r
*/\r
private function calculateWriteOffChoice(\r
int $checkAmount,\r
float $promoBalance,\r
float $cashbackRate = 0.10,\r
int $userBalance = 10000,\r
- int $requestedWriteOff = 0\r
+ int $requestedWriteOff = 0,\r
+ int $summaNoWriteoffs = 0\r
): array {\r
$promoWriteOffAmount = 350;\r
$promoMinCheckAmount = 1700;\r
\r
- // Стандартный расчёт\r
+ // Стандартный расчёт (как в actionSale)\r
$writeOffBonusesTheory = (int) round($amountReal * $bonusRate);\r
$writeOffBonuses = $requestedWriteOff ?: $writeOffBonusesTheory;\r
if ($writeOffBonuses > $writeOffBonusesTheory) {\r
$writeOffBonuses = $userBalance;\r
}\r
\r
- // Промо-проверка\r
+ // Промо-проверка (amount_all используется, не amount_real)\r
$usePromoWriteOff = false;\r
if ($promoBalance >= $promoWriteOffAmount\r
&& $checkAmount >= $promoMinCheckAmount\r
$writeOffBonuses = $promoWriteOffAmount;\r
}\r
\r
- // Кэшбек\r
- $bazaBack = $amountReal - $writeOffBonuses;\r
+ // user_balans_new: при промо обычный баланс не трогается\r
+ $userBalansNew = $usePromoWriteOff ? $userBalance : ($userBalance - $writeOffBonuses);\r
+\r
+ // Кэшбек: baza_back = amount_real + summa_no_writeoffs - write_off_bonuses\r
+ $bazaBack = $amountReal + $summaNoWriteoffs - $writeOffBonuses;\r
$cashback = $usePromoWriteOff ? 0 : (int) round($bazaBack * $cashbackRate);\r
\r
// tip_sale\r
'writeOffBonuses' => $writeOffBonuses,\r
'cashback' => $cashback,\r
'tipSale' => $tipSale,\r
+ 'userBalansNew' => $userBalansNew,\r
];\r
}\r
}\r