]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
fix(TO8-22): нормализация окончаний строк CRLF→LF
authorVVF <developer@DeepBlue.localdomain>
Fri, 6 Mar 2026 13:45:41 +0000 (16:45 +0300)
committerVVF <developer@DeepBlue.localdomain>
Fri, 6 Mar 2026 13:45:41 +0000 (16:45 +0300)
Файлы были сохранены с CRLF из-за WSL, конвертированы в LF
для чистого diff в MR.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
erp24/migrations/m260306_100000_add_activated_fields_to_promocode.php
erp24/records/Promocode.php
erp24/tests/unit/controllers/BonusControllerPromoTest.php
erp24/tests/unit/records/PromocodePromoTest.php

index 9abfc781ac4683b71857ea339818d6a9b7710c57..866cfccc1dbf340af49a7bff4da5d32045ecf27a 100644 (file)
@@ -1,29 +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
+<?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');
+    }
+}
index e268498dd09adccedd92c4761eeb8ac368e8bcc0..995b7670a7ce72009d2977caeacc3d6c2e4ca485 100644 (file)
-<?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;
+    }
+}
index ebf3c91823171860dbdb6b77073921cff862872b..0360ce718f646660957c4e895ea8571e5029b037 100644 (file)
-<?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,
+        ];
+    }
+}
index ce67c6a844163acca0691eabd219a96b81fc51a4..3f2b6fd37d07af40d7f666478c4c70e90f264c3b 100644 (file)
@@ -1,90 +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
+<?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);
+    }
+}