From: Vladimir Fomichev Date: Wed, 21 Jan 2026 14:06:56 +0000 (+0300) Subject: Убираем Успех без данных и правим ошибки X-Git-Url: https://gitweb.erp-flowers.ru/?a=commitdiff_plain;h=013b08a6f86310b478d02b4aff4a3b3fb94d9da6;p=erp24_rep%2Fyii-erp24%2F.git Убираем Успех без данных и правим ошибки --- diff --git a/erp24/.env.example b/erp24/.env.example index ba328de9..9f00cacd 100644 --- a/erp24/.env.example +++ b/erp24/.env.example @@ -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 diff --git a/erp24/api2/config/dev.api2.config.php b/erp24/api2/config/dev.api2.config.php index 8c99a738..0e024d1f 100644 --- a/erp24/api2/config/dev.api2.config.php +++ b/erp24/api2/config/dev.api2.config.php @@ -1,5 +1,7 @@ '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, diff --git a/erp24/api2/config/env.php b/erp24/api2/config/env.php index cd418591..19ba42f0 100644 --- a/erp24/api2/config/env.php +++ b/erp24/api2/config/env.php @@ -1,14 +1,21 @@ 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()); } diff --git a/erp24/api2/controllers/DataController.php b/erp24/api2/controllers/DataController.php index 5ceba167..05aee52c 100644 --- a/erp24/api2/controllers/DataController.php +++ b/erp24/api2/controllers/DataController.php @@ -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; diff --git a/erp24/commands/MarketplaceController.php b/erp24/commands/MarketplaceController.php index 90e9b9c8..bb4290a6 100644 --- a/erp24/commands/MarketplaceController.php +++ b/erp24/commands/MarketplaceController.php @@ -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); diff --git a/erp24/jobs/SendTelegramTestMessageJob.php b/erp24/jobs/SendTelegramTestMessageJob.php index 43be5c11..40be4a6c 100644 --- a/erp24/jobs/SendTelegramTestMessageJob.php +++ b/erp24/jobs/SendTelegramTestMessageJob.php @@ -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'); } } } diff --git a/erp24/records/MarketplaceOrderDailyIssues.php b/erp24/records/MarketplaceOrderDailyIssues.php index e6e6c818..65f0a0b8 100644 --- a/erp24/records/MarketplaceOrderDailyIssues.php +++ b/erp24/records/MarketplaceOrderDailyIssues.php @@ -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']], diff --git a/erp24/services/MarketplaceService.php b/erp24/services/MarketplaceService.php index 1a96c2b2..fd7d7be4 100644 --- a/erp24/services/MarketplaceService.php +++ b/erp24/services/MarketplaceService.php @@ -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) { diff --git a/erp24/services/OrderControlReportService.php b/erp24/services/OrderControlReportService.php index 9230b915..7c7415eb 100644 --- a/erp24/services/OrderControlReportService.php +++ b/erp24/services/OrderControlReportService.php @@ -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 ) - -- РМК-статус НЕ Успех (1С не знает о доставке) + -- РМК-статус НЕ Успех (1С не подтвердила доставку) 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 Интервал Заказ РМК - МП / Причина + МП '; 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 .= ' @@ -1155,7 +985,7 @@ class OrderControlReportService ' . $this->escapeHtml($interval) . ' ' . $this->escapeHtml($issue->orderNumber) . ' ' . $this->escapeHtml($issue->rmkStatus ?? '-') . ' - ' . $this->escapeHtml($mpOrReason) . ' + ' . $this->escapeHtml($mpStatus) . ' '; } @@ -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) { - // Дневная смена: 08:00 - 20:00 - $shiftStart->setTime(8, 0, 0); - if ($testMode) { - // Тестовый режим: endDate = текущее время + if ($testMode) { + // Тестовый режим: анализируем ТЕКУЩУЮ смену, endDate = текущее время + 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('Вычислен диапазон дат на основе смены', [ diff --git a/erp24/services/UploadService.php b/erp24/services/UploadService.php index 8b3a41e9..f85e1b3c 100644 --- a/erp24/services/UploadService.php +++ b/erp24/services/UploadService.php @@ -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; diff --git a/erp24/services/dto/ControlReportResult.php b/erp24/services/dto/ControlReportResult.php index bf50531a..389aeb1b 100644 --- a/erp24/services/dto/ControlReportResult.php +++ b/erp24/services/dto/ControlReportResult.php @@ -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' ); diff --git a/erp24/services/dto/OrderIssue.php b/erp24/services/dto/OrderIssue.php index 8d54138f..f74b9530 100644 --- a/erp24/services/dto/OrderIssue.php +++ b/erp24/services/dto/OrderIssue.php @@ -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; diff --git a/erp24/tests/unit/records/MarketplaceOrderDailyIssuesTest.php b/erp24/tests/unit/records/MarketplaceOrderDailyIssuesTest.php index 70d83868..c039d68a 100644 --- a/erp24/tests/unit/records/MarketplaceOrderDailyIssuesTest.php +++ b/erp24/tests/unit/records/MarketplaceOrderDailyIssuesTest.php @@ -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); diff --git a/erp24/tests/unit/services/OrderControlReportServiceTest.php b/erp24/tests/unit/services/OrderControlReportServiceTest.php index 305d13d8..1cb4c055 100644 --- a/erp24/tests/unit/services/OrderControlReportServiceTest.php +++ b/erp24/tests/unit/services/OrderControlReportServiceTest.php @@ -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); } /** diff --git a/erp24/tests/unit/services/dto/ControlReportResultTest.php b/erp24/tests/unit/services/dto/ControlReportResultTest.php index c96a5ddd..5afbc06a 100644 --- a/erp24/tests/unit/services/dto/ControlReportResultTest.php +++ b/erp24/tests/unit/services/dto/ControlReportResultTest.php @@ -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); + } } diff --git a/erp24/tests/unit/services/dto/OrderIssueTest.php b/erp24/tests/unit/services/dto/OrderIssueTest.php index 7262bc02..9a2c6dac 100644 --- a/erp24/tests/unit/services/dto/OrderIssueTest.php +++ b/erp24/tests/unit/services/dto/OrderIssueTest.php @@ -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' => 'Доставлен', - 'mp_status_code' => 'DELIVERED', + 'rmk_status' => 'Передан курьеру', + 'rmk_status_id' => '5', + 'mp_status_name' => 'Отменён', + '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' => 'Доставлен', + 'rmkStatus' => 'Передан курьеру', + 'mpStatus' => 'Отменён', '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); + } }