]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
Убираем Успех без данных и правим ошибки
authorVladimir Fomichev <vladimir.fomichev@erp-flowers.ru>
Wed, 21 Jan 2026 14:06:56 +0000 (17:06 +0300)
committerVladimir Fomichev <vladimir.fomichev@erp-flowers.ru>
Wed, 21 Jan 2026 14:06:56 +0000 (17:06 +0300)
16 files changed:
erp24/.env.example
erp24/api2/config/dev.api2.config.php
erp24/api2/config/env.php
erp24/api2/controllers/DataController.php
erp24/commands/MarketplaceController.php
erp24/jobs/SendTelegramTestMessageJob.php
erp24/records/MarketplaceOrderDailyIssues.php
erp24/services/MarketplaceService.php
erp24/services/OrderControlReportService.php
erp24/services/UploadService.php
erp24/services/dto/ControlReportResult.php
erp24/services/dto/OrderIssue.php
erp24/tests/unit/records/MarketplaceOrderDailyIssuesTest.php
erp24/tests/unit/services/OrderControlReportServiceTest.php
erp24/tests/unit/services/dto/ControlReportResultTest.php
erp24/tests/unit/services/dto/OrderIssueTest.php

index ba328de9829048fc629161f06eee511dda0ffcea..9f00cacdab5558542a158ae66738c10ba7dfd989 100644 (file)
@@ -73,6 +73,10 @@ RABBIT_HOST=localhost
 # NOTE: Special characters in password are OK (auto URL-encoded)
 RABBIT_USER=admin
 RABBIT_PASSWORD=dev_rabbit_password
+# RabbitMQ container env vars (must match RABBIT_USER/RABBIT_PASSWORD)
+# These are read by RabbitMQ container from env_file
+RABBITMQ_DEFAULT_USER=admin
+RABBITMQ_DEFAULT_PASS=dev_rabbit_password
 
 # === TELEGRAM ===
 # Format: 123456789:ABCdefGHIjklMNOpqrsTUVwxyz
index 8c99a738a67d36a1698512753248037f7a46335c..0e024d1f153ef1c9cd0252ff3fb98c52e35bafed 100644 (file)
@@ -1,5 +1,7 @@
 <?php
 
+use yii\queue\amqp_interop\Queue;
+
 return [
     'language' => 'ru',
     'viewPath' => dirname(__DIR__) . '/views',
@@ -34,6 +36,15 @@ return [
             // ВАЖНО для ERP24: Cookie validation key вынесен в переменную окружения (.env)
             'cookieValidationKey' => getenv('COOKIE_VALIDATION_KEY') ?: 'dev_cookie_key_32chars_minimum!!',
         ],
+        'queue' => [
+            'class' => Queue::class,
+            'dsn' => 'amqp://' . rawurlencode(getenv('RABBIT_USER') ?: '') . ':' . rawurlencode(getenv('RABBIT_PASSWORD') ?: '') . '@' . (getenv('RABBIT_HOST') ?: 'localhost') . ':5672',
+            'queueName' => 'telegram-queue',
+            'as log' => \yii\queue\LogBehavior::class,
+            'ttr' => 600, // Время для выполнения задания
+            'attempts' => 3, // Количество попыток
+            'exchangeName' => 'telegram-exchange',
+        ],
         // ВАЖНО для ERP24: Database credentials вынесены в переменные окружения (.env)
         'db' => [
             'class' => yii\db\Connection::class,
index cd418591c37329dbfe4b962fd34d6dec2ecdc2e0..19ba42f0e7b3b7a86661403c2a770dd2280012ad 100644 (file)
@@ -1,14 +1,21 @@
 <?php
 
 try {
-    $dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/../');
+    $dotenv = Dotenv\Dotenv::createUnsafeImmutable(__DIR__ . '/../../');
     $dotenv->load();
-    $dotenv->required(['APP_ENV']);
+    $dotenv->required([
+        'APP_ENV',
+        'POSTGRES_PASSWORD',
+        'RABBIT_USER',
+        'RABBIT_PASSWORD',
+        'TELEGRAM_BOT_TOKEN',
+        'COOKIE_VALIDATION_KEY',
+        ]);
 
-    foreach ($_ENV as $key => $value) {
-        putenv("$key=$value");
-    }
+//    foreach ($_ENV as $key => $value) {
+//        putenv("$key=$value");
+//    }
 } catch (\Dotenv\Exception\InvalidPathException $e) {
     putenv("APP_ENV=development");
-    Yii::error('Файл .env не найден: ' . $e->getMessage());
+    error_log('[ENV ERROR] Файл .env не найден: ' . $e->getMessage());
 }
index 5ceba1677d6aa395a0ed30c50652cc8fe56ad117..05aee52c8ed972ac8bd30e0b3e91b3d1c6218572 100644 (file)
@@ -1607,8 +1607,25 @@ class DataController extends BaseController
                 $marketplaceOrder->cancelled_order_source = '1c';
                 $marketplaceOrder->cancelled_order_date = date('Y-m-d H:m:s');
             }
-            $marketplaceOrder->status_processing_1c = $statusId1C->id;
-            $marketplaceOrder->seller_id = $mpOrder['seller_id'] ?? null;
+
+            $incomingSellerId = $mpOrder['seller_id'] ?? null;
+            $emptyGuid = '00000000-0000-0000-0000-000000000000';
+            $isSellerIdEmpty = empty($incomingSellerId) || $incomingSellerId === $emptyGuid;
+            $isCheckGuidEmpty = empty($marketplaceOrder->check_guid);
+            $successfulOrderIds = MarketplaceOrder1cStatuses::getSuccessfulOrderIds();
+
+            // Не устанавливаем статус "Успех" без seller_id или без check_guid
+            if (in_array($statusId1C->id, $successfulOrderIds) && ($isSellerIdEmpty || $isCheckGuidEmpty)) {
+                Yii::warning(
+                    "Статус 'Успех' (ID: {$statusId1C->id}) без seller_id или check_guid для заказа {$mpOrder['id']}. " .
+                    "seller_id: " . ($incomingSellerId ?? 'null') . ", check_guid: " . ($marketplaceOrder->check_guid ?? 'null'),
+                    'marketplace-status'
+                );
+            } else {
+                $marketplaceOrder->status_processing_1c = $statusId1C->id;
+            }
+
+            $marketplaceOrder->seller_id = $isSellerIdEmpty ? null : $incomingSellerId;
             $marketplaceOrder->number_1c = $mpOrder['number'] ?? null;
 
 
index 90e9b9c89f85a1c39d81c067413cceea43868844..bb4290a60a4ad7a55c61e9ca5cbede8cf2b0afc2 100644 (file)
@@ -419,8 +419,6 @@ class MarketplaceController extends Controller
                 $result->getSuccessNoCheckCount() > 0 ? BaseConsole::FG_RED : BaseConsole::FG_GREEN);
             $this->stdout("  🚫 Отмена без обработки: {$result->getCancelNoProcessCount()}\n",
                 $result->getCancelNoProcessCount() > 0 ? BaseConsole::FG_RED : BaseConsole::FG_GREEN);
-            $this->stdout("  ⚠️ Успех без данных: {$result->getSuccessMissingDataCount()}\n",
-                $result->getSuccessMissingDataCount() > 0 ? BaseConsole::FG_RED : BaseConsole::FG_GREEN);
 
             $this->stdout("\nВсего проблем: {$result->totalIssues}\n",
                 $result->totalIssues > 0 ? BaseConsole::FG_RED : BaseConsole::FG_GREEN);
index 43be5c114c075ed7db587b2f102851437f075539..40be4a6cf4c637c73b4a90049b584efd646d5fa7 100644 (file)
@@ -2,15 +2,19 @@
 
 namespace yii_app\jobs;
 
-
 use GuzzleHttp\Client;
 use Yii;
 use yii\helpers\Json;
 use yii\queue\JobInterface;
 use yii_app\records\MarketplaceOrders;
-use yii_app\records\WriteOffsErp;
 use yii_app\services\TelegramService;
 
+/**
+ * Job для отправки уведомлений о новых заказах маркетплейсов в Telegram
+ *
+ * Используется для уведомлений о заказах с Яндекс.Маркета и Flowwow.
+ * Отправляет сообщения в канал из TELEGRAM_CHAT_CHANNEL_ID (.env)
+ */
 class SendTelegramTestMessageJob extends \yii\base\BaseObject implements JobInterface
 {
     public $message;
@@ -21,46 +25,87 @@ class SendTelegramTestMessageJob extends \yii\base\BaseObject implements JobInte
         $message = $this->message;
         $guid = $this->guid;
 
-        $marketplaceOrders = MarketplaceOrders::find()->where(['status_telegram' => MarketplaceOrders::STATUS_TELEGRAM_NOT_SENT, 'guid' => $guid])->one();
-        if (!$marketplaceOrders) {
+        // Находим заказ по guid со статусом "не отправлено"
+        $marketplaceOrder = MarketplaceOrders::find()
+            ->where([
+                'status_telegram' => MarketplaceOrders::STATUS_TELEGRAM_NOT_SENT,
+                'guid' => $guid
+            ])
+            ->one();
+
+        if (!$marketplaceOrder) {
+            Yii::warning("SendTelegramTestMessageJob: заказ с guid={$guid} не найден или уже обработан", 'telegram');
             return;
         }
-        /* @var MarketplaceOrders $marketplaceOrders */
-        $marketplaceOrders->status_telegram = MarketplaceOrders::STATUS_TELEGRAM_PREPARED_TO_SEND;
-        $marketplaceOrders->save();
-        if ($marketplaceOrders->getErrors()) {
-            Yii::error("Ошибка отправки сообщения в Telegram: " . Json::encode($marketplaceOrders->getErrors()), 'site');
+
+        // Меняем статус на "подготовлен к отправке"
+        $marketplaceOrder->status_telegram = MarketplaceOrders::STATUS_TELEGRAM_PREPARED_TO_SEND;
+        if (!$marketplaceOrder->save()) {
+            Yii::error("Ошибка сохранения статуса заказа: " . Json::encode($marketplaceOrder->getErrors()), 'telegram');
+        }
+
+        // Получаем токен бота из .env через TelegramService
+        $isDev = TelegramService::isDevEnv();
+        $botToken = getenv('TELEGRAM_BOT_TOKEN') ?: '';
+
+        if (empty($botToken)) {
+            Yii::error("SendTelegramTestMessageJob: TELEGRAM_BOT_TOKEN не установлен в .env", 'telegram');
+            $marketplaceOrder->status_telegram = MarketplaceOrders::STATUS_TELEGRAM_ERROR;
+            $marketplaceOrder->telegram_error = 'TELEGRAM_BOT_TOKEN не установлен';
+            $marketplaceOrder->save();
+            return;
         }
 
-        $botToken = TelegramService::TELEGRAM_BOT_DEV;
         $apiURL = "https://api.telegram.org/bot{$botToken}/sendMessage";
-        $chats = ['337084327', '730432579']; //Алексей
-        $message = TelegramService::escapeMarkdown($message);
+
+        // Получаем chat_id из .env (TELEGRAM_CHAT_CHANNEL_ID для dev, TELEGRAM_CHAT_CHANNEL_ERP_ID для prod)
+        $chatId = $isDev
+            ? getenv('TELEGRAM_CHAT_CHANNEL_ID')
+            : getenv('TELEGRAM_CHAT_CHANNEL_ERP_ID');
+
+        if (empty($chatId)) {
+            Yii::error("SendTelegramTestMessageJob: TELEGRAM_CHAT_CHANNEL_ID не установлен в .env", 'telegram');
+            $marketplaceOrder->status_telegram = MarketplaceOrders::STATUS_TELEGRAM_ERROR;
+            $marketplaceOrder->telegram_error = 'TELEGRAM_CHAT_CHANNEL_ID не установлен';
+            $marketplaceOrder->save();
+            return;
+        }
+
+        // Экранируем спецсимволы для MarkdownV2
+        $escapedMessage = TelegramService::escapeMarkdown($message);
+
         $client = new Client();
-        foreach ($chats as $chatId) {
-            try {
-                $client->post($apiURL, [
-                    'json' => [
-                        'chat_id' => $chatId,
-                        'text' => $message,
-                        'parse_mode' => 'MarkdownV2',
-                    ],
-                ]);
-                $marketplaceOrders->status_telegram = MarketplaceOrders::STATUS_TELEGRAM_SENT;
-                $marketplaceOrders->save();
-                if ($marketplaceOrders->getErrors()) {
-                    Yii::error("Ошибка отправки сообщения в Telegram: " . Json::encode($marketplaceOrders->getErrors()), 'site');
-                }
-            } catch (\Exception $e) {
-                Yii::error("Ошибка отправки сообщения в Telegram: " . $e->getMessage(), 'telegram');
-                $arr = preg_split("/response:\n/", $e->getMessage());
-                $marketplaceOrders->telegram_error = trim($arr[count($arr) - 1]);
-                $marketplaceOrders->status_telegram = MarketplaceOrders::STATUS_TELEGRAM_ERROR;
-                $marketplaceOrders->save();
-                if ($marketplaceOrders->getErrors()) {
-                    Yii::error("Ошибка отправки сообщения в Telegram: " . Json::encode($marketplaceOrders->getErrors()), 'site');
-                }
+
+        try {
+            $response = $client->post($apiURL, [
+                'json' => [
+                    'chat_id' => $chatId,
+                    'text' => $escapedMessage,
+                    'parse_mode' => 'MarkdownV2',
+                ],
+            ]);
+
+            if ($response->getStatusCode() === 200) {
+                $marketplaceOrder->status_telegram = MarketplaceOrders::STATUS_TELEGRAM_SENT;
+                Yii::info("Telegram уведомление отправлено: {$message} -> chat_id={$chatId}", 'telegram');
+            } else {
+                $marketplaceOrder->status_telegram = MarketplaceOrders::STATUS_TELEGRAM_ERROR;
+                $marketplaceOrder->telegram_error = "HTTP {$response->getStatusCode()}";
+                Yii::error("Ошибка отправки в Telegram: HTTP {$response->getStatusCode()}", 'telegram');
             }
+        } catch (\Exception $e) {
+            Yii::error("Ошибка отправки сообщения в Telegram: " . $e->getMessage(), 'telegram');
+
+            // Извлекаем текст ошибки из response
+            $errorParts = preg_split("/response:\n/", $e->getMessage());
+            $errorMessage = trim($errorParts[count($errorParts) - 1]);
+
+            $marketplaceOrder->telegram_error = mb_substr($errorMessage, 0, 255);
+            $marketplaceOrder->status_telegram = MarketplaceOrders::STATUS_TELEGRAM_ERROR;
+        }
+
+        if (!$marketplaceOrder->save()) {
+            Yii::error("Ошибка сохранения статуса заказа после отправки: " . Json::encode($marketplaceOrder->getErrors()), 'telegram');
         }
     }
 }
index e6e6c81828baf2cd895f78e5338458ac37c8c976..65f0a0b8274cfe3807bcb104a53b0cdd094542a5 100644 (file)
@@ -50,7 +50,6 @@ class MarketplaceOrderDailyIssues extends \yii\db\ActiveRecord
     public const TYPE_HUNG_IN_DELIVERY = OrderIssue::TYPE_HUNG_IN_DELIVERY;
     public const TYPE_SUCCESS_NO_CHECK = OrderIssue::TYPE_SUCCESS_NO_CHECK;
     public const TYPE_CANCEL_NO_PROCESS = OrderIssue::TYPE_CANCEL_NO_PROCESS;
-    public const TYPE_SUCCESS_MISSING_DATA = OrderIssue::TYPE_SUCCESS_MISSING_DATA;
 
     /**
      * Метки типов проблем
@@ -101,7 +100,6 @@ class MarketplaceOrderDailyIssues extends \yii\db\ActiveRecord
                 self::TYPE_HUNG_IN_DELIVERY,
                 self::TYPE_SUCCESS_NO_CHECK,
                 self::TYPE_CANCEL_NO_PROCESS,
-                self::TYPE_SUCCESS_MISSING_DATA,
             ]],
             [['interval'], 'in', 'range' => ['08:00', '20:00']],
             [['order_id', 'problem_type', 'report_date', 'interval'], 'unique', 'targetAttribute' => ['order_id', 'problem_type', 'report_date', 'interval']],
index 1a96c2b213273a2b70adacf4262a2eb2e04b7cea..fd7d7be447cb6b0bbe7d33e80717ad0358060e01 100644 (file)
@@ -1278,7 +1278,7 @@ class MarketplaceService
                                 ->asArray()
                                 ->all(), 'id')[0] ?? null;
                             $marketplaceOrder->cancelled_order_source = 'Yandex Market';
-                            $marketplaceOrder->status_processing_1c = $status1cWithCancelledId;
+                           // $marketplaceOrder->status_processing_1c = $status1cWithCancelledId;
                             $marketplaceOrder->cancelled_order_date = date('Y-m-d H:i:s');
                         }
                     }
@@ -1486,7 +1486,7 @@ class MarketplaceService
                                     ->asArray()
                                     ->all(), 'id')[0] ?? null;
                                 $marketplaceOrder->cancelled_order_source = 'Yandex Market';
-                                $marketplaceOrder->status_processing_1c = $status1cWithCancelledId;
+                               // $marketplaceOrder->status_processing_1c = $status1cWithCancelledId;
                                 $marketplaceOrder->cancelled_order_date = date('Y-m-d H:i:s');
                             }
                         }
@@ -1500,7 +1500,7 @@ class MarketplaceService
                             ->andWhere(['marketplace_id' => MarketplaceStore::YANDEX_WAREHOUSE_ID])
                             ->asArray()
                             ->all(), 'id')[0] ?? null;
-                        $marketplaceOrder->status_processing_1c = $status1cWithDeliveredId;
+                        // $marketplaceOrder->status_processing_1c = $status1cWithDeliveredId;
                         if(!$marketplaceOrder->save()) {
                             Yii::error('Не удалось обновить заказ' . json_encode($marketplaceOrder->getErrors(), JSON_UNESCAPED_UNICODE));
                         }
@@ -1798,7 +1798,7 @@ class MarketplaceService
                                     ->asArray()
                                     ->all(), 'id')[0] ?? null;
                                 $marketplaceOrder->cancelled_order_source = 'Yandex Market';
-                                $marketplaceOrder->status_processing_1c = $status1cWithCancelledId;
+                               // $marketplaceOrder->status_processing_1c = $status1cWithCancelledId;
                                 $marketplaceOrder->cancelled_order_date = date('Y-m-d H:i:s');
                             }
                         }
@@ -2379,7 +2379,7 @@ class MarketplaceService
                 if ($statusCode == 'DELIVERED' && $substatusCode == 'DELIVERY_SERVICE_DELIVERED' && $marketplaceOrder) {
                     Yii::error('Заказ доставлен ' . $marketplaceOrder->marketplace_order_id);
                     /* @var $marketplaceOrder MarketplaceOrders */
-                    $marketplaceOrder->status_processing_1c = $status1cWithDeliveredId;
+                   // $marketplaceOrder->status_processing_1c = $status1cWithDeliveredId;
                     if(!$marketplaceOrder->save()) {
                         Yii::error('Не удалось обновить заказ' . json_encode($marketplaceOrder->getErrors(), JSON_UNESCAPED_UNICODE));
                     }
@@ -2782,7 +2782,7 @@ class MarketplaceService
         $marketplaceOrder->cancelled_order_date = date('Y-m-d H:i:s');
 
         if ($status1cWithCancelledId !== null) {
-            $marketplaceOrder->status_processing_1c = $status1cWithCancelledId;
+           // $marketplaceOrder->status_processing_1c = $status1cWithCancelledId;
         }
 
         if ($logError) {
index 9230b915c455ca07cb779291cc11da21d14b7d54..7c7415eb622556dde9cd971f858959abe57d015f 100644 (file)
@@ -130,11 +130,11 @@ class OrderControlReportService
      */
     public function generateControlReport(int $hoursAgo = 12, bool $onlyNew = true, bool $testMode = false): ControlReportResult
     {
-        $result = new ControlReportResult();
-
         // Сохраняем режим тестирования для использования во внутренних методах
         $this->testMode = $testMode;
 
+        $result = new ControlReportResult($testMode);
+
         // Получаем диапазон дат для отображения в отчёте
         $dateRange = $this->getShiftBasedDateRange($hoursAgo, $testMode);
         $result->startDate = $dateRange['startDate'];
@@ -159,27 +159,23 @@ class OrderControlReportService
             // 3. Получаем остальные типы проблем
             $successNoCheck = $this->getSuccessNoCheckOrders($hoursAgo);
             $cancelNoProcess = $this->getCancelNoProcessOrders($hoursAgo);
-            $successMissingData = $this->getSuccessMissingDataOrders($hoursAgo);
 
             // 4. Фильтруем только новые для остальных типов, если требуется
             if ($onlyNew) {
                 $prevSuccess = $this->loadPreviousIssues(OrderIssue::TYPE_SUCCESS_NO_CHECK);
                 $prevCancel = $this->loadPreviousIssues(OrderIssue::TYPE_CANCEL_NO_PROCESS);
-                $prevMissingData = $this->loadPreviousIssues(OrderIssue::TYPE_SUCCESS_MISSING_DATA);
 
                 $successNoCheck = $this->filterNewIssues($successNoCheck, $prevSuccess);
                 $cancelNoProcess = $this->filterNewIssues($cancelNoProcess, $prevCancel);
-                $successMissingData = $this->filterNewIssues($successMissingData, $prevMissingData);
             }
 
             $result->hungInDelivery = $hungInDelivery;
             $result->successNoCheck = $successNoCheck;
             $result->cancelNoProcess = $cancelNoProcess;
-            $result->successMissingData = $successMissingData;
             $result->calculateTotal();
 
             // 5. Сохраняем состояние проблем (кроме кандидатов hung_in_delivery, они уже сохранены)
-            $issuesToSave = array_merge($hungInDelivery, $successNoCheck, $cancelNoProcess, $successMissingData);
+            $issuesToSave = array_merge($hungInDelivery, $successNoCheck, $cancelNoProcess);
             $result->statesSaved = $this->saveControlIssues($issuesToSave);
 
             // 6. Отправляем уведомления только если есть проблемы
@@ -303,7 +299,7 @@ class OrderControlReportService
 
         $issues = [];
         foreach ($orders as $orderData) {
-            $issues[] = OrderIssue::fromOrderData(OrderIssue::TYPE_HUNG_IN_DELIVERY, $orderData);
+            $issues[] = OrderIssue::fromOrderData(OrderIssue::TYPE_HUNG_IN_DELIVERY, $orderData, $this->testMode);
         }
 
         $this->logInfo('Найдено кандидатов "Завис в доставке"', ['count' => count($issues)]);
@@ -323,7 +319,7 @@ class OrderControlReportService
     {
         $saved = 0;
         $reportDate = date('Y-m-d');
-        $interval = (int)date('H') < 12 ? '08:00' : '20:00';
+        $interval = OrderIssue::calculateInterval($this->testMode);
 
         foreach ($candidates as $candidate) {
             // Проверяем, не существует ли уже такая запись
@@ -417,11 +413,17 @@ class OrderControlReportService
      * Получает заказы типа "Успех без чека"
      *
      * Критерий: МП статус = "Выполнен" (DELIVERED или DELIVERY_SERVICE_DELIVERED)
-     *           + (seller_id пустой/нулевой ИЛИ чек не создан в create_checks)
+     *           + РМК статус НЕ "Успех" (successful_order = 1 в marketplace_order_1c_statuses)
+     *
+     * Бизнес-логика:
+     * 1. Заказ получает статус "Выполнен" в маркетплейсе
+     * 2. Для успеха в 1С необходимо: seller_id назначен → чек создан и отправлен → 1С вернула успех
+     * 3. Если любой из этапов не выполнен — статус 1С не будет "Успех"
      *
-     * Причина проблемы:
-     * - Если seller_id пустой или '00000000-0000-0000-0000-000000000000' — чек не создаётся
-     * - Если чек не создан — 1С не получает сигнала о доставке заказа
+     * Причины попадания в этот отчёт (диагностика):
+     * - no_seller_id: продавец не назначен (seller_id пустой или нулевой GUID)
+     * - no_check: чек не создан в create_checks
+     * - rmk_not_success: чек создан, но статус 1С ещё не "Успех" (возможно, в процессе)
      *
      * @see MarketplaceService::createCheckForMarketplaceOrder() — логика создания чека
      *
@@ -455,9 +457,8 @@ class OrderControlReportService
             ? implode(', ', $rmkSuccessPlaceholders)
             : '0'; // fallback если статусов нет
 
-        // Выбираем заказы с МП-статусом "Выполнен", где:
-        // 1. РМК-статус НЕ "Успех" (1С не знает о доставке)
-        // 2. Причина: seller_id пустой или нулевой GUID, ИЛИ чек не создан в create_checks
+        // Выбираем заказы с МП-статусом "Выполнен", где РМК-статус НЕ "Успех"
+        // Диагностика причины добавляется для информационных целей
         $sql = "
             SELECT
                 mo.id,
@@ -469,6 +470,7 @@ class OrderControlReportService
                 mo.total,
                 mo.creation_date,
                 mo.seller_id,
+                mo.check_guid,
                 mo.status_processing_1c as rmk_status_id,
                 mocs.status as rmk_status,
                 most.code as mp_status_code,
@@ -484,7 +486,7 @@ class OrderControlReportService
                     THEN 'no_seller_id'
                     WHEN cc.id IS NULL
                     THEN 'no_check'
-                    ELSE 'unknown'
+                    ELSE 'rmk_not_success'
                 END as issue_reason
             FROM marketplace_orders mo
             LEFT JOIN city_store cs ON cs.id = mo.store_id
@@ -500,18 +502,11 @@ class OrderControlReportService
                   most.code = :delivered
                   OR mosub.code = :delivery_service_delivered
               )
-              -- Ð Ð\9cÐ\9a\81Ñ\82аÑ\82Ñ\83Ñ\81 Ð\9dÐ\95 Ð£Ñ\81пеÑ\85 (1С Ð½Ðµ Ð·Ð½Ð°ÐµÑ\82 Ð¾ Ð´Ð¾Ñ\81Ñ\82авке)
+              -- Ð Ð\9cÐ\9a\81Ñ\82аÑ\82Ñ\83Ñ\81 Ð\9dÐ\95 Ð£Ñ\81пеÑ\85 (1С Ð½Ðµ Ð¿Ð¾Ð´Ñ\82веÑ\80дила Ð´Ð¾Ñ\81Ñ\82авкÑ\83)
               AND (
                   mo.status_processing_1c IS NULL
                   OR mo.status_processing_1c::integer NOT IN ({$rmkSuccessInClause})
               )
-              -- Причина: seller_id пустой ИЛИ чек не создан
-              AND (
-                  mo.seller_id IS NULL
-                  OR mo.seller_id = ''
-                  OR mo.seller_id = :empty_seller_guid
-                  OR cc.id IS NULL
-              )
             ORDER BY cs.name ASC, mo.creation_date DESC
         ";
 
@@ -527,13 +522,14 @@ class OrderControlReportService
 
         $issues = [];
         foreach ($orders as $orderData) {
-            $issues[] = OrderIssue::fromOrderData(OrderIssue::TYPE_SUCCESS_NO_CHECK, $orderData);
+            $issues[] = OrderIssue::fromOrderData(OrderIssue::TYPE_SUCCESS_NO_CHECK, $orderData, $this->testMode);
         }
 
         $this->logInfo('Найдено "Успех без чека"', [
             'count' => count($issues),
             'no_seller_id' => count(array_filter($orders, fn($o) => $o['issue_reason'] === 'no_seller_id')),
             'no_check' => count(array_filter($orders, fn($o) => $o['issue_reason'] === 'no_check')),
+            'rmk_not_success' => count(array_filter($orders, fn($o) => $o['issue_reason'] === 'rmk_not_success')),
         ]);
 
         return $issues;
@@ -614,7 +610,7 @@ class OrderControlReportService
 
         $issues = [];
         foreach ($orders as $orderData) {
-            $issues[] = OrderIssue::fromOrderData(OrderIssue::TYPE_CANCEL_NO_PROCESS, $orderData);
+            $issues[] = OrderIssue::fromOrderData(OrderIssue::TYPE_CANCEL_NO_PROCESS, $orderData, $this->testMode);
         }
 
         $this->logInfo('Найдено "Отмена без обработки"', ['count' => count($issues)]);
@@ -622,128 +618,6 @@ class OrderControlReportService
         return $issues;
     }
 
-    /**
-     * Получает заказы типа "Успех без данных"
-     *
-     * Критерий: МП статус = "Выполнен" (DELIVERED или DELIVERY_SERVICE_DELIVERED)
-     *           + РМК статус = "Успех" (successful_order = 1 в marketplace_order_1c_statuses)
-     *           + (seller_id пустой/нулевой ИЛИ check_guid пустой/нулевой)
-     *
-     * Это заказы, которые успешно завершены и в МП, и в 1С, но при этом
-     * отсутствуют важные данные: продавец (seller_id) и/или GUID чека (check_guid).
-     *
-     * @param int $hoursAgo Период выборки в часах (по умолчанию 12)
-     * @return OrderIssue[]
-     */
-    public function getSuccessMissingDataOrders(int $hoursAgo = 12): array
-    {
-        $this->logInfo('Выборка заказов "Успех без данных"', ['hours_ago' => $hoursAgo, 'test_mode' => $this->testMode]);
-
-        // Получаем диапазон дат на основе конца смены
-        $dateRange = $this->getShiftBasedDateRange($hoursAgo, $this->testMode);
-        $startDateStr = $dateRange['startDate'];
-        $endDateStr = $dateRange['endDate'];
-
-        // Нулевой GUID — признак отсутствия продавца
-        $emptySellerGuid = '00000000-0000-0000-0000-000000000000';
-
-        // Получаем ID статусов "Успех" из БД
-        $rmkSuccessIds = $this->getRmkStatusSuccess();
-
-        // Формируем плейсхолдеры для IN-условия
-        $rmkSuccessPlaceholders = [];
-        $rmkSuccessParams = [];
-        foreach ($rmkSuccessIds as $index => $id) {
-            $placeholder = ':rmk_success_' . $index;
-            $rmkSuccessPlaceholders[] = $placeholder;
-            $rmkSuccessParams[$placeholder] = $id;
-        }
-        $rmkSuccessInClause = !empty($rmkSuccessPlaceholders)
-            ? implode(', ', $rmkSuccessPlaceholders)
-            : '0'; // fallback если статусов нет
-
-        // Выбираем заказы с МП-статусом "Выполнен" И РМК-статусом "Успех", где:
-        // seller_id пустой/нулевой ИЛИ check_guid пустой
-        $sql = "
-            SELECT
-                mo.id,
-                mo.marketplace_order_id,
-                mo.store_id,
-                cs.name as store_name,
-                mo.marketplace_name,
-                mo.marketplace_id,
-                mo.total,
-                mo.creation_date,
-                mo.seller_id,
-                mo.check_guid,
-                mo.status_processing_1c as rmk_status_id,
-                mocs.status as rmk_status,
-                most.code as mp_status_code,
-                mosub.code as mp_substatus_code,
-                COALESCE(most.name, mosub.name) as mp_status_name,
-                CASE
-                    WHEN (mo.seller_id IS NULL OR mo.seller_id = '' OR mo.seller_id = :empty_seller_guid)
-                         AND (mo.check_guid IS NULL OR mo.check_guid = '')
-                    THEN 'no_seller_and_check_guid'
-                    WHEN mo.seller_id IS NULL OR mo.seller_id = '' OR mo.seller_id = :empty_seller_guid2
-                    THEN 'no_seller_id'
-                    WHEN mo.check_guid IS NULL OR mo.check_guid = ''
-                    THEN 'no_check_guid'
-                    ELSE 'unknown'
-                END as issue_reason
-            FROM marketplace_orders mo
-            LEFT JOIN city_store cs ON cs.id = mo.store_id
-            LEFT JOIN marketplace_order_1c_statuses mocs ON mocs.id = mo.status_processing_1c::integer
-            LEFT JOIN marketplace_order_status_types most ON most.id = mo.status_id
-            LEFT JOIN marketplace_order_status_types mosub ON mosub.id = mo.substatus_id
-            WHERE mo.fake = 0
-              AND mo.updated_at >= :start_date
-              AND mo.updated_at <= :end_date
-              -- МП-статус = Выполнен (DELIVERED)
-              AND (
-                  most.code = :delivered
-                  OR mosub.code = :delivery_service_delivered
-              )
-              -- РМК-статус = Успех (1С знает о доставке)
-              AND mo.status_processing_1c::integer IN ({$rmkSuccessInClause})
-              -- Проблема: seller_id пустой ИЛИ check_guid пустой
-              AND (
-                  mo.seller_id IS NULL
-                  OR mo.seller_id = ''
-                  OR mo.seller_id = :empty_seller_guid3
-                  OR mo.check_guid IS NULL
-                  OR mo.check_guid = ''
-              )
-            ORDER BY cs.name ASC, mo.creation_date DESC
-        ";
-
-        $params = array_merge([
-            ':start_date' => $startDateStr,
-            ':end_date' => $endDateStr,
-            ':delivered' => MarketplaceOrderStatusTypes::DELIVERED_CODE,
-            ':delivery_service_delivered' => MarketplaceOrderStatusTypes::DELIVERY_SERVICE_DELIVERED_CODE,
-            ':empty_seller_guid' => $emptySellerGuid,
-            ':empty_seller_guid2' => $emptySellerGuid,
-            ':empty_seller_guid3' => $emptySellerGuid,
-        ], $rmkSuccessParams);
-
-        $orders = Yii::$app->db->createCommand($sql, $params)->queryAll();
-
-        $issues = [];
-        foreach ($orders as $orderData) {
-            $issues[] = OrderIssue::fromOrderData(OrderIssue::TYPE_SUCCESS_MISSING_DATA, $orderData);
-        }
-
-        $this->logInfo('Найдено "Успех без данных"', [
-            'count' => count($issues),
-            'no_seller_id' => count(array_filter($orders, fn($o) => $o['issue_reason'] === 'no_seller_id')),
-            'no_check_guid' => count(array_filter($orders, fn($o) => $o['issue_reason'] === 'no_check_guid')),
-            'no_seller_and_check_guid' => count(array_filter($orders, fn($o) => $o['issue_reason'] === 'no_seller_and_check_guid')),
-        ]);
-
-        return $issues;
-    }
-
     /**
      * Сохраняет состояние проблемных заказов в БД
      *
@@ -754,7 +628,7 @@ class OrderControlReportService
     {
         $saved = 0;
         $reportDate = date('Y-m-d');
-        $interval = (int)date('H') < 12 ? '08:00' : '20:00';
+        $interval = OrderIssue::calculateInterval($this->testMode);
 
         foreach ($issues as $issue) {
             // Проверяем, не существует ли уже такая запись
@@ -858,7 +732,7 @@ class OrderControlReportService
     private function markIssuesAsNotified(array $issues): void
     {
         $reportDate = date('Y-m-d');
-        $interval = (int)date('H') < 12 ? '08:00' : '20:00';
+        $interval = OrderIssue::calculateInterval($this->testMode);
 
         foreach ($issues as $issue) {
             $model = MarketplaceOrderDailyIssues::find()
@@ -911,13 +785,6 @@ class OrderControlReportService
             $lines[] = '';
         }
 
-        // Секция "Успех без данных" - специальный формат с причиной
-        if (!empty($result->successMissingData)) {
-            $lines[] = '*Успех без данных* \\(' . count($result->successMissingData) . '\\)';
-            $lines[] = $this->formatSuccessMissingDataTable($result->successMissingData);
-            $lines[] = '';
-        }
-
         $lines[] = '*Всего:* ' . $this->escapeMarkdownV2((string)$result->totalIssues);
 
         return implode("\n", $lines);
@@ -946,34 +813,6 @@ class OrderControlReportService
         return implode("\n", $rows);
     }
 
-    /**
-     * Форматирует таблицу "Успех без данных" для Telegram
-     *
-     * Специальный формат: | Дата | Интервал | Заказ | Причина
-     * Показывает причину проблемы вместо статусов РМК/МП.
-     *
-     * @param OrderIssue[] $issues
-     * @return string
-     */
-    private function formatSuccessMissingDataTable(array $issues): string
-    {
-        $rows = [];
-        $rows[] = '```';
-        $rows[] = '| Дата | Интервал | Заказ | Причина';
-
-        foreach ($issues as $issue) {
-            $date = $issue->reportDate ?: date('d.m.Y');
-            $interval = $this->getShortInterval($issue->interval);
-            $reason = $issue->getIssueReasonLabel() ?? 'Неизвестно';
-
-            $rows[] = sprintf('| %s | %s | %s | %s', $date, $interval, $issue->orderNumber, $reason);
-        }
-
-        $rows[] = '```';
-
-        return implode("\n", $rows);
-    }
-
     /**
      * Форматирует строку таблицы для проблемы
      *
@@ -1008,7 +847,7 @@ class OrderControlReportService
     private function getShortInterval(?string $interval): string
     {
         if ($interval === null) {
-            return (int)date('H') < 12 ? '08:00' : '20:00';
+            return OrderIssue::calculateInterval($this->testMode);
         }
 
         // Убираем суффиксы типа " (Ночь)" или " (День)"
@@ -1115,9 +954,6 @@ class OrderControlReportService
         foreach ($result->cancelNoProcess as $issue) {
             $allIssues[] = ['type' => 'Отмена без обработки', 'issue' => $issue];
         }
-        foreach ($result->successMissingData as $issue) {
-            $allIssues[] = ['type' => 'Успех без данных', 'issue' => $issue];
-        }
 
         // Сортируем по типу
         usort($allIssues, function ($a, $b) {
@@ -1132,21 +968,15 @@ class OrderControlReportService
             <th>Интервал</th>
             <th>Заказ</th>
             <th>РМК</th>
-            <th>МП / Причина</th>
+            <th>МП</th>
         </tr>';
 
         foreach ($allIssues as $item) {
             /** @var OrderIssue $issue */
             $issue = $item['issue'];
             $date = $issue->reportDate ?: date('d.m.Y');
-            $interval = $issue->interval ?: ((int)date('H') < 12 ? '08:00' : '20:00');
-
-            // Для "Успех без данных" показываем причину вместо МП-статуса
-            if ($issue->problemType === OrderIssue::TYPE_SUCCESS_MISSING_DATA) {
-                $mpOrReason = $issue->getIssueReasonLabel() ?? 'Неизвестно';
-            } else {
-                $mpOrReason = $issue->mpStatus ?? '-';
-            }
+            $interval = $issue->interval ?: OrderIssue::calculateInterval($this->testMode);
+            $mpStatus = $issue->mpStatus ?? '-';
 
             $html .= '
         <tr>
@@ -1155,7 +985,7 @@ class OrderControlReportService
             <td>' . $this->escapeHtml($interval) . '</td>
             <td>' . $this->escapeHtml($issue->orderNumber) . '</td>
             <td>' . $this->escapeHtml($issue->rmkStatus ?? '-') . '</td>
-            <td>' . $this->escapeHtml($mpOrReason) . '</td>
+            <td>' . $this->escapeHtml($mpStatus) . '</td>
         </tr>';
         }
 
@@ -1534,46 +1364,52 @@ class OrderControlReportService
     {
         $now = new \DateTime('now', new \DateTimeZone(self::TIMEZONE));
         $currentHour = (int)$now->format('H');
+        $isDayTime = $currentHour >= 8 && $currentHour < 20;
 
         $shiftStart = clone $now;
         $shiftEnd = clone $now;
         $shiftName = '';
 
-        if ($currentHour >= 8 && $currentHour < 20) {
-            // Ð\94невнаÑ\8f Ñ\81мена: 08:00 - 20:00
-            $shiftStart->setTime(8, 0, 0);
-            if ($testMode) {
-                // Тестовый режим: endDate = текущее время
+        if ($testMode) {
+            // Ð¢ÐµÑ\81Ñ\82овÑ\8bй Ñ\80ежим: Ð°Ð½Ð°Ð»Ð¸Ð·Ð¸Ñ\80Ñ\83ем Ð¢Ð\95Ð\9aУЩУЮ Ñ\81менÑ\83, endDate = Ñ\82екÑ\83Ñ\89ее Ð²Ñ\80емÑ\8f
+            if ($isDayTime) {
+                // Текущая дневная смена: 08:00 сегодня - текущее время
+                $shiftStart->setTime(8, 0, 0);
                 // $shiftEnd уже = $now
-            } else {
-                // Стандартный режим: endDate = 20:00 сегодня
-                $shiftEnd->setTime(20, 0, 0);
-            }
-            $shiftName = 'Дневная смена (08:00-20:00)';
-        } elseif ($currentHour >= 20) {
-            // Ночная смена (начало): 20:00 сегодня - 08:00 завтра
-            $shiftStart->setTime(20, 0, 0);
-            if ($testMode) {
-                // Тестовый режим: endDate = текущее время
+                $shiftName = 'Дневная смена (08:00-20:00)';
+            } elseif ($currentHour >= 20) {
+                // Текущая ночная смена (начало): 20:00 сегодня - текущее время
+                $shiftStart->setTime(20, 0, 0);
                 // $shiftEnd уже = $now
+                $shiftName = 'Ночная смена (20:00-08:00)';
             } else {
-                // Стандартный режим: endDate = 08:00 завтра
-                $shiftEnd->modify('+1 day');
-                $shiftEnd->setTime(8, 0, 0);
+                // Текущая ночная смена (продолжение): 20:00 вчера - текущее время
+                $shiftStart->modify('-1 day');
+                $shiftStart->setTime(20, 0, 0);
+                // $shiftEnd уже = $now
+                $shiftName = 'Ночная смена (20:00-08:00)';
             }
-            $shiftName = 'Ночная смена (20:00-08:00)';
         } else {
-            // Ночная смена (продолжение): 20:00 вчера - 08:00 сегодня
-            $shiftStart->modify('-1 day');
-            $shiftStart->setTime(20, 0, 0);
-            if ($testMode) {
-                // Тестовый режим: endDate = текущее время
-                // $shiftEnd уже = $now
-            } else {
-                // Стандартный режим: endDate = 08:00 сегодня
+            // Обычный режим: анализируем ПРЕДЫДУЩУЮ смену
+            if ($isDayTime) {
+                // Сейчас день → анализируем предыдущую НОЧНУЮ смену (20:00 вчера - 08:00 сегодня)
+                $shiftStart->modify('-1 day');
+                $shiftStart->setTime(20, 0, 0);
                 $shiftEnd->setTime(8, 0, 0);
+                $shiftName = 'Ночная смена (20:00-08:00)';
+            } elseif ($currentHour >= 20) {
+                // Сейчас ночь (20:00-23:59) → анализируем предыдущую ДНЕВНУЮ смену (08:00-20:00 сегодня)
+                $shiftStart->setTime(8, 0, 0);
+                $shiftEnd->setTime(20, 0, 0);
+                $shiftName = 'Дневная смена (08:00-20:00)';
+            } else {
+                // Сейчас ночь (00:00-07:59) → анализируем предыдущую ДНЕВНУЮ смену (08:00-20:00 вчера)
+                $shiftStart->modify('-1 day');
+                $shiftStart->setTime(8, 0, 0);
+                $shiftEnd->modify('-1 day');
+                $shiftEnd->setTime(20, 0, 0);
+                $shiftName = 'Дневная смена (08:00-20:00)';
             }
-            $shiftName = 'Ночная смена (20:00-08:00)';
         }
 
         $this->logInfo('Вычислен диапазон дат на основе смены', [
index 8b3a41e91986b9d73316eca3c3955a612f72aff2..f85e1b3c40c3b05baf0c1b74eec450a5b3bc2a62 100644 (file)
@@ -2370,8 +2370,25 @@ class UploadService {
                 $marketplaceOrder->cancelled_order_source = '1c';
                 $marketplaceOrder->cancelled_order_date = date('Y-m-d H:m:s');
             }
-            $marketplaceOrder->status_processing_1c = $statusId1C->id;
-            $marketplaceOrder->seller_id = $mpOrder['seller_id'] ?? null;
+
+            $incomingSellerId = $mpOrder['seller_id'] ?? null;
+            $emptyGuid = '00000000-0000-0000-0000-000000000000';
+            $isSellerIdEmpty = empty($incomingSellerId) || $incomingSellerId === $emptyGuid;
+            $isCheckGuidEmpty = empty($marketplaceOrder->check_guid);
+            $successfulOrderIds = MarketplaceOrder1cStatuses::getSuccessfulOrderIds();
+
+            // Не устанавливаем статус "Успех" без seller_id или без check_guid
+            if (in_array($statusId1C->id, $successfulOrderIds) && ($isSellerIdEmpty || $isCheckGuidEmpty)) {
+                Yii::warning(
+                    "Статус 'Успех' (ID: {$statusId1C->id}) без seller_id или check_guid для заказа {$mpOrder['id']}. " .
+                    "seller_id: " . ($incomingSellerId ?? 'null') . ", check_guid: " . ($marketplaceOrder->check_guid ?? 'null'),
+                    'marketplace-status'
+                );
+            } else {
+                $marketplaceOrder->status_processing_1c = $statusId1C->id;
+            }
+
+            $marketplaceOrder->seller_id = $isSellerIdEmpty ? null : $incomingSellerId;
             $marketplaceOrder->number_1c = $mpOrder['number'] ?? null;
 
 
index bf50531a5f4703afc8c37503eb527ba7692207c4..389aeb1b974941cd833e3df5037c6a545ecd041d 100644 (file)
@@ -65,13 +65,6 @@ class ControlReportResult
      */
     public array $cancelNoProcess = [];
 
-    /**
-     * Заказы типа "Успех без данных" (успех в МП и 1С, но нет seller_id и/или check_guid)
-     *
-     * @var OrderIssue[]
-     */
-    public array $successMissingData = [];
-
     /**
      * Успешность отправки в Telegram
      */
@@ -99,13 +92,15 @@ class ControlReportResult
 
     /**
      * Конструктор
+     *
+     * @param bool $testMode Тестовый режим (анализ текущей смены)
      */
-    public function __construct()
+    public function __construct(bool $testMode = false)
     {
         $tz = new \DateTimeZone('Europe/Moscow');
         $now = new \DateTime('now', $tz);
         $this->reportDate = $now->format('d.m.Y H:i');
-        $this->interval = (int)$now->format('H') < 12 ? '08:00' : '20:00';
+        $this->interval = OrderIssue::calculateInterval($testMode);
     }
 
     /**
@@ -138,16 +133,6 @@ class ControlReportResult
         return count($this->cancelNoProcess);
     }
 
-    /**
-     * Получает количество заказов по типу "Успех без данных"
-     *
-     * @return int
-     */
-    public function getSuccessMissingDataCount(): int
-    {
-        return count($this->successMissingData);
-    }
-
     /**
      * Рассчитывает и обновляет общее количество проблем
      *
@@ -157,8 +142,7 @@ class ControlReportResult
     {
         $this->totalIssues = $this->getHungInDeliveryCount()
             + $this->getSuccessNoCheckCount()
-            + $this->getCancelNoProcessCount()
-            + $this->getSuccessMissingDataCount();
+            + $this->getCancelNoProcessCount();
 
         return $this->totalIssues;
     }
@@ -173,8 +157,7 @@ class ControlReportResult
         return $this->totalIssues > 0
             || !empty($this->hungInDelivery)
             || !empty($this->successNoCheck)
-            || !empty($this->cancelNoProcess)
-            || !empty($this->successMissingData);
+            || !empty($this->cancelNoProcess);
     }
 
     /**
@@ -187,8 +170,7 @@ class ControlReportResult
         return array_merge(
             $this->hungInDelivery,
             $this->successNoCheck,
-            $this->cancelNoProcess,
-            $this->successMissingData
+            $this->cancelNoProcess
         );
     }
 
@@ -203,7 +185,6 @@ class ControlReportResult
             OrderIssue::TYPE_HUNG_IN_DELIVERY => $this->hungInDelivery,
             OrderIssue::TYPE_SUCCESS_NO_CHECK => $this->successNoCheck,
             OrderIssue::TYPE_CANCEL_NO_PROCESS => $this->cancelNoProcess,
-            OrderIssue::TYPE_SUCCESS_MISSING_DATA => $this->successMissingData,
         ];
     }
 
@@ -266,7 +247,6 @@ class ControlReportResult
             'hung_in_delivery_count' => $this->getHungInDeliveryCount(),
             'success_no_check_count' => $this->getSuccessNoCheckCount(),
             'cancel_no_process_count' => $this->getCancelNoProcessCount(),
-            'success_missing_data_count' => $this->getSuccessMissingDataCount(),
             'telegram_sent' => $this->telegramSent,
             'email_sent' => $this->emailSent,
             'telegram_error' => $this->telegramError,
@@ -319,12 +299,11 @@ class ControlReportResult
     public function getSummary(): string
     {
         return sprintf(
-            'Контроль МП: %d проблем (завис: %d, успех без чека: %d, отмена: %d, успех без данных: %d). TG: %s, Email: %s',
+            'Контроль МП: %d проблем (завис: %d, успех без чека: %d, отмена: %d). TG: %s, Email: %s',
             $this->totalIssues,
             $this->getHungInDeliveryCount(),
             $this->getSuccessNoCheckCount(),
             $this->getCancelNoProcessCount(),
-            $this->getSuccessMissingDataCount(),
             $this->telegramSent ? 'OK' : 'FAIL',
             $this->emailSent ? 'OK' : 'FAIL'
         );
index 8d54138f66731e6a59f172516d6f9e94b26e575b..f74b9530e155cce77ce112395c756f62a70860ea 100644 (file)
@@ -27,11 +27,6 @@ class OrderIssue
      */
     public const TYPE_CANCEL_NO_PROCESS = 'cancel_no_process';
 
-    /**
-     * Тип проблемы: "Успех без данных" (успех в МП и 1С, но нет seller_id и/или check_guid)
-     */
-    public const TYPE_SUCCESS_MISSING_DATA = 'success_missing_data';
-
     /**
      * Метки типов проблем для отображения
      */
@@ -39,7 +34,6 @@ class OrderIssue
         self::TYPE_HUNG_IN_DELIVERY => 'Завис в доставке',
         self::TYPE_SUCCESS_NO_CHECK => 'Успех без чека',
         self::TYPE_CANCEL_NO_PROCESS => 'Отмена без обработки',
-        self::TYPE_SUCCESS_MISSING_DATA => 'Успех без данных',
     ];
 
     /**
@@ -164,34 +158,53 @@ class OrderIssue
      * @param string $problemType Тип проблемы
      * @param int $orderId ID заказа
      * @param string $orderNumber Номер заказа
+     * @param bool $testMode Тестовый режим (анализ текущей смены вместо предыдущей)
      */
     public function __construct(
         string $problemType,
         int $orderId,
-        string $orderNumber
+        string $orderNumber,
+        bool $testMode = false
     ) {
         $this->problemType = $problemType;
         $this->problemTypeLabel = self::TYPE_LABELS[$problemType] ?? $problemType;
         $this->orderId = $orderId;
         $this->orderNumber = $orderNumber;
         $this->reportDate = date('d.m.Y');
-        $this->interval = $this->calculateInterval();
+        $this->interval = self::calculateInterval($testMode);
         $this->total = 0.0;
     }
 
     /**
      * Определяет интервал проверки (Дневная/Ночная смена)
      *
-     * Возвращает только время без суффикса, т.к. поле в БД ограничено 8 символами.
-     * 08:00 - утренняя проверка (ночная смена завершилась)
-     * 20:00 - вечерняя проверка (дневная смена завершилась)
+     * Интервал указывает время НАЧАЛА анализируемой смены:
+     * - 08:00 — дневная смена (08:00-20:00)
+     * - 20:00 — ночная смена (20:00-08:00)
+     *
+     * Обычный режим (testMode=false): анализируем ПРЕДЫДУЩУЮ смену
+     * - Запуск 08:00-19:59 (день) → ночная смена → интервал '20:00'
+     * - Запуск 20:00-07:59 (ночь) → дневная смена → интервал '08:00'
      *
+     * Тестовый режим (testMode=true): анализируем ТЕКУЩУЮ смену
+     * - Запуск 08:00-19:59 (день) → дневная смена → интервал '08:00'
+     * - Запуск 20:00-07:59 (ночь) → ночная смена → интервал '20:00'
+     *
+     * @param bool $testMode Тестовый режим
      * @return string
      */
-    private function calculateInterval(): string
+    public static function calculateInterval(bool $testMode = false): string
     {
         $hour = (int)date('H');
-        return $hour < 12 ? '08:00' : '20:00';
+        $isDayTime = $hour >= 8 && $hour < 20;
+
+        if ($testMode) {
+            // Тестовый режим: анализируем ТЕКУЩУЮ смену
+            return $isDayTime ? '08:00' : '20:00';
+        }
+
+        // Обычный режим: анализируем ПРЕДЫДУЩУЮ смену
+        return $isDayTime ? '20:00' : '08:00';
     }
 
     /**
@@ -199,14 +212,16 @@ class OrderIssue
      *
      * @param string $problemType Тип проблемы
      * @param array $orderData Данные заказа из БД
+     * @param bool $testMode Тестовый режим (анализ текущей смены)
      * @return self
      */
-    public static function fromOrderData(string $problemType, array $orderData): self
+    public static function fromOrderData(string $problemType, array $orderData, bool $testMode = false): self
     {
         $issue = new self(
             $problemType,
             (int)($orderData['id'] ?? 0),
-            $orderData['marketplace_order_id'] ?? ''
+            $orderData['marketplace_order_id'] ?? '',
+            $testMode
         );
 
         $issue->rmkStatus = $orderData['rmk_status'] ?? null;
index 70d83868eda78f7db79833c7fb01b27acf818330..c039d68ac49cae3951f28e92f25991a130188120 100644 (file)
@@ -35,7 +35,6 @@ class MarketplaceOrderDailyIssuesTest extends Unit
         $this->assertSame(OrderIssue::TYPE_HUNG_IN_DELIVERY, MarketplaceOrderDailyIssues::TYPE_HUNG_IN_DELIVERY);
         $this->assertSame(OrderIssue::TYPE_SUCCESS_NO_CHECK, MarketplaceOrderDailyIssues::TYPE_SUCCESS_NO_CHECK);
         $this->assertSame(OrderIssue::TYPE_CANCEL_NO_PROCESS, MarketplaceOrderDailyIssues::TYPE_CANCEL_NO_PROCESS);
-        $this->assertSame(OrderIssue::TYPE_SUCCESS_MISSING_DATA, MarketplaceOrderDailyIssues::TYPE_SUCCESS_MISSING_DATA);
     }
 
     /**
@@ -101,7 +100,6 @@ class MarketplaceOrderDailyIssuesTest extends Unit
         $this->assertContains(MarketplaceOrderDailyIssues::TYPE_HUNG_IN_DELIVERY, $inRule['range']);
         $this->assertContains(MarketplaceOrderDailyIssues::TYPE_SUCCESS_NO_CHECK, $inRule['range']);
         $this->assertContains(MarketplaceOrderDailyIssues::TYPE_CANCEL_NO_PROCESS, $inRule['range']);
-        $this->assertContains(MarketplaceOrderDailyIssues::TYPE_SUCCESS_MISSING_DATA, $inRule['range']);
     }
 
     /**
@@ -205,10 +203,6 @@ class MarketplaceOrderDailyIssuesTest extends Unit
                 MarketplaceOrderDailyIssues::TYPE_CANCEL_NO_PROCESS,
                 'Отмена без обработки',
             ],
-            'success_missing_data' => [
-                MarketplaceOrderDailyIssues::TYPE_SUCCESS_MISSING_DATA,
-                'Успех без данных',
-            ],
         ];
     }
 
@@ -343,13 +337,13 @@ class MarketplaceOrderDailyIssuesTest extends Unit
      */
     public function testFromOrderIssueWithMinimalData(): void
     {
-        $issue = new OrderIssue(OrderIssue::TYPE_SUCCESS_MISSING_DATA, 1, 'TEST-1');
+        $issue = new OrderIssue(OrderIssue::TYPE_SUCCESS_NO_CHECK, 1, 'TEST-1');
 
         $model = MarketplaceOrderDailyIssues::fromOrderIssue($issue);
 
         $this->assertSame(1, $model->order_id);
         $this->assertSame('TEST-1', $model->marketplace_order_id);
-        $this->assertSame(OrderIssue::TYPE_SUCCESS_MISSING_DATA, $model->problem_type);
+        $this->assertSame(OrderIssue::TYPE_SUCCESS_NO_CHECK, $model->problem_type);
         $this->assertNull($model->rmk_status_id);
         $this->assertNull($model->rmk_status);
         $this->assertNull($model->mp_status_code);
index 305d13d8f7faeb977f18494f9b99f187a2736d67..1cb4c0552b19171fb4a85be75dab1cf680b67e22 100644 (file)
@@ -162,15 +162,9 @@ class OrderControlReportServiceTest extends Unit
         $issue3->mpStatus = 'Отменён';
         $issue3->rmkStatus = 'Новый';
 
-        $issue4 = new OrderIssue(OrderIssue::TYPE_SUCCESS_MISSING_DATA, 4, 'FW-4');
-        $issue4->mpStatus = 'Доставлен';
-        $issue4->rmkStatus = 'Успех';
-        $issue4->issueReason = 'no_seller_id';
-
         $result->hungInDelivery = [$issue1];
         $result->successNoCheck = [$issue2];
         $result->cancelNoProcess = [$issue3];
-        $result->successMissingData = [$issue4];
         $result->calculateTotal();
 
         $message = $this->service->formatTelegramControlReport($result);
@@ -178,7 +172,6 @@ class OrderControlReportServiceTest extends Unit
         $this->assertStringContainsString('Завис в доставке', $message);
         $this->assertStringContainsString('Успех без чека', $message);
         $this->assertStringContainsString('Отмена без обработки', $message);
-        $this->assertStringContainsString('Успех без данных', $message);
     }
 
     /**
@@ -287,24 +280,24 @@ class OrderControlReportServiceTest extends Unit
     }
 
     /**
-     * Тест: formatEmailControlReport показывает причину для success_missing_data
+     * Тест: formatEmailControlReport показывает причину для success_no_check
      */
-    public function testFormatEmailControlReportShowsIssueReasonForMissingData(): void
+    public function testFormatEmailControlReportShowsIssueReasonForSuccessNoCheck(): void
     {
         $result = new ControlReportResult();
 
-        $issue = new OrderIssue(OrderIssue::TYPE_SUCCESS_MISSING_DATA, 300, 'FW-300');
-        $issue->rmkStatus = '6. Успех';
+        $issue = new OrderIssue(OrderIssue::TYPE_SUCCESS_NO_CHECK, 300, 'FW-300');
+        $issue->rmkStatus = 'Новый';
         $issue->mpStatus = 'Доставлен';
-        $issue->issueReason = 'no_seller_and_check_guid';
+        $issue->issueReason = 'no_seller_id';
 
-        $result->successMissingData = [$issue];
+        $result->successNoCheck = [$issue];
         $result->calculateTotal();
 
         $html = $this->service->formatEmailControlReport($result);
 
-        $this->assertStringContainsString('Успех без данных', $html);
-        $this->assertStringContainsString('Нет seller_id и check_guid', $html);
+        $this->assertStringContainsString('Успех без чека', $html);
+        $this->assertStringContainsString('Нет seller_id', $html);
     }
 
     /**
index c96a5dddec9e60ec20176722362dfee688074fea..5afbc06a849bed7e0e0d24a879a965aa1539b7d2 100644 (file)
@@ -36,7 +36,6 @@ class ControlReportResultTest extends Unit
         $this->assertSame([], $result->hungInDelivery);
         $this->assertSame([], $result->successNoCheck);
         $this->assertSame([], $result->cancelNoProcess);
-        $this->assertSame([], $result->successMissingData);
         $this->assertFalse($result->telegramSent);
         $this->assertFalse($result->emailSent);
         $this->assertNull($result->telegramError);
@@ -106,22 +105,6 @@ class ControlReportResultTest extends Unit
         $this->assertSame(1, $result->getCancelNoProcessCount());
     }
 
-    /**
-     * Тест: getSuccessMissingDataCount возвращает количество элементов
-     */
-    public function testGetSuccessMissingDataCountReturnsCorrectCount(): void
-    {
-        $result = new ControlReportResult();
-        $this->assertSame(0, $result->getSuccessMissingDataCount());
-
-        $result->successMissingData = [
-            new OrderIssue(OrderIssue::TYPE_SUCCESS_MISSING_DATA, 200, 'FW-200'),
-            new OrderIssue(OrderIssue::TYPE_SUCCESS_MISSING_DATA, 201, 'FW-201'),
-        ];
-
-        $this->assertSame(2, $result->getSuccessMissingDataCount());
-    }
-
     /**
      * Тест: calculateTotal рассчитывает и возвращает общее количество
      */
@@ -141,14 +124,11 @@ class ControlReportResultTest extends Unit
             new OrderIssue(OrderIssue::TYPE_CANCEL_NO_PROCESS, 101, 'FW-101'),
             new OrderIssue(OrderIssue::TYPE_CANCEL_NO_PROCESS, 102, 'FW-102'),
         ];
-        $result->successMissingData = [
-            new OrderIssue(OrderIssue::TYPE_SUCCESS_MISSING_DATA, 200, 'FW-200'),
-        ];
 
         $total = $result->calculateTotal();
 
-        $this->assertSame(7, $total);
-        $this->assertSame(7, $result->totalIssues);
+        $this->assertSame(6, $total);
+        $this->assertSame(6, $result->totalIssues);
     }
 
     /**
@@ -191,7 +171,6 @@ class ControlReportResultTest extends Unit
             'hungInDelivery' => ['hungInDelivery'],
             'successNoCheck' => ['successNoCheck'],
             'cancelNoProcess' => ['cancelNoProcess'],
-            'successMissingData' => ['successMissingData'],
         ];
     }
 
@@ -205,20 +184,17 @@ class ControlReportResultTest extends Unit
         $issue1 = new OrderIssue(OrderIssue::TYPE_HUNG_IN_DELIVERY, 1, 'FW-1');
         $issue2 = new OrderIssue(OrderIssue::TYPE_SUCCESS_NO_CHECK, 2, 'YM-2');
         $issue3 = new OrderIssue(OrderIssue::TYPE_CANCEL_NO_PROCESS, 3, 'FW-3');
-        $issue4 = new OrderIssue(OrderIssue::TYPE_SUCCESS_MISSING_DATA, 4, 'FW-4');
 
         $result->hungInDelivery = [$issue1];
         $result->successNoCheck = [$issue2];
         $result->cancelNoProcess = [$issue3];
-        $result->successMissingData = [$issue4];
 
         $allIssues = $result->getAllIssues();
 
-        $this->assertCount(4, $allIssues);
+        $this->assertCount(3, $allIssues);
         $this->assertSame($issue1, $allIssues[0]);
         $this->assertSame($issue2, $allIssues[1]);
         $this->assertSame($issue3, $allIssues[2]);
-        $this->assertSame($issue4, $allIssues[3]);
     }
 
     /**
@@ -239,12 +215,10 @@ class ControlReportResultTest extends Unit
         $this->assertArrayHasKey(OrderIssue::TYPE_HUNG_IN_DELIVERY, $grouped);
         $this->assertArrayHasKey(OrderIssue::TYPE_SUCCESS_NO_CHECK, $grouped);
         $this->assertArrayHasKey(OrderIssue::TYPE_CANCEL_NO_PROCESS, $grouped);
-        $this->assertArrayHasKey(OrderIssue::TYPE_SUCCESS_MISSING_DATA, $grouped);
 
         $this->assertSame([$issue1], $grouped[OrderIssue::TYPE_HUNG_IN_DELIVERY]);
         $this->assertSame([$issue2], $grouped[OrderIssue::TYPE_SUCCESS_NO_CHECK]);
         $this->assertSame([], $grouped[OrderIssue::TYPE_CANCEL_NO_PROCESS]);
-        $this->assertSame([], $grouped[OrderIssue::TYPE_SUCCESS_MISSING_DATA]);
     }
 
     /**
@@ -365,7 +339,6 @@ class ControlReportResultTest extends Unit
         $result->hungInDelivery = [new OrderIssue(OrderIssue::TYPE_HUNG_IN_DELIVERY, 1, 'FW-1')];
         $result->successNoCheck = [new OrderIssue(OrderIssue::TYPE_SUCCESS_NO_CHECK, 2, 'YM-2')];
         $result->cancelNoProcess = [];
-        $result->successMissingData = [new OrderIssue(OrderIssue::TYPE_SUCCESS_MISSING_DATA, 3, 'FW-3')];
         $result->calculateTotal();
         $result->telegramSent = true;
         $result->emailSent = false;
@@ -384,7 +357,6 @@ class ControlReportResultTest extends Unit
         $this->assertArrayHasKey('hung_in_delivery_count', $array);
         $this->assertArrayHasKey('success_no_check_count', $array);
         $this->assertArrayHasKey('cancel_no_process_count', $array);
-        $this->assertArrayHasKey('success_missing_data_count', $array);
         $this->assertArrayHasKey('telegram_sent', $array);
         $this->assertArrayHasKey('email_sent', $array);
         $this->assertArrayHasKey('telegram_error', $array);
@@ -395,11 +367,10 @@ class ControlReportResultTest extends Unit
         $this->assertSame('2026-01-19 20:00:00', $array['start_date']);
         $this->assertSame('2026-01-20 08:00:00', $array['end_date']);
         $this->assertSame('Ночная смена', $array['shift_name']);
-        $this->assertSame(3, $array['total_issues']);
+        $this->assertSame(2, $array['total_issues']);
         $this->assertSame(1, $array['hung_in_delivery_count']);
         $this->assertSame(1, $array['success_no_check_count']);
         $this->assertSame(0, $array['cancel_no_process_count']);
-        $this->assertSame(1, $array['success_missing_data_count']);
         $this->assertTrue($array['telegram_sent']);
         $this->assertFalse($array['email_sent']);
         $this->assertNull($array['telegram_error']);
@@ -464,9 +435,6 @@ class ControlReportResultTest extends Unit
         ];
         $result->successNoCheck = [new OrderIssue(OrderIssue::TYPE_SUCCESS_NO_CHECK, 3, 'YM-3')];
         $result->cancelNoProcess = [];
-        $result->successMissingData = [
-            new OrderIssue(OrderIssue::TYPE_SUCCESS_MISSING_DATA, 4, 'FW-4'),
-        ];
         $result->calculateTotal();
         $result->telegramSent = true;
         $result->emailSent = false;
@@ -474,7 +442,7 @@ class ControlReportResultTest extends Unit
         $summary = $result->getSummary();
 
         $this->assertSame(
-            'Контроль МП: 4 проблем (завис: 2, успех без чека: 1, отмена: 0, успех без данных: 1). TG: OK, Email: FAIL',
+            'Контроль МП: 3 проблем (завис: 2, успех без чека: 1, отмена: 0). TG: OK, Email: FAIL',
             $summary
         );
     }
@@ -493,4 +461,32 @@ class ControlReportResultTest extends Unit
         $this->assertStringContainsString('TG: OK', $summary);
         $this->assertStringContainsString('Email: OK', $summary);
     }
+
+    /**
+     * Тест: конструктор использует testMode для расчёта интервала
+     *
+     * Логика интервалов:
+     * - Обычный режим (testMode=false): анализируем ПРЕДЫДУЩУЮ смену
+     *   - Днём (08:00-19:59) → интервал '20:00'
+     *   - Ночью (20:00-07:59) → интервал '08:00'
+     * - Тестовый режим (testMode=true): анализируем ТЕКУЩУЮ смену
+     *   - Днём (08:00-19:59) → интервал '08:00'
+     *   - Ночью (20:00-07:59) → интервал '20:00'
+     */
+    public function testConstructorUsesTestModeForInterval(): void
+    {
+        $normalResult = new ControlReportResult(false);
+        $testResult = new ControlReportResult(true);
+
+        // Оба интервала должны быть допустимыми
+        $this->assertMatchesRegularExpression('/^(08:00|20:00)$/', $normalResult->interval);
+        $this->assertMatchesRegularExpression('/^(08:00|20:00)$/', $testResult->interval);
+
+        // Интервалы должны быть противоположными
+        $this->assertNotSame($normalResult->interval, $testResult->interval);
+
+        // Интервалы должны соответствовать OrderIssue::calculateInterval
+        $this->assertSame(OrderIssue::calculateInterval(false), $normalResult->interval);
+        $this->assertSame(OrderIssue::calculateInterval(true), $testResult->interval);
+    }
 }
index 7262bc02c4ffd3b4c21e92b2b054b9f6c2fc5131..9a2c6dac137cd77706e5179adae815d48edb2771 100644 (file)
@@ -50,7 +50,6 @@ class OrderIssueTest extends Unit
             OrderIssue::TYPE_HUNG_IN_DELIVERY => 'Завис в доставке',
             OrderIssue::TYPE_SUCCESS_NO_CHECK => 'Успех без чека',
             OrderIssue::TYPE_CANCEL_NO_PROCESS => 'Отмена без обработки',
-            OrderIssue::TYPE_SUCCESS_MISSING_DATA => 'Успех без данных',
         ];
 
         foreach ($expectedTypes as $type => $label) {
@@ -139,29 +138,26 @@ class OrderIssueTest extends Unit
                     'total' => 3200.00,
                 ],
             ],
-            'success_missing_data' => [
-                OrderIssue::TYPE_SUCCESS_MISSING_DATA,
+            'cancel_no_process' => [
+                OrderIssue::TYPE_CANCEL_NO_PROCESS,
                 [
                     'id' => 300,
                     'marketplace_order_id' => 'FW-300',
-                    'rmk_status' => '6. Успех',
-                    'rmk_status_id' => '6',
-                    'mp_status_name' => 'Ð\94оÑ\81Ñ\82авлен',
-                    'mp_status_code' => 'DELIVERED',
+                    'rmk_status' => 'Передан курьеру',
+                    'rmk_status_id' => '5',
+                    'mp_status_name' => 'Ð\9eÑ\82менÑ\91н',
+                    'mp_status_code' => 'CANCELLED',
                     'store_id' => 30,
                     'store_name' => 'Магазин Юг',
                     'marketplace_name' => 'Flowwow',
                     'marketplace_id' => 1,
                     'total' => 6800.00,
-                    'seller_id' => '',
-                    'check_guid' => null,
-                    'issue_reason' => 'no_seller_and_check_guid',
                 ],
                 [
                     'orderId' => 300,
                     'orderNumber' => 'FW-300',
-                    'rmkStatus' => '6. Успех',
-                    'mpStatus' => 'Ð\94оÑ\81Ñ\82авлен',
+                    'rmkStatus' => 'Передан курьеру',
+                    'mpStatus' => 'Ð\9eÑ\82менÑ\91н',
                     'storeId' => 30,
                     'storeName' => 'Магазин Юг',
                     'total' => 6800.00,
@@ -259,9 +255,9 @@ class OrderIssueTest extends Unit
      */
     public function testToArrayReturnsAllFields(): void
     {
-        $issue = new OrderIssue(OrderIssue::TYPE_SUCCESS_MISSING_DATA, 500, 'FW-500');
-        $issue->rmkStatus = '6. Успех';
-        $issue->rmkStatusId = '6';
+        $issue = new OrderIssue(OrderIssue::TYPE_SUCCESS_NO_CHECK, 500, 'FW-500');
+        $issue->rmkStatus = 'Новый';
+        $issue->rmkStatusId = '1';
         $issue->mpStatus = 'Доставлен';
         $issue->mpStatusCode = 'DELIVERED';
         $issue->mpSubstatusCode = 'DELIVERY_SERVICE_DELIVERED';
@@ -274,7 +270,7 @@ class OrderIssueTest extends Unit
         $issue->sellerId = '';
         $issue->checkGuid = null;
         $issue->checkExists = false;
-        $issue->issueReason = 'no_seller_and_check_guid';
+        $issue->issueReason = 'no_seller_id';
 
         $array = $issue->toArray();
 
@@ -301,12 +297,12 @@ class OrderIssueTest extends Unit
         $this->assertArrayHasKey('issue_reason', $array);
         $this->assertArrayHasKey('issue_reason_label', $array);
 
-        $this->assertSame(OrderIssue::TYPE_SUCCESS_MISSING_DATA, $array['problem_type']);
-        $this->assertSame('Успех без данных', $array['problem_type_label']);
+        $this->assertSame(OrderIssue::TYPE_SUCCESS_NO_CHECK, $array['problem_type']);
+        $this->assertSame('Успех без чека', $array['problem_type_label']);
         $this->assertSame(500, $array['order_id']);
         $this->assertSame('FW-500', $array['order_number']);
-        $this->assertSame('no_seller_and_check_guid', $array['issue_reason']);
-        $this->assertSame('Нет seller_id и check_guid', $array['issue_reason_label']);
+        $this->assertSame('no_seller_id', $array['issue_reason']);
+        $this->assertSame('Нет seller_id', $array['issue_reason_label']);
     }
 
     /**
@@ -341,4 +337,83 @@ class OrderIssueTest extends Unit
 
         $this->assertSame('DELIVERED/DELIVERY_SERVICE_DELIVERED', $issue->mpStatus);
     }
+
+    /**
+     * Тест: calculateInterval возвращает допустимые значения
+     */
+    public function testCalculateIntervalReturnsValidValues(): void
+    {
+        $normalMode = OrderIssue::calculateInterval(false);
+        $testMode = OrderIssue::calculateInterval(true);
+
+        // Оба значения должны быть либо '08:00', либо '20:00'
+        $this->assertMatchesRegularExpression('/^(08:00|20:00)$/', $normalMode);
+        $this->assertMatchesRegularExpression('/^(08:00|20:00)$/', $testMode);
+    }
+
+    /**
+     * Тест: calculateInterval возвращает противоположные значения для testMode и normalMode
+     *
+     * Логика интервалов:
+     * - Обычный режим (testMode=false): анализируем ПРЕДЫДУЩУЮ смену
+     *   - Днём (08:00-19:59) → анализируем ночную смену → '20:00'
+     *   - Ночью (20:00-07:59) → анализируем дневную смену → '08:00'
+     * - Тестовый режим (testMode=true): анализируем ТЕКУЩУЮ смену
+     *   - Днём (08:00-19:59) → анализируем дневную смену → '08:00'
+     *   - Ночью (20:00-07:59) → анализируем ночную смену → '20:00'
+     *
+     * Таким образом, в любой момент времени значения для testMode и normalMode противоположны.
+     */
+    public function testCalculateIntervalReturnsOppositeValuesForTestMode(): void
+    {
+        $normalMode = OrderIssue::calculateInterval(false);
+        $testMode = OrderIssue::calculateInterval(true);
+
+        $this->assertNotSame($normalMode, $testMode);
+
+        // Если normalMode = '08:00', то testMode должен быть '20:00' и наоборот
+        if ($normalMode === '08:00') {
+            $this->assertSame('20:00', $testMode);
+        } else {
+            $this->assertSame('20:00', $normalMode);
+            $this->assertSame('08:00', $testMode);
+        }
+    }
+
+    /**
+     * Тест: конструктор использует testMode для расчёта интервала
+     */
+    public function testConstructorUsesTestModeForInterval(): void
+    {
+        $normalIssue = new OrderIssue(OrderIssue::TYPE_HUNG_IN_DELIVERY, 1, 'TEST-1', false);
+        $testIssue = new OrderIssue(OrderIssue::TYPE_HUNG_IN_DELIVERY, 2, 'TEST-2', true);
+
+        // Интервалы должны быть противоположными
+        $this->assertNotSame($normalIssue->interval, $testIssue->interval);
+
+        // Интервалы должны соответствовать calculateInterval
+        $this->assertSame(OrderIssue::calculateInterval(false), $normalIssue->interval);
+        $this->assertSame(OrderIssue::calculateInterval(true), $testIssue->interval);
+    }
+
+    /**
+     * Тест: fromOrderData передаёт testMode в конструктор
+     */
+    public function testFromOrderDataPassesTestModeToConstructor(): void
+    {
+        $orderData = [
+            'id' => 100,
+            'marketplace_order_id' => 'FW-100',
+        ];
+
+        $normalIssue = OrderIssue::fromOrderData(OrderIssue::TYPE_HUNG_IN_DELIVERY, $orderData, false);
+        $testIssue = OrderIssue::fromOrderData(OrderIssue::TYPE_HUNG_IN_DELIVERY, $orderData, true);
+
+        // Интервалы должны быть противоположными
+        $this->assertNotSame($normalIssue->interval, $testIssue->interval);
+
+        // Интервалы должны соответствовать calculateInterval
+        $this->assertSame(OrderIssue::calculateInterval(false), $normalIssue->interval);
+        $this->assertSame(OrderIssue::calculateInterval(true), $testIssue->interval);
+    }
 }