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
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace yii_app\jobs;
+
+use Yii;
+use yii\base\BaseObject;
+use yii\queue\JobInterface;
+use yii\queue\RetryableJobInterface;
+use Symfony\Component\Mailer\Mailer;
+use Symfony\Component\Mailer\Transport;
+use Symfony\Component\Mime\Email;
+
+/**
+ * Job для неблокирующей отправки отчёта контроля заказов по Email
+ *
+ * Использует RetryableJobInterface для автоматических повторных попыток
+ * через механизм очереди вместо блокирующего sleep().
+ *
+ * @see \yii_app\services\OrderControlReportService
+ */
+class SendOrderControlEmailJob extends BaseObject implements JobInterface, RetryableJobInterface
+{
+ /**
+ * Максимальное количество попыток
+ */
+ private const MAX_ATTEMPTS = 5;
+
+ /**
+ * Время жизни Job в очереди (5 минут)
+ */
+ private const TTR = 300;
+
+ /**
+ * Тема письма
+ */
+ public string $subject = '';
+
+ /**
+ * HTML-содержимое письма
+ */
+ public string $htmlBody = '';
+
+ /**
+ * Текстовое содержимое письма
+ */
+ public string $textBody = '';
+
+ /**
+ * Список получателей (email адреса)
+ *
+ * @var string[]
+ */
+ public array $recipients = [];
+
+ /**
+ * Адрес отправителя
+ */
+ public string $fromEmail = '';
+
+ /**
+ * Имя отправителя
+ */
+ public string $fromName = 'ERP24 Отчёты';
+
+ /**
+ * DSN для Symfony Mailer
+ */
+ public string $mailerDsn = '';
+
+ /**
+ * Идентификатор отчёта (для логирования)
+ */
+ public string $reportId = '';
+
+ /**
+ * Выполняет отправку email
+ *
+ * @param \yii\queue\Queue $queue
+ * @return void
+ * @throws \RuntimeException При ошибке отправки (для retry)
+ */
+ public function execute($queue): void
+ {
+ if (empty($this->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;
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace yii_app\jobs;
+
+use Yii;
+use yii\base\BaseObject;
+use yii\queue\JobInterface;
+use yii\queue\RetryableJobInterface;
+
+/**
+ * Job для неблокирующей отправки отчёта контроля заказов в Telegram
+ *
+ * Использует RetryableJobInterface для автоматических повторных попыток
+ * через механизм очереди вместо блокирующего sleep().
+ *
+ * @see \yii_app\services\OrderControlReportService
+ */
+class SendOrderControlTelegramJob extends BaseObject implements JobInterface, RetryableJobInterface
+{
+ /**
+ * Максимальное количество попыток
+ */
+ private const MAX_ATTEMPTS = 5;
+
+ /**
+ * Время жизни Job в очереди (5 минут)
+ */
+ private const TTR = 300;
+
+ /**
+ * Текст сообщения для отправки (MarkdownV2)
+ */
+ public string $message = '';
+
+ /**
+ * ID чата Telegram
+ */
+ public string $chatId = '';
+
+ /**
+ * Токен бота Telegram
+ */
+ public string $botToken = '';
+
+ /**
+ * Индекс чанка (для многочастных сообщений)
+ */
+ public int $chunkIndex = 0;
+
+ /**
+ * Общее количество чанков
+ */
+ public int $totalChunks = 1;
+
+ /**
+ * Идентификатор отчёта (для логирования)
+ */
+ public string $reportId = '';
+
+ /**
+ * Выполняет отправку сообщения в Telegram
+ *
+ * @param \yii\queue\Queue $queue
+ * @return void
+ * @throws \RuntimeException При ошибке отправки (для retry)
+ */
+ public function execute($queue): void
+ {
+ if (empty($this->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;
+ }
+}
use Yii;
use yii\helpers\Json;
use yii\queue\JobInterface;
+use yii\queue\RetryableJobInterface;
use yii_app\records\MarketplaceOrders;
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
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;
}
: 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;
}
'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');
$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');
+ }
+ }
}
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;
/**
* Сервис контроля статусов заказов маркетплейса
*/
private bool $testMode = false;
+ /**
+ * Режим асинхронной отправки: использовать Job-очередь вместо блокирующего sleep()
+ */
+ private bool $asyncMode = false;
+
+ /**
+ * Уникальный идентификатор отчёта (для логирования и отслеживания Job-ов)
+ */
+ private string $reportId;
+
/**
* Кеш ID статусов 1С (ленивая загрузка)
*/
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;
}
/**
/**
* Отправляет сообщение в Telegram с retry-логикой
*
+ * В синхронном режиме (по умолчанию) использует блокирующий retry с sleep().
+ * В асинхронном режиме (setAsyncMode(true)) ставит Job-ы в очередь.
+ *
* @param string $message Текст сообщения (MarkdownV2)
- * @return bool Успешность отправки
+ * @return bool Успешность отправки (в async режиме всегда true, если Job поставлен)
*/
public function sendToTelegram(string $message): bool
{
}
$chunks = $this->splitTelegramMessage($message);
+
+ // Асинхронный режим: ставим Job-ы в очередь
+ if ($this->asyncMode) {
+ return $this->sendToTelegramAsync($chatId, $chunks);
+ }
+
+ // Синхронный режим: отправляем с блокирующим retry
$allSent = true;
foreach ($chunks as $index => $chunk) {
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
*
/**
* Отправляет отчёт на email с retry-логикой
*
+ * В синхронном режиме (по умолчанию) использует блокирующий retry с sleep().
+ * В асинхронном режиме (setAsyncMode(true)) ставит Job в очередь.
+ *
* @param string $html HTML-контент письма
- * @return bool Успешность отправки
+ * @return bool Успешность отправки (в async режиме всегда true, если Job поставлен)
*/
public function sendToEmail(string $html): bool
{
return false;
}
+ // Асинхронный режим: ставим Job в очередь
+ if ($this->asyncMode) {
+ return $this->sendToEmailAsync($html, $validRecipients);
+ }
+
// Диагностика конфигурации mailer
$this->logMailerDiagnostics($validRecipients);
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
*
'unknown' => 'Неизвестно',
];
+ /**
+ * Допустимые типы проблем
+ */
+ private const VALID_PROBLEM_TYPES = [
+ self::TYPE_HUNG_IN_DELIVERY,
+ self::TYPE_SUCCESS_NO_CHECK,
+ self::TYPE_CANCEL_NO_PROCESS,
+ ];
+
/**
* Конструктор с параметрами
*
* @param int $orderId ID заказа
* @param string $orderNumber Номер заказа
* @param bool $testMode Тестовый режим (анализ текущей смены вместо предыдущей)
+ * @throws \InvalidArgumentException Если передан недопустимый тип проблемы
*/
public function __construct(
string $problemType,
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;
$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);
+ }
+
/**
* Определяет интервал проверки (Дневная/Ночная смена)
*
{
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');
}
/**
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);
}
}
/**
- * Тест: сервис создаётся без ошибок
+ * Тест: сервис создаётся без ошибок (без параметров — обратная совместимость)
*/
public function testServiceCanBeInstantiated(): void
{
$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());
+ }
}