]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
Изменяем логику обработки писем от Flowwow
authorVladimir Fomichev <vladimir.fomichev@erp-flowers.ru>
Wed, 18 Feb 2026 08:48:12 +0000 (11:48 +0300)
committerVladimir Fomichev <vladimir.fomichev@erp-flowers.ru>
Wed, 18 Feb 2026 08:48:12 +0000 (11:48 +0300)
erp24/commands/MarketplaceController.php
erp24/controllers/MarketplaceFlowwowEmailsController.php
erp24/media/controllers/FlowwowController.php
erp24/migrations/m260218_000001_improve_marketplace_flowwow_emails.php [new file with mode: 0644]
erp24/records/MarketplaceFlowwowEmails.php
erp24/records/MarketplaceFlowwowEmailsSearch.php
erp24/services/MarketplaceService.php
erp24/views/marketplace-flowwow-emails/index.php

index 4c242fd0f164f9b536888101f9fd271145e84014..d370164da055af19baa5dee14b2afb3cc30206dd 100644 (file)
@@ -159,6 +159,28 @@ class MarketplaceController extends Controller
         return ExitCode::OK;
     }
 
+    /**
+     * Повторная обработка писем Flowwow, которые не были успешно обработаны.
+     * Выбирает из БД письма со статусом NEW или RETRY и запускает processMessage для каждого.
+     *
+     * Использование: php yii marketplace/retry-flowwow-emails
+     */
+    public function actionRetryFlowwowEmails(): int
+    {
+        $progressCallback = function (string $message) {
+            $this->stdout($message . "\n", BaseConsole::FG_YELLOW);
+        };
+
+        $result = MarketplaceService::processUnprocessedEmails($progressCallback);
+
+        $this->stdout(
+            "Итог: обработано {$result['processed']} из {$result['total']}, ошибок: {$result['failed']}.\n",
+            BaseConsole::FG_GREEN
+        );
+
+        return ExitCode::OK;
+    }
+
     public function actionGetYandexOrders()
     {
         $fromDate = date('d-m-Y', strtotime('-1 day'));
index c30c9fbbad760811bd3af6403bbe1da6595c03a1..3f382e13ffd1a902359aa27e378a5a8136682d56 100644 (file)
@@ -42,6 +42,7 @@ class MarketplaceFlowwowEmailsController extends Controller
     {
         $searchModel = new MarketplaceFlowwowEmailsSearch();
         $dataProvider = $searchModel->search($this->request->queryParams);
+        $dataProvider->query->with(['order']);
 
         return $this->render('index', [
             'searchModel' => $searchModel,
index 04b0d0766f76c14f906b87aff9e91b0eb8693ac7..7b1fa85c21dc361806d8b7709d83b5ce056d401b 100644 (file)
@@ -70,15 +70,13 @@ class FlowwowController extends Controller
         $oldMail = 0;
         $seen = 0;
 
-        $countMessages = 0;
-        $count = 0;
-
         $messages = MarketplaceService::getFlowwowOrdersFromMail($date, $since, $oldMail, null, $seen);
 
-        $countMessages = count($messages);
-        $count = MarketplaceService::processMessages($messages);
+        if (!is_array($messages)) {
+            return ['success' => false, 'processed' => 0, 'all' => 0];
+        }
 
-        return ['success' => true, 'count' => $count];
+        return ['success' => true, 'processed' => $messages['processed'], 'all' => $messages['all']];
     }
 
 
diff --git a/erp24/migrations/m260218_000001_improve_marketplace_flowwow_emails.php b/erp24/migrations/m260218_000001_improve_marketplace_flowwow_emails.php
new file mode 100644 (file)
index 0000000..f9be76b
--- /dev/null
@@ -0,0 +1,64 @@
+<?php
+
+use yii\db\Migration;
+
+/**
+ * Улучшение таблицы marketplace_flowwow_emails:
+ * - Добавляет колонки для отслеживания состояния обработки
+ * - Помечает все существующие записи как обработанные
+ */
+class m260218_000001_improve_marketplace_flowwow_emails extends Migration
+{
+    public function safeUp()
+    {
+        $this->addColumn(
+            'marketplace_flowwow_emails',
+            'subject_type',
+            $this->smallInteger()->null()
+                ->comment('Тип письма (1=NEW, 2=APPROVED, 3=CHANGED, 4=CANCELLED, 5=DELIVERED)')
+        );
+
+        $this->addColumn(
+            'marketplace_flowwow_emails',
+            'processing_attempts',
+            $this->integer()->notNull()->defaultValue(0)
+                ->comment('Счётчик попыток обработки')
+        );
+
+        $this->addColumn(
+            'marketplace_flowwow_emails',
+            'processed_at',
+            $this->timestamp()->null()
+                ->comment('Время успешной обработки')
+        );
+
+        $this->addColumn(
+            'marketplace_flowwow_emails',
+            'error_message',
+            $this->text()->null()
+                ->comment('Текст последней ошибки обработки')
+        );
+
+        $this->addColumn(
+            'marketplace_flowwow_emails',
+            'marketplace_order_id',
+            $this->string(50)->null()
+                ->comment('ID заказа Flowwow (связка с marketplace_orders.marketplace_order_id)')
+        );
+
+        // Все существующие записи помечаем как обработанные, чтобы новый механизм retry их не трогал.
+        // processed_at берётся из created_at — так дата обработки соответствует фактическому времени письма.
+        $this->execute(
+            "UPDATE marketplace_flowwow_emails SET email_status = 1, processed_at = COALESCE(created_at, NOW()) WHERE email_status IS NULL OR email_status = 0"
+        );
+    }
+
+    public function safeDown()
+    {
+        $this->dropColumn('marketplace_flowwow_emails', 'marketplace_order_id');
+        $this->dropColumn('marketplace_flowwow_emails', 'error_message');
+        $this->dropColumn('marketplace_flowwow_emails', 'processed_at');
+        $this->dropColumn('marketplace_flowwow_emails', 'processing_attempts');
+        $this->dropColumn('marketplace_flowwow_emails', 'subject_type');
+    }
+}
index 42f33a22f758fb3bbf9db62db552b6c78c3d58af..7cb00e951dc1047a06bd8e8d4c3b08bc4b9477d4 100644 (file)
@@ -9,16 +9,28 @@ use Yii;
  *
  * @property int $id ID
  * @property string $subject Тема письма
- * @property int|null $email_status Ð¡Ñ\82аÑ\82Ñ\83Ñ\81 Ð¿Ð¸Ñ\81Ñ\8cма - Ñ\80азобÑ\80ано - 1, Ð½Ðµ Ñ\80азобÑ\80ано - 2
+ * @property int|null $email_status Ð¡Ñ\82аÑ\82Ñ\83Ñ\81 Ð¾Ð±Ñ\80абоÑ\82ки
  * @property string $from Отправитель письма
  * @property string $to Получатель письма
  * @property string $date Дата письма
  * @property string $body Тело письма
  * @property string|null $created_at Дата создания записи
+ * @property int|null $subject_type Тип письма (1=NEW, 2=APPROVED, 3=CHANGED, 4=CANCELLED, 5=DELIVERED)
+ * @property int $processing_attempts Счётчик попыток обработки
+ * @property string|null $processed_at Время успешной обработки
+ * @property string|null $error_message Текст последней ошибки
+ * @property string|null $marketplace_order_id ID заказа Flowwow
+ *
+ * @property-read \yii_app\records\MarketplaceOrders|null $order Связанный заказ
  */
 class MarketplaceFlowwowEmails extends \yii\db\ActiveRecord
 {
+    public const STATUS_NEW = 0;        // Зарегистрировано, ожидает обработки
+    public const STATUS_PROCESSED = 1;  // Успешно обработано
+    public const STATUS_ERROR = 2;      // Ошибка (исчерпаны попытки)
+    public const STATUS_RETRY = 3;      // Ожидает повторной обработки
 
+    public const MAX_PROCESSING_ATTEMPTS = 5;
 
     /**
      * {@inheritdoc}
@@ -34,12 +46,13 @@ class MarketplaceFlowwowEmails extends \yii\db\ActiveRecord
     public function rules()
     {
         return [
-            [['email_status'], 'default', 'value' => 0],
+            [['email_status'], 'default', 'value' => self::STATUS_NEW],
             [['subject', 'from', 'to', 'date', 'body'], 'required'],
-            [['date', 'created_at'], 'safe'],
-            [['body'], 'string'],
-            [['email_status'], 'integer'],
-            [['subject',  'from', 'to'], 'string', 'max' => 255],
+            [['date', 'created_at', 'processed_at'], 'safe'],
+            [['body', 'error_message'], 'string'],
+            [['email_status', 'subject_type', 'processing_attempts'], 'integer'],
+            [['subject', 'from', 'to'], 'string', 'max' => 255],
+            [['marketplace_order_id'], 'string', 'max' => 50],
         ];
     }
 
@@ -51,13 +64,106 @@ class MarketplaceFlowwowEmails extends \yii\db\ActiveRecord
         return [
             'id' => 'ID',
             'subject' => 'Тема письма',
-            'email_status' => 'Статус письма',
-            'from' => 'Отправитель письма',
-            'to' => 'Получатель письма',
+            'email_status' => 'Статус',
+            'from' => 'Отправитель',
+            'to' => 'Получатель',
             'date' => 'Дата письма',
             'body' => 'Тело письма',
-            'created_at' => 'Дата создания записи',
+            'created_at' => 'Дата создания',
+            'subject_type' => 'Тип письма',
+            'processing_attempts' => 'Попыток',
+            'processed_at' => 'Обработано в',
+            'error_message' => 'Текст ошибки',
+            'marketplace_order_id' => 'ID заказа',
+        ];
+    }
+
+    /**
+     * Связь с заказом маркетплейса по marketplace_order_id.
+     * Поле marketplace_order_id в emails хранит ID заказа из маркетплейса (например "123456"),
+     * которому соответствует marketplace_orders.marketplace_order_id.
+     */
+    public function getOrder(): \yii\db\ActiveQuery
+    {
+        return $this->hasOne(MarketplaceOrders::class, ['marketplace_order_id' => 'marketplace_order_id'])
+            ->andWhere(['marketplace_id' => \yii_app\records\MarketplaceStore::FLOWWOW_WAREHOUSE_ID]);
+    }
+
+    /**
+     * Помечает письмо как успешно обработанное.
+     * Устанавливает STATUS_PROCESSED и фиксирует время обработки.
+     */
+    public function markAsProcessed(): bool
+    {
+        $this->email_status = self::STATUS_PROCESSED;
+        $this->processed_at = date('Y-m-d H:i:s');
+        return $this->save(false, ['email_status', 'processed_at']);
+    }
+
+    /**
+     * Помечает письмо как завершённое с ошибкой (исчерпаны попытки).
+     */
+    public function markAsError(string $errorMessage): bool
+    {
+        $this->email_status = self::STATUS_ERROR;
+        $this->error_message = $errorMessage;
+        $this->processing_attempts++;
+        return $this->save(false, ['email_status', 'error_message', 'processing_attempts']);
+    }
+
+    /**
+     * Помечает письмо для повторной обработки.
+     */
+    public function markForRetry(string $reason): bool
+    {
+        $this->email_status = self::STATUS_RETRY;
+        $this->error_message = $reason;
+        $this->processing_attempts++;
+        return $this->save(false, ['email_status', 'error_message', 'processing_attempts']);
+    }
+
+    /**
+     * Проверяет, разрешена ли ещё повторная обработка.
+     */
+    public function isRetryAllowed(): bool
+    {
+        return $this->processing_attempts < self::MAX_PROCESSING_ATTEMPTS;
+    }
+
+    /**
+     * Возвращает ActiveQuery для писем, ожидающих обработки (STATUS_NEW или STATUS_RETRY).
+     */
+    public static function findUnprocessed(): \yii\db\ActiveQuery
+    {
+        return static::find()
+            ->where(['in', 'email_status', [self::STATUS_NEW, self::STATUS_RETRY]])
+            ->andWhere(['<', 'processing_attempts', self::MAX_PROCESSING_ATTEMPTS]);
+    }
+
+    /**
+     * Текстовые метки статусов обработки.
+     */
+    public static function statusLabels(): array
+    {
+        return [
+            self::STATUS_NEW => 'Необработано',
+            self::STATUS_PROCESSED => 'Обработано',
+            self::STATUS_ERROR => 'Ошибка',
+            self::STATUS_RETRY => 'Повтор',
         ];
     }
 
+    /**
+     * Текстовые метки типов писем.
+     */
+    public static function subjectTypeLabels(): array
+    {
+        return [
+            1 => 'Новый заказ',
+            2 => 'Принят',
+            3 => 'Изменён',
+            4 => 'Отменён',
+            5 => 'Доставлен',
+        ];
+    }
 }
index 13f2e8145c97fc7585ad04fe22bc295d0dc0f87a..08deeb145346215836fb862d00355488b74b79bd 100644 (file)
@@ -17,8 +17,9 @@ class MarketplaceFlowwowEmailsSearch extends MarketplaceFlowwowEmails
     public function rules()
     {
         return [
-            [['id', 'email_status'], 'integer'],
-            [['subject', 'from', 'to', 'date', 'body', 'created_at', 'email_status'], 'safe'],
+            [['id', 'email_status', 'subject_type', 'processing_attempts'], 'integer'],
+            [['subject', 'from', 'to', 'date', 'body', 'created_at', 'email_status',
+              'processed_at', 'error_message', 'marketplace_order_id'], 'safe'],
         ];
     }
 
@@ -43,8 +44,6 @@ class MarketplaceFlowwowEmailsSearch extends MarketplaceFlowwowEmails
     {
         $query = MarketplaceFlowwowEmails::find();
 
-        // add conditions that should always apply here
-
         $dataProvider = new ActiveDataProvider([
             'query' => $query,
         ]);
@@ -52,8 +51,6 @@ class MarketplaceFlowwowEmailsSearch extends MarketplaceFlowwowEmails
         $this->load($params, $formName);
 
         if (!$this->validate()) {
-            // uncomment the following line if you do not want to return any records when validation fails
-            // $query->where('0=1');
             return $dataProvider;
         }
 
@@ -62,13 +59,17 @@ class MarketplaceFlowwowEmailsSearch extends MarketplaceFlowwowEmails
             'id' => $this->id,
             'date' => $this->date,
             'created_at' => $this->created_at,
+            'subject_type' => $this->subject_type,
+            'processing_attempts' => $this->processing_attempts,
         ]);
 
         $query->andFilterWhere(['ilike', 'subject', $this->subject])
             ->andFilterWhere(['email_status' => $this->email_status])
             ->andFilterWhere(['ilike', 'from', $this->from])
             ->andFilterWhere(['ilike', 'to', $this->to])
-            ->andFilterWhere(['ilike', 'body', $this->body]);
+            ->andFilterWhere(['ilike', 'body', $this->body])
+            ->andFilterWhere(['ilike', 'error_message', $this->error_message])
+            ->andFilterWhere(['ilike', 'marketplace_order_id', $this->marketplace_order_id]);
 
         return $dataProvider;
     }
index 2f9bae882978a985b5dd1eb6c94a9c509d792a00..327eabf8a0058e87a63c3c60905f40b5a17e06b0 100644 (file)
@@ -2197,13 +2197,24 @@ class MarketplaceService
                     }
                     $savedEmail = self::saveEmailIfNotExists($subject, null, $from, $to, $date, $htmlMessage);
 
+                    // Если письмо уже существует — подгружаем из БД
+                    if ($savedEmail === null) {
+                        $emailRecord = MarketplaceFlowwowEmails::find()
+                            ->where(['subject' => $subject, 'from' => $from, 'date' => $date])
+                            ->one();
+                    } else {
+                        $emailRecord = $savedEmail;
+                    }
+
+                    // Уже обработанное письмо — только ставим SEEN и пропускаем
+                    if ($emailRecord !== null && $emailRecord->email_status === MarketplaceFlowwowEmails::STATUS_PROCESSED) {
+                        imap_setflag_full($inbox, $email_number, "\\Seen");
+                        continue;
+                    }
+
                     foreach ($subjectPatterns as $pattern) {
                         if (preg_match($pattern, $subject)) {
                             $subjectIndex = self::SUBJECT_INDEX[$pattern];
-                            if ($savedEmail !== null) {
-                                $savedEmail->email_status = 1;
-                                $savedEmail->save();
-                            }
                             $message = [
                                 'subject' => $subject,
                                 'subject_index' => $subjectIndex,
@@ -2213,9 +2224,17 @@ class MarketplaceService
                                 'body' => quoted_printable_decode($htmlMessage),
                             ];
 
-                            $output = MarketplaceService::processMessage($message);
+                            try {
+                                $output = MarketplaceService::processMessage($message);
+
+                                if ($emailRecord !== null) {
+                                    $orderData = self::getOrdersDataFromMessage($message);
+                                    if (!empty($orderData)) {
+                                        $emailRecord->marketplace_order_id = (string)key($orderData);
+                                    }
+                                    $emailRecord->markAsProcessed();
+                                }
 
-                            if ($output > 0) {
                                 self::imap_debug_log("Установка флага SEEN для сообшения #" . $email_number, $debugMode, $progressCallback);
                                 $result = imap_setflag_full($inbox, $email_number, "\\Seen");
                                 if (!$result) {
@@ -2231,8 +2250,18 @@ class MarketplaceService
                                 } else {
                                     self::imap_debug_log("WARNING: Сообщение #" . $email_number . " не удалось пометить как  SEEN", $debugMode, $progressCallback);
                                 }
+
+                                $countProcessedMessages += $output;
+                            } catch (\Throwable $e) {
+                                Yii::error('Ошибка при обработке письма: ' . $e->getMessage(), __METHOD__);
+                                if ($emailRecord !== null) {
+                                    if ($emailRecord->isRetryAllowed()) {
+                                        $emailRecord->markForRetry($e->getMessage());
+                                    } else {
+                                        $emailRecord->markAsError($e->getMessage());
+                                    }
+                                }
                             }
-                            $countProcessedMessages += $output;
 
                             if ($progressCallback) {
                                 call_user_func($progressCallback, "От: " . $from . " тема " . $subject . " от " . $date);
@@ -2257,6 +2286,85 @@ class MarketplaceService
         return ['processed' => $countProcessedMessages, 'all' => $countAllMessages];
     }
 
+    /**
+     * Определяет тип письма по теме через SUBJECT_INDEX regex patterns.
+     *
+     * @param string $subject Тема письма (уже декодированная)
+     * @return int|null Тип письма (1=NEW, 2=APPROVED, 3=CHANGED, 4=CANCELLED, 5=DELIVERED) или null
+     */
+    private static function detectSubjectType(string $subject): ?int
+    {
+        foreach (self::SUBJECT_INDEX as $pattern => $index) {
+            if (preg_match($pattern, $subject)) {
+                return $index;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Обрабатывает все необработанные письма из БД (статус NEW или RETRY с заполненным subject_type).
+     * Используется командой marketplace/retry-flowwow-emails для повторной обработки.
+     *
+     * @param callable|null $progressCallback
+     * @return array ['processed' => int, 'failed' => int, 'total' => int]
+     */
+    public static function processUnprocessedEmails(?callable $progressCallback = null): array
+    {
+        $emails = MarketplaceFlowwowEmails::findUnprocessed()
+            ->andWhere(['not', ['subject_type' => null]])
+            ->all();
+
+        $total = count($emails);
+        $processed = 0;
+        $failed = 0;
+
+        if ($progressCallback) {
+            call_user_func($progressCallback, "Найдено необработанных писем: {$total}");
+        }
+
+        foreach ($emails as $emailRecord) {
+            $message = [
+                'subject'       => $emailRecord->subject,
+                'subject_index' => $emailRecord->subject_type,
+                'from'          => $emailRecord->from,
+                'to'            => $emailRecord->to,
+                'date'          => $emailRecord->date,
+                'body'          => $emailRecord->body,
+            ];
+
+            try {
+                self::processMessage($message);
+
+                $orderData = self::getOrdersDataFromMessage($message);
+                if (!empty($orderData)) {
+                    $emailRecord->marketplace_order_id = (string)key($orderData);
+                }
+                $emailRecord->markAsProcessed();
+                $processed++;
+
+                if ($progressCallback) {
+                    call_user_func($progressCallback, "Обработано письмо #{$emailRecord->id}: {$emailRecord->subject}");
+                }
+            } catch (\Throwable $e) {
+                $failed++;
+                Yii::error("Ошибка при повторной обработке письма #{$emailRecord->id}: " . $e->getMessage(), __METHOD__);
+
+                if ($emailRecord->isRetryAllowed()) {
+                    $emailRecord->markForRetry($e->getMessage());
+                } else {
+                    $emailRecord->markAsError($e->getMessage());
+                }
+
+                if ($progressCallback) {
+                    call_user_func($progressCallback, "Ошибка письма #{$emailRecord->id}: " . $e->getMessage());
+                }
+            }
+        }
+
+        return ['processed' => $processed, 'failed' => $failed, 'total' => $total];
+    }
+
     public static function saveEmailIfNotExists($subject, $subjectPattern, $from, $to, $date, $body)
     {
         if (strpos($from, 'info@flowwow.com') === false) {
@@ -2280,6 +2388,7 @@ class MarketplaceService
             $email->date = $date;
             $email->body = quoted_printable_decode($body);
             $email->created_at = date('Y-m-d H:i:s');
+            $email->subject_type = self::detectSubjectType($subject);
 
             if ($email->save()) {
                 return $email;
@@ -2687,6 +2796,7 @@ class MarketplaceService
         $statuses = ArrayHelper::map($statuses, 'code', 'id');
         $statusCodes = array_unique(array_keys($statuses));
         $newOrdersCount = 0;
+        $processingSuccess = false;
         $campaignId = $store;
         
         // Проверяем, что $order не пустой
@@ -2717,6 +2827,7 @@ class MarketplaceService
             if ($index == self::SUBJECT_INDEX[self::SUBJECT_NEW]) {
                 $marketplaceOrder = self::createOrderFlowwow($orderDetails, $campaignId, $statusId, $substatusId);
                 if ($marketplaceOrder && $marketplaceOrder->save()) {
+                    $processingSuccess = true;
                     self::sendMessageToTelegram($marketplaceOrder->guid, "Новый  заказ Флаувау №" . $marketplaceOrder->marketplace_order_id);
                      $newOrdersCount += 1;
                      self::createOrUpdateStatusHistory($marketplaceOrder->id, $statusId, $substatusId, $orderDetails);
@@ -2747,6 +2858,7 @@ class MarketplaceService
 
                 }
                 if ($marketplaceOrder->save()) {
+                    $processingSuccess = true;
                     self::createOrUpdateStatusHistory($marketplaceOrder->id, $statusId, $substatusId, $orderDetails);
                     if (isset($orderDetails['delivery'])) {
                         $deliveryRecord = self::saveFromDeliveryText($marketplaceOrder->id, $orderDetails['delivery']);
@@ -2781,6 +2893,7 @@ class MarketplaceService
                 if ($isChanged) {
                     $marketplaceOrder->raw_data = json_encode($oldRawData, JSON_UNESCAPED_UNICODE);
                     if ($marketplaceOrder->save()) {
+                        $processingSuccess = true;
                         self::createOrUpdateStatusHistory($marketplaceOrder->id, $statusId, $substatusId, $orderDetails);
                         if (isset($orderDetails['delivery'])) {
                             $deliveryRecord = self::saveFromDeliveryText($marketplaceOrder->id, $orderDetails['delivery']);
@@ -2796,6 +2909,7 @@ class MarketplaceService
             } else {
                 // отмена или успешное выполнение
                 if ($marketplaceOrder->save()) {
+                    $processingSuccess = true;
                     self::createOrUpdateStatusHistory($marketplaceOrder->id, $statusId, $substatusId, $orderDetails);
                 } else {
                     Yii::error('Не удалось обновить заказ' . json_encode($marketplaceOrder->getErrors(), JSON_UNESCAPED_UNICODE));
@@ -2805,7 +2919,7 @@ class MarketplaceService
             self::checkAndSetReadyTo1c($marketplaceOrder);
         }
 
-        return $newOrdersCount;
+        return $processingSuccess ? max($newOrdersCount, 1) : 0;
     }
 
     /**
index d30df5741fe55e4ca72e686ad5a3aee643863014..a71ea6664d1bb5121a15a8a62c989b6a8fdc2c7a 100644 (file)
@@ -31,24 +31,66 @@ $this->params['breadcrumbs'][] = $this->title;
 
             'id',
             'subject',
-            
+
             [
                 'attribute' => 'email_status',
                 'format' => 'raw',
                 'value' => function ($model) {
-                    $text = '';
-                    $class = '';
-                    if ($model->email_status === 1) {
-                        $class = 'bg-success text-white';
-                        $text = 'Обработано';
-                    } elseif ($model->email_status === 0) {
-                        $class = 'bg-danger text-white';
-                        $text = 'Необработано';
-                    }
+                    $labels = [
+                        MarketplaceFlowwowEmails::STATUS_NEW       => ['Необработано', 'bg-danger text-white'],
+                        MarketplaceFlowwowEmails::STATUS_PROCESSED => ['Обработано',   'bg-success text-white'],
+                        MarketplaceFlowwowEmails::STATUS_ERROR     => ['Ошибка',       'bg-dark text-white'],
+                        MarketplaceFlowwowEmails::STATUS_RETRY     => ['Повтор',       'bg-primary text-white'],
+                    ];
+                    [$text, $class] = $labels[$model->email_status] ?? ['—', ''];
                     return Html::tag('span', $text, ['class' => "badge $class"]);
                 },
-                'filter' => Html::input('text', 'MarketplaceFlowwowEmailsSearch[email_status]', $searchModel->email_status, ['class' => 'form-control']),
+                'filter' => Html::activeDropDownList(
+                    $searchModel,
+                    'email_status',
+                    MarketplaceFlowwowEmails::statusLabels(),
+                    ['class' => 'form-control', 'prompt' => '— все —']
+                ),
+            ],
+
+            [
+                'attribute' => 'subject_type',
+                'value' => fn($model) => MarketplaceFlowwowEmails::subjectTypeLabels()[$model->subject_type] ?? '—',
+                'filter' => Html::activeDropDownList(
+                    $searchModel,
+                    'subject_type',
+                    MarketplaceFlowwowEmails::subjectTypeLabels(),
+                    ['class' => 'form-control', 'prompt' => '— все —']
+                ),
             ],
+
+            [
+                'attribute' => 'marketplace_order_id',
+                'format' => 'raw',
+                'value' => function ($model) {
+                    if (!$model->marketplace_order_id) {
+                        return '—';
+                    }
+                    $order = $model->order;
+                    if ($order) {
+                        return Html::a(
+                            '№' . $model->marketplace_order_id,
+                            ['/marketplace-orders/view', 'id' => $order->id],
+                            ['class' => 'btn btn-xs btn-outline-primary', 'target' => '_blank']
+                        );
+                    }
+                    return Html::encode($model->marketplace_order_id) . ' (не найден)';
+                },
+                'filter' => Html::activeInput(
+                    'text',
+                    $searchModel,
+                    'marketplace_order_id',
+                    ['class' => 'form-control', 'placeholder' => '№ заказа']
+                ),
+            ],
+
+            'processing_attempts',
+
             'from',
             'to',
             'date',