# 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
<?php
+use yii\queue\amqp_interop\Queue;
+
return [
'language' => 'ru',
'viewPath' => dirname(__DIR__) . '/views',
// ВАЖНО для 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,
<?php
try {
- $dotenv = Dotenv\Dotenv::createImmutable(__DIR__ . '/../');
+ $dotenv = Dotenv\Dotenv::createUnsafeImmutable(__DIR__ . '/../../');
$dotenv->load();
- $dotenv->required(['APP_ENV']);
+ $dotenv->required([
+ 'APP_ENV',
+ 'POSTGRES_PASSWORD',
+ 'RABBIT_USER',
+ 'RABBIT_PASSWORD',
+ 'TELEGRAM_BOT_TOKEN',
+ 'COOKIE_VALIDATION_KEY',
+ ]);
- foreach ($_ENV as $key => $value) {
- putenv("$key=$value");
- }
+// foreach ($_ENV as $key => $value) {
+// putenv("$key=$value");
+// }
} catch (\Dotenv\Exception\InvalidPathException $e) {
putenv("APP_ENV=development");
- Yii::error('Файл .env не найден: ' . $e->getMessage());
+ error_log('[ENV ERROR] Файл .env не найден: ' . $e->getMessage());
}
$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;
$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);
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;
$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');
}
}
}
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;
/**
* Метки типов проблем
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']],
->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');
}
}
->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');
}
}
->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));
}
->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');
}
}
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));
}
$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) {
*/
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'];
// 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. Отправляем уведомления только если есть проблемы
$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)]);
{
$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) {
// Проверяем, не существует ли уже такая запись
* Получает заказы типа "Успех без чека"
*
* Критерий: МП статус = "Выполнен" (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() — логика создания чека
*
? implode(', ', $rmkSuccessPlaceholders)
: '0'; // fallback если статусов нет
- // Выбираем заказы с МП-статусом "Выполнен", где:
- // 1. РМК-статус НЕ "Успех" (1С не знает о доставке)
- // 2. Причина: seller_id пустой или нулевой GUID, ИЛИ чек не создан в create_checks
+ // Выбираем заказы с МП-статусом "Выполнен", где РМК-статус НЕ "Успех"
+ // Диагностика причины добавляется для информационных целей
$sql = "
SELECT
mo.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,
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
most.code = :delivered
OR mosub.code = :delivery_service_delivered
)
- -- Ð Ð\9cÐ\9a-Ñ\81Ñ\82аÑ\82Ñ\83Ñ\81 Ð\9dÐ\95 УÑ\81пеÑ\85 (1С не знаеÑ\82 о доÑ\81Ñ\82авке)
+ -- Ð Ð\9cÐ\9a-Ñ\81Ñ\82аÑ\82Ñ\83Ñ\81 Ð\9dÐ\95 УÑ\81пеÑ\85 (1С не подÑ\82веÑ\80дила доÑ\81Ñ\82авкÑ\83)
AND (
mo.status_processing_1c IS NULL
OR mo.status_processing_1c::integer NOT IN ({$rmkSuccessInClause})
)
- -- Причина: seller_id пустой ИЛИ чек не создан
- AND (
- mo.seller_id IS NULL
- OR mo.seller_id = ''
- OR mo.seller_id = :empty_seller_guid
- OR cc.id IS NULL
- )
ORDER BY cs.name ASC, mo.creation_date DESC
";
$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;
$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)]);
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;
- }
-
/**
* Сохраняет состояние проблемных заказов в БД
*
{
$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) {
// Проверяем, не существует ли уже такая запись
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()
$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);
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);
- }
-
/**
* Форматирует строку таблицы для проблемы
*
private function getShortInterval(?string $interval): string
{
if ($interval === null) {
- return (int)date('H') < 12 ? '08:00' : '20:00';
+ return OrderIssue::calculateInterval($this->testMode);
}
// Убираем суффиксы типа " (Ночь)" или " (День)"
foreach ($result->cancelNoProcess as $issue) {
$allIssues[] = ['type' => 'Отмена без обработки', 'issue' => $issue];
}
- foreach ($result->successMissingData as $issue) {
- $allIssues[] = ['type' => 'Успех без данных', 'issue' => $issue];
- }
// Сортируем по типу
usort($allIssues, function ($a, $b) {
<th>Интервал</th>
<th>Заказ</th>
<th>РМК</th>
- <th>МП / Причина</th>
+ <th>МП</th>
</tr>';
foreach ($allIssues as $item) {
/** @var OrderIssue $issue */
$issue = $item['issue'];
$date = $issue->reportDate ?: date('d.m.Y');
- $interval = $issue->interval ?: ((int)date('H') < 12 ? '08:00' : '20:00');
-
- // Для "Успех без данных" показываем причину вместо МП-статуса
- if ($issue->problemType === OrderIssue::TYPE_SUCCESS_MISSING_DATA) {
- $mpOrReason = $issue->getIssueReasonLabel() ?? 'Неизвестно';
- } else {
- $mpOrReason = $issue->mpStatus ?? '-';
- }
+ $interval = $issue->interval ?: OrderIssue::calculateInterval($this->testMode);
+ $mpStatus = $issue->mpStatus ?? '-';
$html .= '
<tr>
<td>' . $this->escapeHtml($interval) . '</td>
<td>' . $this->escapeHtml($issue->orderNumber) . '</td>
<td>' . $this->escapeHtml($issue->rmkStatus ?? '-') . '</td>
- <td>' . $this->escapeHtml($mpOrReason) . '</td>
+ <td>' . $this->escapeHtml($mpStatus) . '</td>
</tr>';
}
{
$now = new \DateTime('now', new \DateTimeZone(self::TIMEZONE));
$currentHour = (int)$now->format('H');
+ $isDayTime = $currentHour >= 8 && $currentHour < 20;
$shiftStart = clone $now;
$shiftEnd = clone $now;
$shiftName = '';
- if ($currentHour >= 8 && $currentHour < 20) {
- // Ð\94невнаÑ\8f Ñ\81мена: 08:00 - 20:00
- $shiftStart->setTime(8, 0, 0);
- if ($testMode) {
- // Тестовый режим: endDate = текущее время
+ if ($testMode) {
+ // ТеÑ\81Ñ\82овÑ\8bй Ñ\80ежим: анализиÑ\80Ñ\83ем ТÐ\95Ð\9aУЩУЮ Ñ\81менÑ\83, endDate = Ñ\82екÑ\83Ñ\89ее вÑ\80емÑ\8f
+ if ($isDayTime) {
+ // Текущая дневная смена: 08:00 сегодня - текущее время
+ $shiftStart->setTime(8, 0, 0);
// $shiftEnd уже = $now
- } else {
- // Стандартный режим: endDate = 20:00 сегодня
- $shiftEnd->setTime(20, 0, 0);
- }
- $shiftName = 'Дневная смена (08:00-20:00)';
- } elseif ($currentHour >= 20) {
- // Ночная смена (начало): 20:00 сегодня - 08:00 завтра
- $shiftStart->setTime(20, 0, 0);
- if ($testMode) {
- // Тестовый режим: endDate = текущее время
+ $shiftName = 'Дневная смена (08:00-20:00)';
+ } elseif ($currentHour >= 20) {
+ // Текущая ночная смена (начало): 20:00 сегодня - текущее время
+ $shiftStart->setTime(20, 0, 0);
// $shiftEnd уже = $now
+ $shiftName = 'Ночная смена (20:00-08:00)';
} else {
- // Стандартный режим: endDate = 08:00 завтра
- $shiftEnd->modify('+1 day');
- $shiftEnd->setTime(8, 0, 0);
+ // Текущая ночная смена (продолжение): 20:00 вчера - текущее время
+ $shiftStart->modify('-1 day');
+ $shiftStart->setTime(20, 0, 0);
+ // $shiftEnd уже = $now
+ $shiftName = 'Ночная смена (20:00-08:00)';
}
- $shiftName = 'Ночная смена (20:00-08:00)';
} else {
- // Ночная смена (продолжение): 20:00 вчера - 08:00 сегодня
- $shiftStart->modify('-1 day');
- $shiftStart->setTime(20, 0, 0);
- if ($testMode) {
- // Тестовый режим: endDate = текущее время
- // $shiftEnd уже = $now
- } else {
- // Стандартный режим: endDate = 08:00 сегодня
+ // Обычный режим: анализируем ПРЕДЫДУЩУЮ смену
+ if ($isDayTime) {
+ // Сейчас день → анализируем предыдущую НОЧНУЮ смену (20:00 вчера - 08:00 сегодня)
+ $shiftStart->modify('-1 day');
+ $shiftStart->setTime(20, 0, 0);
$shiftEnd->setTime(8, 0, 0);
+ $shiftName = 'Ночная смена (20:00-08:00)';
+ } elseif ($currentHour >= 20) {
+ // Сейчас ночь (20:00-23:59) → анализируем предыдущую ДНЕВНУЮ смену (08:00-20:00 сегодня)
+ $shiftStart->setTime(8, 0, 0);
+ $shiftEnd->setTime(20, 0, 0);
+ $shiftName = 'Дневная смена (08:00-20:00)';
+ } else {
+ // Сейчас ночь (00:00-07:59) → анализируем предыдущую ДНЕВНУЮ смену (08:00-20:00 вчера)
+ $shiftStart->modify('-1 day');
+ $shiftStart->setTime(8, 0, 0);
+ $shiftEnd->modify('-1 day');
+ $shiftEnd->setTime(20, 0, 0);
+ $shiftName = 'Дневная смена (08:00-20:00)';
}
- $shiftName = 'Ночная смена (20:00-08:00)';
}
$this->logInfo('Вычислен диапазон дат на основе смены', [
$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;
*/
public array $cancelNoProcess = [];
- /**
- * Заказы типа "Успех без данных" (успех в МП и 1С, но нет seller_id и/или check_guid)
- *
- * @var OrderIssue[]
- */
- public array $successMissingData = [];
-
/**
* Успешность отправки в Telegram
*/
/**
* Конструктор
+ *
+ * @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);
}
/**
return count($this->cancelNoProcess);
}
- /**
- * Получает количество заказов по типу "Успех без данных"
- *
- * @return int
- */
- public function getSuccessMissingDataCount(): int
- {
- return count($this->successMissingData);
- }
-
/**
* Рассчитывает и обновляет общее количество проблем
*
{
$this->totalIssues = $this->getHungInDeliveryCount()
+ $this->getSuccessNoCheckCount()
- + $this->getCancelNoProcessCount()
- + $this->getSuccessMissingDataCount();
+ + $this->getCancelNoProcessCount();
return $this->totalIssues;
}
return $this->totalIssues > 0
|| !empty($this->hungInDelivery)
|| !empty($this->successNoCheck)
- || !empty($this->cancelNoProcess)
- || !empty($this->successMissingData);
+ || !empty($this->cancelNoProcess);
}
/**
return array_merge(
$this->hungInDelivery,
$this->successNoCheck,
- $this->cancelNoProcess,
- $this->successMissingData
+ $this->cancelNoProcess
);
}
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,
];
}
'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,
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'
);
*/
public const TYPE_CANCEL_NO_PROCESS = 'cancel_no_process';
- /**
- * Тип проблемы: "Успех без данных" (успех в МП и 1С, но нет seller_id и/или check_guid)
- */
- public const TYPE_SUCCESS_MISSING_DATA = 'success_missing_data';
-
/**
* Метки типов проблем для отображения
*/
self::TYPE_HUNG_IN_DELIVERY => 'Завис в доставке',
self::TYPE_SUCCESS_NO_CHECK => 'Успех без чека',
self::TYPE_CANCEL_NO_PROCESS => 'Отмена без обработки',
- self::TYPE_SUCCESS_MISSING_DATA => 'Успех без данных',
];
/**
* @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';
}
/**
*
* @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;
$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);
}
/**
$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']);
}
/**
MarketplaceOrderDailyIssues::TYPE_CANCEL_NO_PROCESS,
'Отмена без обработки',
],
- 'success_missing_data' => [
- MarketplaceOrderDailyIssues::TYPE_SUCCESS_MISSING_DATA,
- 'Успех без данных',
- ],
];
}
*/
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);
$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);
$this->assertStringContainsString('Завис в доставке', $message);
$this->assertStringContainsString('Успех без чека', $message);
$this->assertStringContainsString('Отмена без обработки', $message);
- $this->assertStringContainsString('Успех без данных', $message);
}
/**
}
/**
- * Тест: 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);
}
/**
$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);
$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 рассчитывает и возвращает общее количество
*/
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);
}
/**
'hungInDelivery' => ['hungInDelivery'],
'successNoCheck' => ['successNoCheck'],
'cancelNoProcess' => ['cancelNoProcess'],
- 'successMissingData' => ['successMissingData'],
];
}
$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]);
}
/**
$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]);
}
/**
$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;
$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);
$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']);
];
$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;
$summary = $result->getSummary();
$this->assertSame(
- 'Контроль МП: 4 проблем (завис: 2, успех без чека: 1, отмена: 0, успех без данных: 1). TG: OK, Email: FAIL',
+ 'Контроль МП: 3 проблем (завис: 2, успех без чека: 1, отмена: 0). TG: OK, Email: FAIL',
$summary
);
}
$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);
+ }
}
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) {
'total' => 3200.00,
],
],
- 'success_missing_data' => [
- OrderIssue::TYPE_SUCCESS_MISSING_DATA,
+ 'cancel_no_process' => [
+ OrderIssue::TYPE_CANCEL_NO_PROCESS,
[
'id' => 300,
'marketplace_order_id' => 'FW-300',
- 'rmk_status' => '6. Успех',
- 'rmk_status_id' => '6',
- 'mp_status_name' => 'Ð\94оÑ\81Ñ\82авлен',
- 'mp_status_code' => 'DELIVERED',
+ 'rmk_status' => 'Передан курьеру',
+ 'rmk_status_id' => '5',
+ 'mp_status_name' => 'Ð\9eÑ\82менÑ\91н',
+ 'mp_status_code' => 'CANCELLED',
'store_id' => 30,
'store_name' => 'Магазин Юг',
'marketplace_name' => 'Flowwow',
'marketplace_id' => 1,
'total' => 6800.00,
- 'seller_id' => '',
- 'check_guid' => null,
- 'issue_reason' => 'no_seller_and_check_guid',
],
[
'orderId' => 300,
'orderNumber' => 'FW-300',
- 'rmkStatus' => '6. Успех',
- 'mpStatus' => 'Ð\94оÑ\81Ñ\82авлен',
+ 'rmkStatus' => 'Передан курьеру',
+ 'mpStatus' => 'Ð\9eÑ\82менÑ\91н',
'storeId' => 30,
'storeName' => 'Магазин Юг',
'total' => 6800.00,
*/
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';
$issue->sellerId = '';
$issue->checkGuid = null;
$issue->checkExists = false;
- $issue->issueReason = 'no_seller_and_check_guid';
+ $issue->issueReason = 'no_seller_id';
$array = $issue->toArray();
$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']);
}
/**
$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);
+ }
}