]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
fix(ERP-241): use InvalidArgumentException in TimetableService for proper error display origin/feature_filippov_ERP-241_fix_api3_timetable_exception
authorAleksey Filippov <Aleksey.Filippov@erp-flowers.ru>
Thu, 26 Feb 2026 15:47:41 +0000 (18:47 +0300)
committerAleksey Filippov <Aleksey.Filippov@erp-flowers.ru>
Thu, 26 Feb 2026 15:47:41 +0000 (18:47 +0300)
TimetableService threw generic \Exception which EventBehavior couldn't
handle, causing "Произошла неизвестная ошибка" in mobile app instead of
actual validation messages. Changed to InvalidArgumentException (matching
pattern used by all other api3 services) and switched from Json::encode
to firstErrors[0] for human-readable messages.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
erp24/api3/core/services/TimetableService.php
erp24/tests/unit/services/TimetableServiceExceptionTest.php [new file with mode: 0644]

index 48d4167c24baafb5b42e2de53122e44f0b3d879e..6449af934a21f06c0cb8045ab5abf6dcc21fe6cb 100644 (file)
@@ -4,7 +4,6 @@ namespace yii_app\api3\core\services;
 
 use yii\base\InvalidArgumentException;
 use yii\db\Exception;
-use yii\helpers\Json;
 use yii\web\NotFoundHttpException;
 use yii_app\api3\modules\v1\models\Admin;
 use yii_app\api3\modules\v1\models\timetable\Timetable;
@@ -39,7 +38,7 @@ class TimetableService
 
         //убрать после согласования оплаты подработчиков
         if (Admin::findOne($admin_id)->group_id === AdminGroup::GROUP_WORKERS && !$data->plan_id) {
-            throw new \Exception('Подработчики не могут открыть смены без плана!');
+            throw new InvalidArgumentException('Подработчики не могут открыть смены без плана!');
         }
 
         $transaction = \Yii::$app->db->beginTransaction();
@@ -55,7 +54,7 @@ class TimetableService
                 $fact->tabel = 1;
                 $fact->save();
                 if ($fact->getErrors()) {
-                    throw new \Exception(Json::encode($fact->getErrors()));
+                    throw new InvalidArgumentException(array_values($fact->firstErrors)[0] ?? "");
                 }
             }
 
@@ -98,7 +97,7 @@ class TimetableService
             }
 
             if ($checkIn->getErrors()) {
-                throw new \Exception(Json::encode($checkIn->getErrors()));
+                throw new InvalidArgumentException(array_values($checkIn->firstErrors)[0] ?? "");
             }
 
             $transaction->commit();
@@ -169,7 +168,7 @@ class TimetableService
         }
 
         if ($checkIn->getErrors()) {
-            throw new \Exception(Json::encode($checkIn->getErrors()));
+            throw new InvalidArgumentException(array_values($checkIn->firstErrors)[0] ?? "");
         }
 
         return true;
@@ -219,7 +218,7 @@ class TimetableService
             $checkIn->status = 0;
             $checkIn->save();
             if ($checkIn->getErrors()) {
-                throw new \Exception(Json::encode($checkIn->getErrors()));
+                throw new InvalidArgumentException(array_values($checkIn->firstErrors)[0] ?? "");
             }
 
             $transaction->commit();
diff --git a/erp24/tests/unit/services/TimetableServiceExceptionTest.php b/erp24/tests/unit/services/TimetableServiceExceptionTest.php
new file mode 100644 (file)
index 0000000..48c9f2c
--- /dev/null
@@ -0,0 +1,135 @@
+<?php
+
+declare(strict_types=1);
+
+namespace tests\unit\services;
+
+use Codeception\Test\Unit;
+
+/**
+ * Тесты для ERP-241: TimetableService должен бросать InvalidArgumentException,
+ * а не голый \Exception, чтобы EventBehavior мог отобразить понятное сообщение.
+ *
+ * @group services
+ * @group api3
+ * @group regression
+ */
+class TimetableServiceExceptionTest extends Unit
+{
+    private string $serviceFilePath;
+
+    protected function _before(): void
+    {
+        $this->serviceFilePath = dirname(__DIR__, 3) . '/api3/core/services/TimetableService.php';
+    }
+
+    // ========================================================================
+    // ГРУППА 1: Код TimetableService не должен бросать голый \Exception
+    // ========================================================================
+
+    /**
+     * TimetableService не должен содержать throw new \Exception
+     *
+     * EventBehavior обрабатывает только ErrorException, HttpException,
+     * InvalidArgumentException. Голый \Exception приводит к "Произошла
+     * неизвестная ошибка" вместо понятного сообщения.
+     *
+     * @see \yii_app\api3\core\behaviors\EventBehavior::beforeSend()
+     */
+    public function testService_NoGenericExceptionThrows(): void
+    {
+        if (!file_exists($this->serviceFilePath)) {
+            $this->markTestSkipped('TimetableService.php not found');
+        }
+
+        $content = file_get_contents($this->serviceFilePath);
+
+        // throw new \Exception — голый Exception без типизации
+        $pattern = '/throw\s+new\s+\\\\Exception\s*\(/';
+        preg_match_all($pattern, $content, $matches);
+
+        $this->assertEmpty(
+            $matches[0],
+            'TimetableService не должен бросать голый \\Exception. '
+            . 'Используйте InvalidArgumentException для ошибок валидации. '
+            . 'Найдено ' . count($matches[0]) . ' вхождений.'
+        );
+    }
+
+    /**
+     * TimetableService должен использовать InvalidArgumentException для ошибок валидации
+     */
+    public function testService_UsesInvalidArgumentException(): void
+    {
+        if (!file_exists($this->serviceFilePath)) {
+            $this->markTestSkipped('TimetableService.php not found');
+        }
+
+        $content = file_get_contents($this->serviceFilePath);
+
+        $this->assertStringContainsString(
+            'use yii\base\InvalidArgumentException',
+            $content,
+            'TimetableService должен импортировать InvalidArgumentException'
+        );
+
+        $this->assertStringContainsString(
+            'throw new InvalidArgumentException',
+            $content,
+            'TimetableService должен использовать InvalidArgumentException для ошибок валидации'
+        );
+    }
+
+    /**
+     * TimetableService должен передавать firstErrors[0] при ошибках save(),
+     * а не Json::encode(getErrors()) — для читаемого сообщения в API
+     */
+    public function testService_UsesFirstErrorsNotJsonEncode(): void
+    {
+        if (!file_exists($this->serviceFilePath)) {
+            $this->markTestSkipped('TimetableService.php not found');
+        }
+
+        $content = file_get_contents($this->serviceFilePath);
+
+        // Не должно быть Json::encode($...->getErrors()) в throw
+        $pattern = '/throw\s+new\s+\w+Exception\s*\(\s*Json::encode\s*\(/';
+        preg_match_all($pattern, $content, $matches);
+
+        $this->assertEmpty(
+            $matches[0],
+            'TimetableService не должен использовать Json::encode(getErrors()) в исключениях. '
+            . 'Используйте array_values($model->firstErrors)[0] для читаемого сообщения.'
+        );
+    }
+
+    // ========================================================================
+    // ГРУППА 2: EventBehavior корректно обрабатывает InvalidArgumentException
+    // ========================================================================
+
+    /**
+     * EventBehavior должен возвращать type=invalid_request_type_2
+     * для InvalidArgumentException
+     */
+    public function testEventBehavior_HandlesInvalidArgumentException(): void
+    {
+        $behaviorPath = dirname(__DIR__, 3) . '/api3/core/behaviors/EventBehavior.php';
+        if (!file_exists($behaviorPath)) {
+            $this->markTestSkipped('EventBehavior.php not found');
+        }
+
+        $content = file_get_contents($behaviorPath);
+
+        $this->assertStringContainsString(
+            'InvalidArgumentException',
+            $content,
+            'EventBehavior должен обрабатывать InvalidArgumentException'
+        );
+
+        $this->assertStringContainsString(
+            'invalid_request_type_2',
+            $content,
+            'EventBehavior должен возвращать тип "invalid_request_type_2" для InvalidArgumentException'
+        );
+    }
+}