namespace yii_app\services;
use Yii;
+use yii_app\records\MarketplaceOrder1cStatuses;
use yii_app\records\MarketplaceOrderStatusTypes;
use yii_app\records\MarketplaceOrderDailyIssues;
use yii_app\services\dto\OrderIssue;
private const TELEGRAM_BOT_PROD = '5456741805:AAG7xOSiYDwUdV5NMb2v9vh8CWzEczDP4yU';
/**
- * СÑ\82аÑ\82Ñ\83Ñ\81Ñ\8b 1С длÑ\8f пÑ\80овеÑ\80ок
+ * Ð\9aонÑ\84игÑ\83Ñ\80аÑ\86иÑ\8f оÑ\82Ñ\87Ñ\91Ñ\82а
*/
- private const RMK_STATUS_COURIER = ['1004', '1011']; // "Передан курьеру"
- private const RMK_STATUS_SUCCESS = ['1005', '1012']; // "Успех"
- private const RMK_STATUS_CANCEL = ['1006', '1013']; // "Отказ"
+ private array $config;
/**
- * Ð\9aонÑ\84игÑ\83Ñ\80аÑ\86иÑ\8f оÑ\82Ñ\87Ñ\91Ñ\82а
+ * Ð\9aеÑ\88 ID Ñ\81Ñ\82аÑ\82Ñ\83Ñ\81ов 1С (лениваÑ\8f загÑ\80Ñ\83зка)
*/
- private array $config;
+ private ?array $rmkStatusCourier = null;
+ private ?array $rmkStatusSuccess = null;
+ private ?array $rmkStatusCancel = null;
public function __construct()
{
$this->config = Yii::$app->params['MARKETPLACE_ORDER_CONTROL_REPORT'] ?? [];
}
+ /**
+ * Получает ID статусов 1С "Передан курьеру" (с кешированием)
+ *
+ * @return int[]
+ */
+ private function getRmkStatusCourier(): array
+ {
+ if ($this->rmkStatusCourier === null) {
+ $this->rmkStatusCourier = MarketplaceOrder1cStatuses::getCourierOrderIds();
+ }
+ return $this->rmkStatusCourier;
+ }
+
+ /**
+ * Получает ID статусов 1С "Успех" (successful_order = 1) с кешированием
+ *
+ * @return int[]
+ */
+ private function getRmkStatusSuccess(): array
+ {
+ if ($this->rmkStatusSuccess === null) {
+ $this->rmkStatusSuccess = MarketplaceOrder1cStatuses::getSuccessfulOrderIds();
+ }
+ return $this->rmkStatusSuccess;
+ }
+
+ /**
+ * Получает ID статусов 1С "Отказ" (cancelled_order = 1) с кешированием
+ *
+ * @return int[]
+ */
+ private function getRmkStatusCancel(): array
+ {
+ if ($this->rmkStatusCancel === null) {
+ $this->rmkStatusCancel = MarketplaceOrder1cStatuses::getCancelledOrderIds();
+ }
+ return $this->rmkStatusCancel;
+ }
+
/**
* Генерирует отчёт контроля статусов заказов МП
*
* @param bool $onlyNew Отправлять только новые проблемы
* @return ControlReportResult
*/
- public function generateControlReport(int $hoursAgo = 24, bool $onlyNew = true): ControlReportResult
+ public function generateControlReport(int $hoursAgo = 12, bool $onlyNew = true): ControlReportResult
{
$result = new ControlReportResult();
{
$this->logInfo('Выборка кандидатов "Завис в доставке"', ['hours_ago' => $hoursAgo]);
- $startDate = new \DateTime('now', new \DateTimeZone(self::TIMEZONE));
- $startDate->modify("-{$hoursAgo} hours");
- $startDateStr = $startDate->format('Y-m-d H:i:s');
+ // Получаем диапазон дат на основе конца смены
+ $dateRange = $this->getShiftBasedDateRange($hoursAgo);
+ $startDateStr = $dateRange['startDate'];
+ $endDateStr = $dateRange['endDate'];
// Выбираем заказы с РМК-статусом "Передан курьеру", где МП-статус НЕ "Выполнен"
$sql = "
mocs.status as rmk_status,
most.code as mp_status_code,
mosub.code as mp_substatus_code,
- most.name as mp_status_name
+ COALESCE(most.name, mosub.name) as mp_status_name
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
WHERE mo.fake = 0
AND mo.status_processing_1c::integer IN (:rmk_1004, :rmk_1011)
AND mo.updated_at >= :start_date
+ AND mo.updated_at <= :end_date
AND (
most.code IS NULL
OR (most.code != :delivered AND mosub.code IS DISTINCT FROM :delivery_service_delivered)
':rmk_1004' => 1004,
':rmk_1011' => 1011,
':start_date' => $startDateStr,
+ ':end_date' => $endDateStr,
':delivered' => MarketplaceOrderStatusTypes::DELIVERED_CODE,
':delivery_service_delivered' => MarketplaceOrderStatusTypes::DELIVERY_SERVICE_DELIVERED_CODE,
])->queryAll();
'report_date' => $prevDate,
'interval' => $prevInterval,
])
- ->andWhere(['rmk_status_id' => self::RMK_STATUS_COURIER])
+ ->andWhere(['rmk_status_id' => $this->getRmkStatusCourier()])
->indexBy('order_id')
->asArray()
->all();
* Получает заказы типа "Успех без чека"
*
* Критерий: МП статус = "Выполнен" (DELIVERED или DELIVERY_SERVICE_DELIVERED)
- * + РМК статус НЕ "Успех" (НЕ 1005/1012)
+ * + (seller_id пустой/нулевой ИЛИ чек не создан в create_checks)
*
- * @param int $hoursAgo Период выборки в часах (по умолчанию 24)
+ * Причина проблемы:
+ * - Если seller_id пустой или '00000000-0000-0000-0000-000000000000' — чек не создаётся
+ * - Если чек не создан — 1С не получает сигнала о доставке заказа
+ *
+ * @see MarketplaceService::createCheckForMarketplaceOrder() — логика создания чека
+ *
+ * @param int $hoursAgo Период выборки в часах (по умолчанию 12)
* @return OrderIssue[]
*/
- public function getSuccessNoCheckOrders(int $hoursAgo = 24): array
+ public function getSuccessNoCheckOrders(int $hoursAgo = 12): array
{
$this->logInfo('Выборка заказов "Успех без чека"', ['hours_ago' => $hoursAgo]);
- $startDate = new \DateTime('now', new \DateTimeZone(self::TIMEZONE));
- $startDate->modify("-{$hoursAgo} hours");
- $startDateStr = $startDate->format('Y-m-d H:i:s');
+ // Получаем диапазон дат на основе конца смены
+ $dateRange = $this->getShiftBasedDateRange($hoursAgo);
+ $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 если статусов нет
+
+ // Выбираем заказы с МП-статусом "Выполнен", где:
+ // 1. РМК-статус НЕ "Успех" (1С не знает о доставке)
+ // 2. Причина: seller_id пустой или нулевой GUID, ИЛИ чек не создан в create_checks
$sql = "
SELECT
mo.id,
mo.marketplace_id,
mo.total,
mo.creation_date,
+ mo.seller_id,
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,
- most.name as mp_status_name
+ COALESCE(most.name, mosub.name) as mp_status_name,
+ cc.id as check_id,
+ CASE
+ WHEN cc.id IS NOT NULL THEN true
+ ELSE false
+ END as check_exists,
+ CASE
+ WHEN mo.seller_id IS NULL OR mo.seller_id = '' OR mo.seller_id = :empty_seller_guid
+ THEN 'no_seller_id'
+ WHEN cc.id IS NULL
+ THEN 'no_check'
+ 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
+ LEFT JOIN create_checks cc ON cc.marketplace_order_id = mo.marketplace_order_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 IS NULL
- OR mo.status_processing_1c::integer NOT IN (:rmk_1005, :rmk_1012)
+ 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
";
- $orders = Yii::$app->db->createCommand($sql, [
+ $params = array_merge([
':start_date' => $startDateStr,
+ ':end_date' => $endDateStr,
':delivered' => MarketplaceOrderStatusTypes::DELIVERED_CODE,
':delivery_service_delivered' => MarketplaceOrderStatusTypes::DELIVERY_SERVICE_DELIVERED_CODE,
- ':rmk_1005' => 1005,
- ':rmk_1012' => 1012,
- ])->queryAll();
+ ':empty_seller_guid' => $emptySellerGuid,
+ ], $rmkSuccessParams);
+
+ $orders = Yii::$app->db->createCommand($sql, $params)->queryAll();
$issues = [];
foreach ($orders as $orderData) {
$issues[] = OrderIssue::fromOrderData(OrderIssue::TYPE_SUCCESS_NO_CHECK, $orderData);
}
- $this->logInfo('Найдено "Успех без чека"', ['count' => count($issues)]);
+ $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')),
+ ]);
return $issues;
}
{
$this->logInfo('Выборка заказов "Отмена без обработки"', ['hours_ago' => $hoursAgo]);
- $startDate = new \DateTime('now', new \DateTimeZone(self::TIMEZONE));
- $startDate->modify("-{$hoursAgo} hours");
- $startDateStr = $startDate->format('Y-m-d H:i:s');
+ // Получаем диапазон дат на основе конца смены
+ $dateRange = $this->getShiftBasedDateRange($hoursAgo);
+ $startDateStr = $dateRange['startDate'];
+ $endDateStr = $dateRange['endDate'];
// Выбираем заказы с МП-статусом "Отменён", где РМК-статус НЕ "Отказ"
$sql = "
mocs.status as rmk_status,
most.code as mp_status_code,
mosub.code as mp_substatus_code,
- most.name as mp_status_name
+ COALESCE(most.name, mosub.name) as mp_status_name
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 mosub ON mosub.id = mo.substatus_id
WHERE mo.fake = 0
AND mo.updated_at >= :start_date
+ AND mo.updated_at <= :end_date
AND most.code = :cancelled
AND (
mo.status_processing_1c IS NULL
$orders = Yii::$app->db->createCommand($sql, [
':start_date' => $startDateStr,
+ ':end_date' => $endDateStr,
':cancelled' => MarketplaceOrderStatusTypes::CANSELLED_CODE,
':rmk_1006' => 1006,
':rmk_1013' => 1013,
/**
* Формирует текстовый отчёт контроля статусов для Telegram (MarkdownV2)
*
+ * Использует моноширинный блок для корректного отображения таблицы.
+ *
* @param ControlReportResult $result Результат отчёта
* @return string Текст сообщения
*/
public function formatTelegramControlReport(ControlReportResult $result): string
{
$lines = [];
- $lines[] = $this->escapeMarkdownV2('[Контроль MP]') . ' *Отчёт за ' . $this->escapeMarkdownV2($result->reportDate) . ' ' . $this->escapeMarkdownV2($result->interval) . '*';
+ $lines[] = '*\[Контроль MP\]* Отчёт за ' . $this->escapeMarkdownV2($result->reportDate) . ' ' . $this->escapeMarkdownV2($result->interval);
$lines[] = '';
// Секция "Завис в доставке"
if (!empty($result->hungInDelivery)) {
- $lines[] = '*' . $this->escapeMarkdownV2('Завис в доставке') . '*';
- $lines[] = $this->escapeMarkdownV2('| Дата | Интервал | Заказ | РМК | МП');
-
- foreach ($result->hungInDelivery as $issue) {
- $line = $this->formatIssueLineForTelegram($issue);
- $lines[] = $this->escapeMarkdownV2($line);
- }
+ $lines[] = '*Завис в доставке* \\(' . count($result->hungInDelivery) . '\\)';
+ $lines[] = $this->formatIssuesTable($result->hungInDelivery);
$lines[] = '';
}
// Секция "Успех без чека"
if (!empty($result->successNoCheck)) {
- $lines[] = '*' . $this->escapeMarkdownV2('Успех без чека') . '*';
- $lines[] = $this->escapeMarkdownV2('| Дата | Интервал | Заказ | РМК | МП');
-
- foreach ($result->successNoCheck as $issue) {
- $line = $this->formatIssueLineForTelegram($issue);
- $lines[] = $this->escapeMarkdownV2($line);
- }
+ $lines[] = '*Успех без чека* \\(' . count($result->successNoCheck) . '\\)';
+ $lines[] = $this->formatIssuesTable($result->successNoCheck);
$lines[] = '';
}
// Секция "Отмена без обработки"
if (!empty($result->cancelNoProcess)) {
- $lines[] = '*' . $this->escapeMarkdownV2('Отмена без обработки') . '*';
- $lines[] = $this->escapeMarkdownV2('| Дата | Интервал | Заказ | РМК | МП');
-
- foreach ($result->cancelNoProcess as $issue) {
- $line = $this->formatIssueLineForTelegram($issue);
- $lines[] = $this->escapeMarkdownV2($line);
- }
+ $lines[] = '*Отмена без обработки* \\(' . count($result->cancelNoProcess) . '\\)';
+ $lines[] = $this->formatIssuesTable($result->cancelNoProcess);
$lines[] = '';
}
+ $lines[] = '*Всего:* ' . $this->escapeMarkdownV2((string)$result->totalIssues);
+
return implode("\n", $lines);
}
/**
- * Форматирует строку проблемы для Telegram
+ * Форматирует таблицу проблем для Telegram (моноширинный блок)
+ *
+ * @param OrderIssue[] $issues
+ * @return string
+ */
+ private function formatIssuesTable(array $issues): string
+ {
+ $rows = [];
+ $rows[] = '```';
+
+ foreach ($issues as $issue) {
+ $rows[] = $this->formatIssueRow($issue);
+ }
+
+ $rows[] = '```';
+
+ return implode("\n", $rows);
+ }
+
+ /**
+ * Форматирует строку таблицы для проблемы
+ *
+ * Формат: Заказ | РМК | МП
*
* @param OrderIssue $issue
* @return string
*/
- private function formatIssueLineForTelegram(OrderIssue $issue): string
+ private function formatIssueRow(OrderIssue $issue): string
{
- $date = $issue->reportDate ?: date('d.m.Y');
- $interval = $issue->interval ?: ((int)date('H') < 12 ? '08:00' : '20:00');
$rmk = $issue->rmkStatus ?? '-';
$mp = $issue->mpStatus ?? '-';
return sprintf(
- '| %s | %s | %s | %s | %s',
- $date,
- $interval,
+ '%s | %s | %s',
$issue->orderNumber,
$rmk,
$mp
return $recipients;
}
+ /**
+ * Вычисляет диапазон дат на основе конца смены
+ *
+ * Логика:
+ * - Если текущий час < 20, база = 08:00 сегодня (конец утренней смены)
+ * - Если текущий час >= 20, база = 20:00 сегодня (конец вечерней смены)
+ * - startDate = база - $hoursAgo часов
+ * - endDate = база
+ *
+ * Пример:
+ * - 15:00, hoursAgo=12 → база 08:00 → диапазон: вчера 20:00 - сегодня 08:00
+ * - 22:00, hoursAgo=12 → база 20:00 → диапазон: сегодня 08:00 - сегодня 20:00
+ *
+ * @param int $hoursAgo Количество часов назад от конца смены
+ * @return array{startDate: string, endDate: string} Массив с датами в формате 'Y-m-d H:i:s'
+ */
+ private function getShiftBasedDateRange(int $hoursAgo): array
+ {
+ $now = new \DateTime('now', new \DateTimeZone(self::TIMEZONE));
+ $currentHour = (int)$now->format('H');
+
+ // Определяем конец текущей смены (база для расчёта)
+ $shiftEnd = clone $now;
+ if ($currentHour < 20) {
+ // До 20:00 — база 08:00 сегодня
+ $shiftEnd->setTime(8, 0, 0);
+ } else {
+ // После 20:00 — база 20:00 сегодня
+ $shiftEnd->setTime(20, 0, 0);
+ }
+
+ // Вычисляем начало периода
+ $shiftStart = clone $shiftEnd;
+ $shiftStart->modify("-{$hoursAgo} hours");
+
+ $this->logInfo('Вычислен диапазон дат на основе смены', [
+ 'current_time' => $now->format('Y-m-d H:i:s'),
+ 'shift_end' => $shiftEnd->format('Y-m-d H:i:s'),
+ 'shift_start' => $shiftStart->format('Y-m-d H:i:s'),
+ 'hours_ago' => $hoursAgo,
+ ]);
+
+ return [
+ 'startDate' => $shiftStart->format('Y-m-d H:i:s'),
+ 'endDate' => $shiftEnd->format('Y-m-d H:i:s'),
+ ];
+ }
+
/**
* Логирование в структурированном JSON-формате
*