From: Vladimir Fomichev Date: Mon, 26 Jan 2026 14:30:37 +0000 (+0300) Subject: Правки по ревью X-Git-Url: https://gitweb.erp-flowers.ru/?a=commitdiff_plain;h=2c17df83bf198d782943e98055fa727349d1022d;p=erp24_rep%2Fyii-erp24%2F.git Правки по ревью --- diff --git a/erp24/.env.example b/erp24/.env.example index 804571fb..7051d8f1 100644 --- a/erp24/.env.example +++ b/erp24/.env.example @@ -22,6 +22,14 @@ APP_ENV=development SERVER_NAME=local-dev +# === DEBUG & GII === +# Разрешённые IP для Yii debug toolbar и Gii (через запятую) +# По умолчанию (если не задано): только localhost (127.0.0.1, ::1) +# Пример для разрешения всех IP: DEBUG_ALLOWED_IPS=* +# Пример для конкретных IP: DEBUG_ALLOWED_IPS=192.168.1.100,10.0.0.5 +# ВАЖНО: НЕ используйте '*' в production! +DEBUG_ALLOWED_IPS=127.0.0.1,::1 + # === SMTP Configuration === # Для отправки email из консольных команд (отчёты, уведомления) # Для Яндекса с SSL на порту 465 используйте scheme=smtps diff --git a/erp24/jobs/SendOrderControlEmailJob.php b/erp24/jobs/SendOrderControlEmailJob.php new file mode 100644 index 00000000..7b38c681 --- /dev/null +++ b/erp24/jobs/SendOrderControlEmailJob.php @@ -0,0 +1,158 @@ +recipients)) { + Yii::warning("SendOrderControlEmailJob: нет получателей, report={$this->reportId}", 'email'); + return; + } + + if (empty($this->mailerDsn)) { + Yii::error("SendOrderControlEmailJob: mailerDsn не настроен", 'email'); + return; // Не повторяем — ошибка конфигурации + } + + try { + $transport = Transport::fromDsn($this->mailerDsn); + $mailer = new Mailer($transport); + + $email = (new Email()) + ->from("{$this->fromName} <{$this->fromEmail}>") + ->subject($this->subject) + ->html($this->htmlBody); + + if (!empty($this->textBody)) { + $email->text($this->textBody); + } + + // Добавляем получателей + foreach ($this->recipients as $recipient) { + $email->addTo($recipient); + } + + $mailer->send($email); + + Yii::info("SendOrderControlEmailJob: email отправлен на " . count($this->recipients) . " адресов, report={$this->reportId}", 'email'); + } catch (\Exception $e) { + Yii::error("SendOrderControlEmailJob: ошибка отправки - {$e->getMessage()}", 'email'); + throw new \RuntimeException("Email send error: {$e->getMessage()}", 0, $e); + } + } + + /** + * Время жизни Job в очереди + * + * @return int Время в секундах + */ + public function getTtr(): int + { + return self::TTR; + } + + /** + * Определяет, можно ли повторить выполнение Job + * + * @param int $attempt Номер текущей попытки + * @param \Throwable $error Ошибка + * @return bool + */ + public function canRetry($attempt, $error): bool + { + if ($attempt >= self::MAX_ATTEMPTS) { + Yii::error("SendOrderControlEmailJob: превышено количество попыток ({$attempt}), report={$this->reportId}", 'email'); + return false; + } + + $errorMessage = $error->getMessage(); + + // Не повторяем при ошибках конфигурации + if (str_contains($errorMessage, 'не настроен') + || str_contains($errorMessage, 'Authentication failed') + || str_contains($errorMessage, 'Invalid address')) { + return false; + } + + return true; + } +} diff --git a/erp24/jobs/SendOrderControlTelegramJob.php b/erp24/jobs/SendOrderControlTelegramJob.php new file mode 100644 index 00000000..4c1e2392 --- /dev/null +++ b/erp24/jobs/SendOrderControlTelegramJob.php @@ -0,0 +1,167 @@ +botToken)) { + Yii::error("SendOrderControlTelegramJob: botToken не установлен", 'telegram'); + return; // Не повторяем — ошибка конфигурации + } + + if (empty($this->chatId)) { + Yii::error("SendOrderControlTelegramJob: chatId не установлен", 'telegram'); + return; // Не повторяем — ошибка конфигурации + } + + $url = "https://api.telegram.org/bot{$this->botToken}/sendMessage"; + + $ch = curl_init(); + curl_setopt_array($ch, [ + CURLOPT_URL => $url, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => [ + 'chat_id' => $this->chatId, + 'text' => $this->message, + 'parse_mode' => 'MarkdownV2', + 'disable_web_page_preview' => true, + ], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 30, + CURLOPT_SSL_VERIFYPEER => true, + ]); + + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlError = curl_error($ch); + curl_close($ch); + + if ($curlError) { + Yii::error("SendOrderControlTelegramJob: cURL error - {$curlError}", 'telegram'); + throw new \RuntimeException("cURL error: {$curlError}"); + } + + $data = json_decode($response, true); + + if ($httpCode === 200 && ($data['ok'] ?? false)) { + $chunkInfo = $this->totalChunks > 1 + ? " (чанк {$this->chunkIndex}/{$this->totalChunks})" + : ''; + Yii::info("SendOrderControlTelegramJob: сообщение отправлено{$chunkInfo}, report={$this->reportId}", 'telegram'); + return; + } + + // Обработка rate limit (429) + if ($httpCode === 429) { + $retryAfter = $data['parameters']['retry_after'] ?? 10; + Yii::warning("SendOrderControlTelegramJob: rate limit, retry_after={$retryAfter}s", 'telegram'); + throw new \RuntimeException("Rate limit: retry after {$retryAfter}s"); + } + + // Другие ошибки + $errorDescription = $data['description'] ?? 'Unknown error'; + Yii::error("SendOrderControlTelegramJob: HTTP {$httpCode}, error: {$errorDescription}", 'telegram'); + throw new \RuntimeException("Telegram API error: {$errorDescription}"); + } + + /** + * Время жизни Job в очереди + * + * @return int Время в секундах + */ + public function getTtr(): int + { + return self::TTR; + } + + /** + * Определяет, можно ли повторить выполнение Job + * + * @param int $attempt Номер текущей попытки + * @param \Throwable $error Ошибка + * @return bool + */ + public function canRetry($attempt, $error ): bool + { + if ($attempt >= self::MAX_ATTEMPTS) { + Yii::error("SendOrderControlTelegramJob: превышено количество попыток ({$attempt}), report={$this->reportId}", 'telegram'); + return false; + } + + $errorMessage = $error->getMessage(); + + // Не повторяем при ошибках конфигурации + if (str_contains($errorMessage, 'не установлен') + || str_contains($errorMessage, 'Invalid token') + || str_contains($errorMessage, 'chat not found') + || str_contains($errorMessage, 'bot was blocked')) { + return false; + } + + return true; + } +} diff --git a/erp24/jobs/SendTelegramTestMessageJob.php b/erp24/jobs/SendTelegramTestMessageJob.php index fb7fc407..841df4a0 100644 --- a/erp24/jobs/SendTelegramTestMessageJob.php +++ b/erp24/jobs/SendTelegramTestMessageJob.php @@ -8,6 +8,7 @@ use GuzzleHttp\Client; use Yii; use yii\helpers\Json; use yii\queue\JobInterface; +use yii\queue\RetryableJobInterface; use yii_app\records\MarketplaceOrders; use yii_app\services\TelegramService; @@ -16,34 +17,61 @@ use yii_app\services\TelegramService; * * Используется для уведомлений о заказах с Яндекс.Маркета и Flowwow. * Отправляет сообщения в канал из TELEGRAM_CHAT_CHANNEL_ID (.env) + * + * Реализует RetryableJobInterface для повторных попыток при сбоях. */ -class SendTelegramTestMessageJob extends \yii\base\BaseObject implements JobInterface +class SendTelegramTestMessageJob extends \yii\base\BaseObject implements JobInterface, RetryableJobInterface { - public $message; - public $guid; - - public function execute($queue) + /** + * Максимальное количество попыток выполнения Job + */ + private const MAX_ATTEMPTS = 5; + + /** + * Время жизни Job в очереди (в секундах) + */ + private const TTR = 300; + + /** + * Сообщение для отправки + */ + public string $message = ''; + + /** + * GUID заказа + */ + public string $guid = ''; + + /** + * Выполняет отправку сообщения в Telegram + * + * @param \yii\queue\Queue $queue + * @return void + */ + public function execute($queue): void { $message = $this->message; $guid = $this->guid; - // Находим заказ по guid со статусом "не отправлено" - $marketplaceOrder = MarketplaceOrders::find() - ->where([ + // Атомарно захватываем заказ через updateAll (защита от Race Condition) + $updatedCount = MarketplaceOrders::updateAll( + ['status_telegram' => MarketplaceOrders::STATUS_TELEGRAM_PREPARED_TO_SEND], + [ 'status_telegram' => MarketplaceOrders::STATUS_TELEGRAM_NOT_SENT, 'guid' => $guid - ]) - ->one(); + ] + ); - if (!$marketplaceOrder) { + if ($updatedCount === 0) { Yii::warning("SendTelegramTestMessageJob: заказ с guid={$guid} не найден или уже обработан", 'telegram'); return; } - // Меняем статус на "подготовлен к отправке" - $marketplaceOrder->status_telegram = MarketplaceOrders::STATUS_TELEGRAM_PREPARED_TO_SEND; - if (!$marketplaceOrder->save()) { - Yii::error("Ошибка сохранения статуса заказа: " . Json::encode($marketplaceOrder->getErrors()), 'telegram'); + // Получаем заказ для дальнейшей работы + $marketplaceOrder = MarketplaceOrders::findOne(['guid' => $guid]); + if (!$marketplaceOrder) { + Yii::error("SendTelegramTestMessageJob: заказ с guid={$guid} не найден после updateAll", 'telegram'); + return; } // Получаем токен бота из .env через TelegramService @@ -52,9 +80,7 @@ class SendTelegramTestMessageJob extends \yii\base\BaseObject implements JobInte 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(); + $this->markOrderError($marketplaceOrder, 'TELEGRAM_BOT_TOKEN не установлен'); return; } @@ -66,10 +92,9 @@ class SendTelegramTestMessageJob extends \yii\base\BaseObject implements JobInte : 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(); + $envVar = $isDev ? 'TELEGRAM_CHAT_CHANNEL_ID' : 'TELEGRAM_CHAT_CHANNEL_ERP_ID'; + Yii::error("SendTelegramTestMessageJob: {$envVar} не установлен в .env", 'telegram'); + $this->markOrderError($marketplaceOrder, "{$envVar} не установлен"); return; } @@ -85,15 +110,16 @@ class SendTelegramTestMessageJob extends \yii\base\BaseObject implements JobInte 'text' => $escapedMessage, 'parse_mode' => 'MarkdownV2', ], + 'timeout' => 30, ]); if ($response->getStatusCode() === 200) { $marketplaceOrder->status_telegram = MarketplaceOrders::STATUS_TELEGRAM_SENT; + $marketplaceOrder->telegram_error = null; 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'); + // Выбрасываем исключение для retry + throw new \RuntimeException("HTTP {$response->getStatusCode()}"); } } catch (\Exception $e) { Yii::error("Ошибка отправки сообщения в Telegram: " . $e->getMessage(), 'telegram'); @@ -104,10 +130,65 @@ class SendTelegramTestMessageJob extends \yii\base\BaseObject implements JobInte $marketplaceOrder->telegram_error = mb_substr($errorMessage, 0, 255); $marketplaceOrder->status_telegram = MarketplaceOrders::STATUS_TELEGRAM_ERROR; + + // Перебрасываем исключение для механизма retry + if (!$marketplaceOrder->save()) { + Yii::error("Ошибка сохранения статуса заказа: " . Json::encode($marketplaceOrder->getErrors()), 'telegram'); + } + throw $e; } if (!$marketplaceOrder->save()) { Yii::error("Ошибка сохранения статуса заказа после отправки: " . Json::encode($marketplaceOrder->getErrors()), 'telegram'); } } + + /** + * Время жизни Job в очереди (Time To Reserve) + * + * @return int Время в секундах + */ + public function getTtr(): int + { + return self::TTR; + } + + /** + * Определяет, можно ли повторить выполнение Job при ошибке + * + * @param int $attempt Номер текущей попытки + * @param \Throwable $error Ошибка, из-за которой Job завершился + * @return bool + */ + public function canRetry($attempt, $error): bool + { + // Повторяем при сетевых ошибках и ошибках API + if ($attempt >= self::MAX_ATTEMPTS) { + Yii::warning("SendTelegramTestMessageJob: превышено количество попыток ({$attempt}) для guid={$this->guid}", 'telegram'); + return false; + } + + // Не повторяем при критических ошибках конфигурации + $errorMessage = $error->getMessage(); + if (str_contains($errorMessage, 'не установлен') || str_contains($errorMessage, 'Invalid token')) { + return false; + } + + return true; + } + + /** + * Помечает заказ как ошибочный + * + * @param MarketplaceOrders $order Заказ + * @param string $errorMessage Текст ошибки + */ + private function markOrderError(MarketplaceOrders $order, string $errorMessage): void + { + $order->status_telegram = MarketplaceOrders::STATUS_TELEGRAM_ERROR; + $order->telegram_error = mb_substr($errorMessage, 0, 255); + if (!$order->save()) { + Yii::error("Ошибка сохранения статуса заказа: " . Json::encode($order->getErrors()), 'telegram'); + } + } } diff --git a/erp24/services/OrderControlReportService.php b/erp24/services/OrderControlReportService.php index ee5de5bb..cffd2b38 100644 --- a/erp24/services/OrderControlReportService.php +++ b/erp24/services/OrderControlReportService.php @@ -10,6 +10,8 @@ use yii_app\records\MarketplaceOrderStatusTypes; use yii_app\records\MarketplaceOrderDailyIssues; use yii_app\services\dto\OrderIssue; use yii_app\services\dto\ControlReportResult; +use yii_app\jobs\SendOrderControlTelegramJob; +use yii_app\jobs\SendOrderControlEmailJob; /** * Сервис контроля статусов заказов маркетплейса @@ -59,6 +61,16 @@ class OrderControlReportService */ private bool $testMode = false; + /** + * Режим асинхронной отправки: использовать Job-очередь вместо блокирующего sleep() + */ + private bool $asyncMode = false; + + /** + * Уникальный идентификатор отчёта (для логирования и отслеживания Job-ов) + */ + private string $reportId; + /** * Кеш ID статусов 1С (ленивая загрузка) */ @@ -66,9 +78,51 @@ class OrderControlReportService private ?array $rmkStatusSuccess = null; private ?array $rmkStatusCancel = null; - public function __construct() + /** + * Создаёт экземпляр сервиса контроля заказов + * + * @param array|null $config Конфигурация отчёта. Если null, загружается из Yii::$app->params + * @param string|null $reportId Идентификатор отчёта. Если null, генерируется автоматически + */ + public function __construct(?array $config = null, ?string $reportId = null) + { + $this->config = $config ?? Yii::$app->params['MARKETPLACE_ORDER_CONTROL_REPORT'] ?? []; + $this->reportId = $reportId ?? uniqid('report_', true); + } + + /** + * Включает асинхронный режим отправки через Job-очередь + * + * В асинхронном режиме отправка Telegram и Email происходит через + * Job-очередь с автоматическими retry, без блокирования основного процесса. + * + * @param bool $enabled + * @return self + */ + public function setAsyncMode(bool $enabled): self + { + $this->asyncMode = $enabled; + return $this; + } + + /** + * Проверяет, включён ли асинхронный режим + * + * @return bool + */ + public function isAsyncMode(): bool { - $this->config = Yii::$app->params['MARKETPLACE_ORDER_CONTROL_REPORT'] ?? []; + return $this->asyncMode; + } + + /** + * Получает идентификатор текущего отчёта + * + * @return string + */ + public function getReportId(): string + { + return $this->reportId; } /** @@ -1059,8 +1113,11 @@ class OrderControlReportService /** * Отправляет сообщение в Telegram с retry-логикой * + * В синхронном режиме (по умолчанию) использует блокирующий retry с sleep(). + * В асинхронном режиме (setAsyncMode(true)) ставит Job-ы в очередь. + * * @param string $message Текст сообщения (MarkdownV2) - * @return bool Успешность отправки + * @return bool Успешность отправки (в async режиме всегда true, если Job поставлен) */ public function sendToTelegram(string $message): bool { @@ -1078,6 +1135,13 @@ class OrderControlReportService } $chunks = $this->splitTelegramMessage($message); + + // Асинхронный режим: ставим Job-ы в очередь + if ($this->asyncMode) { + return $this->sendToTelegramAsync($chatId, $chunks); + } + + // Синхронный режим: отправляем с блокирующим retry $allSent = true; foreach ($chunks as $index => $chunk) { @@ -1120,6 +1184,55 @@ class OrderControlReportService return $allSent; } + /** + * Отправляет сообщение в Telegram асинхронно через Job-очередь + * + * Каждый чанк сообщения ставится в очередь как отдельный Job с задержкой. + * Retry-логика обрабатывается механизмом очереди (RetryableJobInterface). + * + * @param string $chatId ID чата/канала + * @param array $chunks Части сообщения + * @return bool true если все Job-ы поставлены в очередь + */ + private function sendToTelegramAsync(string $chatId, array $chunks): bool + { + $botToken = $this->getTelegramBotToken(); + + if (empty($botToken)) { + $envVar = $this->isDevEnvironment() ? 'TELEGRAM_BOT_TOKEN' : 'TELEGRAM_BOT_TOKEN_PROD'; + $this->logError("Telegram bot token не установлен ({$envVar})"); + return false; + } + + $queue = Yii::$app->queue; + $totalChunks = count($chunks); + $delayBetweenChunks = 2; // Задержка в секундах между чанками + + foreach ($chunks as $index => $chunk) { + $job = new SendOrderControlTelegramJob([ + 'message' => $chunk, + 'chatId' => $chatId, + 'botToken' => $botToken, + 'chunkIndex' => $index + 1, + 'totalChunks' => $totalChunks, + 'reportId' => $this->reportId, + ]); + + // Первый чанк сразу, остальные с задержкой + $delay = $index * $delayBetweenChunks; + + if ($delay > 0) { + $queue->delay($delay)->push($job); + } else { + $queue->push($job); + } + + $this->logInfo("Telegram Job поставлен в очередь: чанк {$index}/{$totalChunks}, delay={$delay}s, report={$this->reportId}"); + } + + return true; + } + /** * Отправляет сообщение в Telegram * @@ -1185,8 +1298,11 @@ class OrderControlReportService /** * Отправляет отчёт на email с retry-логикой * + * В синхронном режиме (по умолчанию) использует блокирующий retry с sleep(). + * В асинхронном режиме (setAsyncMode(true)) ставит Job в очередь. + * * @param string $html HTML-контент письма - * @return bool Успешность отправки + * @return bool Успешность отправки (в async режиме всегда true, если Job поставлен) */ public function sendToEmail(string $html): bool { @@ -1213,6 +1329,11 @@ class OrderControlReportService return false; } + // Асинхронный режим: ставим Job в очередь + if ($this->asyncMode) { + return $this->sendToEmailAsync($html, $validRecipients); + } + // Диагностика конфигурации mailer $this->logMailerDiagnostics($validRecipients); @@ -1267,6 +1388,73 @@ class OrderControlReportService return $sent; } + /** + * Отправляет email асинхронно через Job-очередь + * + * Retry-логика обрабатывается механизмом очереди (RetryableJobInterface). + * + * @param string $html HTML-контент письма + * @param array $recipients Валидные email-адреса + * @return bool true если Job поставлен в очередь + */ + private function sendToEmailAsync(string $html, array $recipients): bool + { + $subject = $this->config['email_subject'] ?? 'Контроль статусов заказов МП'; + $fromEmail = getenv('MAIL_USERNAME') ?: 'flow@bazacvetov24.ru'; + + // Формируем DSN для Symfony Mailer + $mailerDsn = $this->buildMailerDsn(); + if (empty($mailerDsn)) { + $this->logError('Не удалось сформировать DSN для email'); + return false; + } + + $job = new SendOrderControlEmailJob([ + 'subject' => $subject, + 'htmlBody' => $html, + 'recipients' => $recipients, + 'fromEmail' => $fromEmail, + 'fromName' => 'ERP24 Контроль МП', + 'mailerDsn' => $mailerDsn, + 'reportId' => $this->reportId, + ]); + + Yii::$app->queue->push($job); + + $this->logInfo("Email Job поставлен в очередь: " . count($recipients) . " получателей, report={$this->reportId}"); + return true; + } + + /** + * Формирует DSN для Symfony Mailer из ENV-переменных + * + * @return string DSN строка или пустая строка при ошибке + */ + private function buildMailerDsn(): string + { + $scheme = getenv('MAIL_SCHEME') ?: 'smtp'; + $host = getenv('MAIL_HOST'); + $port = getenv('MAIL_PORT') ?: 587; + $username = getenv('MAIL_USERNAME'); + $password = getenv('MAIL_PASSWORD'); + + if (empty($host) || empty($username)) { + return ''; + } + + // Формат: smtp://user:pass@host:port + $dsn = sprintf( + '%s://%s:%s@%s:%d', + $scheme, + rawurlencode($username), + rawurlencode($password ?: ''), + $host, + (int)$port + ); + + return $dsn; + } + /** * Логирует диагностику конфигурации mailer * diff --git a/erp24/services/dto/OrderIssue.php b/erp24/services/dto/OrderIssue.php index 36000b5d..ffe72584 100644 --- a/erp24/services/dto/OrderIssue.php +++ b/erp24/services/dto/OrderIssue.php @@ -154,6 +154,15 @@ class OrderIssue 'unknown' => 'Неизвестно', ]; + /** + * Допустимые типы проблем + */ + private const VALID_PROBLEM_TYPES = [ + self::TYPE_HUNG_IN_DELIVERY, + self::TYPE_SUCCESS_NO_CHECK, + self::TYPE_CANCEL_NO_PROCESS, + ]; + /** * Конструктор с параметрами * @@ -161,6 +170,7 @@ class OrderIssue * @param int $orderId ID заказа * @param string $orderNumber Номер заказа * @param bool $testMode Тестовый режим (анализ текущей смены вместо предыдущей) + * @throws \InvalidArgumentException Если передан недопустимый тип проблемы */ public function __construct( string $problemType, @@ -168,6 +178,15 @@ class OrderIssue string $orderNumber, bool $testMode = false ) { + // Валидация типа проблемы + if (!in_array($problemType, self::VALID_PROBLEM_TYPES, true)) { + throw new \InvalidArgumentException(sprintf( + 'Недопустимый тип проблемы: "%s". Допустимые значения: %s', + $problemType, + implode(', ', self::VALID_PROBLEM_TYPES) + )); + } + $this->problemType = $problemType; $this->problemTypeLabel = self::TYPE_LABELS[$problemType] ?? $problemType; $this->orderId = $orderId; @@ -177,6 +196,17 @@ class OrderIssue $this->total = 0.0; } + /** + * Проверяет, является ли тип проблемы допустимым + * + * @param string $problemType Тип проблемы + * @return bool + */ + public static function isValidProblemType(string $problemType): bool + { + return in_array($problemType, self::VALID_PROBLEM_TYPES, true); + } + /** * Определяет интервал проверки (Дневная/Ночная смена) * diff --git a/erp24/tests/unit/services/OrderControlReportServiceTest.php b/erp24/tests/unit/services/OrderControlReportServiceTest.php index 1cb4c055..50146f94 100644 --- a/erp24/tests/unit/services/OrderControlReportServiceTest.php +++ b/erp24/tests/unit/services/OrderControlReportServiceTest.php @@ -27,10 +27,21 @@ class OrderControlReportServiceTest extends Unit { private OrderControlReportService $service; + /** + * Тестовая конфигурация (внедряется через конструктор) + */ + private array $testConfig = [ + 'max_retries' => 3, + 'retry_delay_seconds' => 5, + 'telegram_max_message_length' => 4000, + 'email_subject' => 'Test Report', + ]; + protected function _before(): void { parent::_before(); - $this->service = new OrderControlReportService(); + // Внедряем конфигурацию через конструктор (Dependency Injection) + $this->service = new OrderControlReportService($this->testConfig, 'test_report_123'); } /** @@ -39,8 +50,8 @@ class OrderControlReportServiceTest extends Unit public function testConstantsHaveCorrectValues(): void { $this->assertSame(12, OrderControlReportService::REPORT_PERIOD_HOURS); - $this->assertSame(3, OrderControlReportService::MAX_RETRIES); - $this->assertSame(5, OrderControlReportService::RETRY_DELAY_SECONDS); + $this->assertSame(5, OrderControlReportService::MAX_RETRIES); + $this->assertSame(10, OrderControlReportService::RETRY_DELAY_SECONDS); $this->assertSame(4000, OrderControlReportService::TELEGRAM_MAX_LENGTH); $this->assertSame('Europe/Moscow', OrderControlReportService::TIMEZONE); } @@ -360,7 +371,7 @@ class OrderControlReportServiceTest extends Unit } /** - * Тест: сервис создаётся без ошибок + * Тест: сервис создаётся без ошибок (без параметров — обратная совместимость) */ public function testServiceCanBeInstantiated(): void { @@ -368,4 +379,73 @@ class OrderControlReportServiceTest extends Unit $this->assertInstanceOf(OrderControlReportService::class, $service); } + + /** + * Тест: сервис создаётся с внедрённой конфигурацией (Dependency Injection) + */ + public function testServiceCanBeInstantiatedWithConfig(): void + { + $config = [ + 'max_retries' => 10, + 'email_subject' => 'Custom Subject', + ]; + $reportId = 'custom_report_456'; + + $service = new OrderControlReportService($config, $reportId); + + $this->assertInstanceOf(OrderControlReportService::class, $service); + $this->assertSame($reportId, $service->getReportId()); + } + + /** + * Тест: getReportId возвращает переданный идентификатор + */ + public function testGetReportIdReturnsInjectedValue(): void + { + $expectedReportId = 'test_report_123'; + $service = new OrderControlReportService([], $expectedReportId); + + $this->assertSame($expectedReportId, $service->getReportId()); + } + + /** + * Тест: getReportId генерирует идентификатор, если не передан + */ + public function testGetReportIdGeneratesValueIfNotProvided(): void + { + $service = new OrderControlReportService([]); + + $reportId = $service->getReportId(); + + $this->assertNotEmpty($reportId); + $this->assertStringStartsWith('report_', $reportId); + } + + /** + * Тест: setAsyncMode возвращает $this для fluent interface + */ + public function testSetAsyncModeReturnsThis(): void + { + $service = new OrderControlReportService(); + + $result = $service->setAsyncMode(true); + + $this->assertSame($service, $result); + } + + /** + * Тест: isAsyncMode отражает установленный режим + */ + public function testIsAsyncModeReflectsSetValue(): void + { + $service = new OrderControlReportService(); + + $this->assertFalse($service->isAsyncMode()); + + $service->setAsyncMode(true); + $this->assertTrue($service->isAsyncMode()); + + $service->setAsyncMode(false); + $this->assertFalse($service->isAsyncMode()); + } }