-<?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
+<?php
+
+use yii\db\Migration;
+
+/**
+ * TO8-22: Поля трекинга активации промокода.
+ */
+class m260306_100000_add_activated_fields_to_promocode extends Migration
+{
+ public function safeUp()
+ {
+ $this->addColumn(
+ '{{%erp24.promocode}}',
+ 'activated_by',
+ $this->integer()->null()->comment('ID клиента, активировавшего промокод')
+ );
+ $this->addColumn(
+ '{{%erp24.promocode}}',
+ 'activated_at',
+ $this->dateTime()->null()->comment('Дата и время активации промокода')
+ );
+ }
+
+ public function safeDown()
+ {
+ $this->dropColumn('{{%erp24.promocode}}', 'activated_at');
+ $this->dropColumn('{{%erp24.promocode}}', 'activated_by');
+ }
+}
-<?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
- 'activated_by' => 'Activated By',\r
- 'activated_at' => 'Activated 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
+<?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 Начислять в промо-баланс (а не в обычные бонусы)
+ * @property int|null $activated_by ID клиента, активировавшего промокод
+ * @property string|null $activated_at Дата и время активации промокода
+ */
+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';
+
+ const TIP_SALE_PROMOBONUS = 'promobonus';
+
+ 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'],
+ [['activated_by'], 'integer'],
+ [['activated_at'], 'datetime', 'format' => 'php:Y-m-d H:i:s'],
+ [['updated_by', 'updated_at', 'activated_by', 'activated_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',
+ 'activated_by' => 'Activated By',
+ 'activated_at' => 'Activated 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']);
+ }
+
+ /**
+ * Проверяет, можно ли активировать промокод.
+ * @return int|true true если можно, иначе error_id:
+ * 1 — не найден/неактивен
+ * 2 — уже использован
+ * 3 — просрочен
+ */
+ public function isActivatable()
+ {
+ if ($this->active != self::ACTIVE_ON) {
+ return 1;
+ }
+ if ($this->used == self::USED_YES) {
+ return 2;
+ }
+ $now = date('Y-m-d H:i:s');
+ if ($this->date_start > $now || $this->date_end < $now) {
+ return 3;
+ }
+ return true;
+ }
+}
-<?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
- * При промо-списании обычный баланс НЕ уменьшается.\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
- int $amountReal,\r
- float $bonusRate,\r
- float $promoBalance,\r
- float $cashbackRate = 0.10,\r
- int $userBalance = 10000,\r
- int $requestedWriteOff = 0,\r
- int $summaNoWriteoffs = 0\r
- ): array {\r
- $promoWriteOffAmount = 350;\r
- $promoMinCheckAmount = 1700;\r
-\r
- // Стандартный расчёт (как в actionSale)\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
- // Промо-проверка (amount_all используется, не amount_real)\r
- $usePromoWriteOff = false;\r
- if ($promoBalance >= $promoWriteOffAmount\r
- && $checkAmount >= $promoMinCheckAmount\r
- && $promoWriteOffAmount > $writeOffBonusesTheory\r
- ) {\r
- $usePromoWriteOff = true;\r
- $writeOffBonuses = $promoWriteOffAmount;\r
- }\r
-\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
- $tipSale = $usePromoWriteOff ? Promocode::TIP_SALE_PROMOBONUS : 'sale';\r
-\r
- return [\r
- 'usePromoWriteOff' => $usePromoWriteOff,\r
- 'writeOffBonuses' => $writeOffBonuses,\r
- 'cashback' => $cashback,\r
- 'tipSale' => $tipSale,\r
- 'userBalansNew' => $userBalansNew,\r
- ];\r
- }\r
-}\r
+<?php
+
+namespace app\tests\unit\controllers;
+
+use Codeception\Test\Unit;
+use yii_app\records\Promocode;
+
+/**
+ * TO8-22: Тесты логики промо-списания БЛАГО.
+ *
+ * Тестируем алгоритм выбора: стандартное списание vs промо-списание.
+ * Алгоритм из actionSale():
+ * 1. standard_max = amount_real * bonus_rate
+ * 2. promo_balance >= 350 AND check_amount >= 1700 AND 350 > standard_max => промо
+ * 3. Иначе => стандартное списание
+ */
+class BonusControllerPromoTest extends Unit
+{
+ /**
+ * Промо-списание применяется: покупка 2000р, промо-баланс 350р, стандарт 200р (10%).
+ * 350 > 200 => промо выгоднее.
+ */
+ public function testPromoAppliedWhenBetter()
+ {
+ $result = $this->calculateWriteOffChoice(
+ checkAmount: 2000,
+ amountReal: 2000,
+ bonusRate: 0.10,
+ promoBalance: 350
+ );
+
+ $this->assertTrue($result['usePromoWriteOff']);
+ $this->assertSame(350, $result['writeOffBonuses']);
+ }
+
+ /**
+ * Стандартное списание лучше: покупка 5000р, стандарт 500р (10%).
+ * 350 < 500 => стандартное выгоднее.
+ */
+ public function testStandardBetterThanPromo()
+ {
+ $result = $this->calculateWriteOffChoice(
+ checkAmount: 5000,
+ amountReal: 5000,
+ bonusRate: 0.10,
+ promoBalance: 350
+ );
+
+ $this->assertFalse($result['usePromoWriteOff']);
+ $this->assertSame(500, $result['writeOffBonuses']);
+ }
+
+ /**
+ * Покупка слишком маленькая (< 1700р) — промо-списание не применяется.
+ */
+ public function testCheckTooSmallForPromo()
+ {
+ $result = $this->calculateWriteOffChoice(
+ checkAmount: 1500,
+ amountReal: 1500,
+ bonusRate: 0.10,
+ promoBalance: 350
+ );
+
+ $this->assertFalse($result['usePromoWriteOff']);
+ $this->assertSame(150, $result['writeOffBonuses']);
+ }
+
+ /**
+ * Недостаточный промо-баланс (< 350р) — стандартное списание.
+ */
+ public function testInsufficientPromoBalance()
+ {
+ $result = $this->calculateWriteOffChoice(
+ checkAmount: 2000,
+ amountReal: 2000,
+ bonusRate: 0.10,
+ promoBalance: 100
+ );
+
+ $this->assertFalse($result['usePromoWriteOff']);
+ $this->assertSame(200, $result['writeOffBonuses']);
+ }
+
+ /**
+ * При промо-списании кэшбек НЕ начисляется.
+ */
+ public function testNoCashbackWithPromoWriteOff()
+ {
+ $result = $this->calculateWriteOffChoice(
+ checkAmount: 2000,
+ amountReal: 2000,
+ bonusRate: 0.10,
+ promoBalance: 350
+ );
+
+ $this->assertTrue($result['usePromoWriteOff']);
+ // При промо-списании cashback = 0
+ $this->assertSame(0, $result['cashback']);
+ }
+
+ /**
+ * При стандартном списании кэшбек начисляется нормально.
+ */
+ public function testCashbackWithStandardWriteOff()
+ {
+ $result = $this->calculateWriteOffChoice(
+ checkAmount: 5000,
+ amountReal: 5000,
+ bonusRate: 0.10,
+ promoBalance: 350,
+ cashbackRate: 0.10
+ );
+
+ $this->assertFalse($result['usePromoWriteOff']);
+ $this->assertGreaterThan(0, $result['cashback']);
+ }
+
+ /**
+ * Граничный случай: покупка ровно 1700р — промо применяется (>=).
+ */
+ public function testExactMinCheckAmount()
+ {
+ $result = $this->calculateWriteOffChoice(
+ checkAmount: 1700,
+ amountReal: 1700,
+ bonusRate: 0.10,
+ promoBalance: 350
+ );
+
+ // standard_max = 1700 * 0.10 = 170, 350 > 170 => промо
+ $this->assertTrue($result['usePromoWriteOff']);
+ }
+
+ /**
+ * Граничный случай: промо-баланс ровно 350 — промо применяется (>=).
+ */
+ public function testExactPromoBalance()
+ {
+ $result = $this->calculateWriteOffChoice(
+ checkAmount: 2000,
+ amountReal: 2000,
+ bonusRate: 0.10,
+ promoBalance: 350
+ );
+
+ $this->assertTrue($result['usePromoWriteOff']);
+ }
+
+ /**
+ * При промо-списании tip_sale должен быть 'promobonus'.
+ */
+ public function testPromoTipSaleValue()
+ {
+ $result = $this->calculateWriteOffChoice(
+ checkAmount: 2000,
+ amountReal: 2000,
+ bonusRate: 0.10,
+ promoBalance: 350
+ );
+
+ $this->assertSame(Promocode::TIP_SALE_PROMOBONUS, $result['tipSale']);
+ }
+
+ /**
+ * При промо-списании обычный баланс НЕ уменьшается.
+ */
+ public function testRegularBalanceUnchangedWithPromo()
+ {
+ $result = $this->calculateWriteOffChoice(
+ checkAmount: 2000,
+ amountReal: 2000,
+ bonusRate: 0.10,
+ promoBalance: 350,
+ userBalance: 500
+ );
+
+ $this->assertTrue($result['usePromoWriteOff']);
+ // Обычный баланс остаётся неизменным при промо-списании
+ $this->assertSame(500, $result['userBalansNew']);
+ }
+
+ /**
+ * При стандартном списании обычный баланс уменьшается.
+ */
+ public function testRegularBalanceDecreasedWithStandard()
+ {
+ $result = $this->calculateWriteOffChoice(
+ checkAmount: 5000,
+ amountReal: 5000,
+ bonusRate: 0.10,
+ promoBalance: 350,
+ userBalance: 1000
+ );
+
+ $this->assertFalse($result['usePromoWriteOff']);
+ // 1000 - 500 = 500
+ $this->assertSame(500, $result['userBalansNew']);
+ }
+
+ /**
+ * Воспроизводит алгоритм выбора списания из actionSale().
+ * Это чистая логика без БД — тестируем алгоритм.
+ *
+ * Параметры соответствуют переменным в actionSale():
+ * checkAmount -> amount_all
+ * amountReal -> amount_real
+ * summaNoWriteoffs -> summa_no_writeoffs (товары без списания)
+ */
+ private function calculateWriteOffChoice(
+ int $checkAmount,
+ int $amountReal,
+ float $bonusRate,
+ float $promoBalance,
+ float $cashbackRate = 0.10,
+ int $userBalance = 10000,
+ int $requestedWriteOff = 0,
+ int $summaNoWriteoffs = 0
+ ): array {
+ $promoWriteOffAmount = 350;
+ $promoMinCheckAmount = 1700;
+
+ // Стандартный расчёт (как в actionSale)
+ $writeOffBonusesTheory = (int) round($amountReal * $bonusRate);
+ $writeOffBonuses = $requestedWriteOff ?: $writeOffBonusesTheory;
+ if ($writeOffBonuses > $writeOffBonusesTheory) {
+ $writeOffBonuses = $writeOffBonusesTheory;
+ }
+ if ($userBalance < $writeOffBonuses) {
+ $writeOffBonuses = $userBalance;
+ }
+
+ // Промо-проверка (amount_all используется, не amount_real)
+ $usePromoWriteOff = false;
+ if ($promoBalance >= $promoWriteOffAmount
+ && $checkAmount >= $promoMinCheckAmount
+ && $promoWriteOffAmount > $writeOffBonusesTheory
+ ) {
+ $usePromoWriteOff = true;
+ $writeOffBonuses = $promoWriteOffAmount;
+ }
+
+ // user_balans_new: при промо обычный баланс не трогается
+ $userBalansNew = $usePromoWriteOff ? $userBalance : ($userBalance - $writeOffBonuses);
+
+ // Кэшбек: baza_back = amount_real + summa_no_writeoffs - write_off_bonuses
+ $bazaBack = $amountReal + $summaNoWriteoffs - $writeOffBonuses;
+ $cashback = $usePromoWriteOff ? 0 : (int) round($bazaBack * $cashbackRate);
+
+ // tip_sale
+ $tipSale = $usePromoWriteOff ? Promocode::TIP_SALE_PROMOBONUS : 'sale';
+
+ return [
+ 'usePromoWriteOff' => $usePromoWriteOff,
+ 'writeOffBonuses' => $writeOffBonuses,
+ 'cashback' => $cashback,
+ 'tipSale' => $tipSale,
+ 'userBalansNew' => $userBalansNew,
+ ];
+ }
+}
-<?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
+<?php
+
+namespace app\tests\unit\records;
+
+use Codeception\Test\Unit;
+use yii_app\records\Promocode;
+
+/**
+ * TO8-22: Тесты модели Promocode — метод isActivatable() и константы.
+ */
+class PromocodePromoTest extends Unit
+{
+ /**
+ * Активный, неиспользованный промокод в пределах дат — можно активировать.
+ */
+ public function testIsActivatableSuccess()
+ {
+ $promo = new Promocode();
+ $promo->active = Promocode::ACTIVE_ON;
+ $promo->used = Promocode::USED_NO;
+ $promo->date_start = date('Y-m-d H:i:s', strtotime('-1 day'));
+ $promo->date_end = date('Y-m-d H:i:s', strtotime('+30 day'));
+
+ $this->assertTrue($promo->isActivatable());
+ }
+
+ /**
+ * Уже использованный промокод — error_id=2.
+ */
+ public function testIsActivatableAlreadyUsed()
+ {
+ $promo = new Promocode();
+ $promo->active = Promocode::ACTIVE_ON;
+ $promo->used = Promocode::USED_YES;
+ $promo->date_start = date('Y-m-d H:i:s', strtotime('-1 day'));
+ $promo->date_end = date('Y-m-d H:i:s', strtotime('+30 day'));
+
+ $this->assertSame(2, $promo->isActivatable());
+ }
+
+ /**
+ * Неактивный промокод — error_id=1.
+ */
+ public function testIsActivatableInactive()
+ {
+ $promo = new Promocode();
+ $promo->active = Promocode::ACTIVE_OFF;
+ $promo->used = Promocode::USED_NO;
+ $promo->date_start = date('Y-m-d H:i:s', strtotime('-1 day'));
+ $promo->date_end = date('Y-m-d H:i:s', strtotime('+30 day'));
+
+ $this->assertSame(1, $promo->isActivatable());
+ }
+
+ /**
+ * Просроченный промокод (date_end в прошлом) — error_id=3.
+ */
+ public function testIsActivatableExpired()
+ {
+ $promo = new Promocode();
+ $promo->active = Promocode::ACTIVE_ON;
+ $promo->used = Promocode::USED_NO;
+ $promo->date_start = date('Y-m-d H:i:s', strtotime('-30 day'));
+ $promo->date_end = date('Y-m-d H:i:s', strtotime('-1 day'));
+
+ $this->assertSame(3, $promo->isActivatable());
+ }
+
+ /**
+ * Промокод ещё не начал действовать (date_start в будущем) — error_id=3.
+ */
+ public function testIsActivatableNotStartedYet()
+ {
+ $promo = new Promocode();
+ $promo->active = Promocode::ACTIVE_ON;
+ $promo->used = Promocode::USED_NO;
+ $promo->date_start = date('Y-m-d H:i:s', strtotime('+1 day'));
+ $promo->date_end = date('Y-m-d H:i:s', strtotime('+30 day'));
+
+ $this->assertSame(3, $promo->isActivatable());
+ }
+
+ /**
+ * Константа TIP_SALE_PROMOBONUS имеет правильное значение.
+ */
+ public function testTipSalePromobonusConstant()
+ {
+ $this->assertSame('promobonus', Promocode::TIP_SALE_PROMOBONUS);
+ }
+}