]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
Тестирование разделителя
authorVladimir Fomichev <vladimir.fomichev@erp-flowers.ru>
Thu, 2 Oct 2025 14:42:55 +0000 (17:42 +0300)
committerVladimir Fomichev <vladimir.fomichev@erp-flowers.ru>
Thu, 2 Oct 2025 14:42:55 +0000 (17:42 +0300)
erp24/controllers/MarketplaceOrdersController.php
erp24/services/MarketplaceService.php
erp24/views/marketplace-orders/test-article-processing.php [new file with mode: 0644]
erp24/views/marketplace-orders/test-order-parsing.php [new file with mode: 0644]

index e1981ab73a6eae1484fa6fa535678cb9e63d748b..3bbf07f87c0187d165cc789a1bcd0bfc360ac297 100644 (file)
@@ -3,6 +3,7 @@
 namespace app\controllers;
 
 use app\records\OrdersUnion;
+use yii\base\DynamicModel;
 
 use OpenAPI\Client\Configuration;
 use OpenAPI\Client\ObjectSerializer;
@@ -578,7 +579,7 @@ class MarketplaceOrdersController extends Controller
             $data = json_decode($order->raw_data, true);
             if ($order->marketplace_id == 1 && isset($data['delivery'])) {
                 $deliveryText = (explode(',', $data['delivery']))[1] ?? '';
-                $deliveryText = trim(str_replace('Нижний Новгород', '', $deliveryText));
+                $deliveryText = trim(str_replace('Нижний Новгород', '', $deliveryText ?? ''));
 
                 $months = [
                     'января' => '01', 'февраля' => '02', 'марта' => '03',
@@ -974,4 +975,55 @@ class MarketplaceOrdersController extends Controller
             return $this->redirect(['view', 'id' => $item->order_id]);
         }
     }
+
+    public function actionTestOrderParsing()
+    {
+        $parsedOrder = null;
+        $htmlBody = '';
+        $parsingError = null;
+
+        $model = new DynamicModel(['html_body']);
+        $model->addRule(['html_body'], 'required', ['message' => 'HTML тело письма не может быть пустым']);
+        $model->addRule(['html_body'], 'string', ['min' => 1, 'message' => 'HTML тело письма содержит только пробелы или невидимые символы']);
+
+        if ($model->load(Yii::$app->request->post()) && $model->validate()) {
+            $htmlBody = $model->html_body;
+            try {
+                $message = [
+                    'body' => $htmlBody,
+                    'date' => date('Y-m-d H:i:s'),
+                    'to' => 'test@example.com'
+                ];
+
+                $parsedOrder = MarketplaceService::getOrdersDataFromMessage($message);
+
+                // Если результат null или пустой массив, это не ошибка разбора,
+                // а просто отсутствие заказов в HTML
+                if ($parsedOrder === null) {
+                    $parsingError = 'Не удалось разобрать заказы из HTML. Проверьте корректность HTML структуры.';
+                }
+            } catch (\Exception $e) {
+                $parsingError = 'Ошибка при разборе: ' . $e->getMessage();
+            }
+        } elseif (Yii::$app->request->isPost) {
+            // Если POST-запрос был, но валидация не прошла
+            $parsingError = implode('<br>', $model->getFirstErrors());
+        }
+
+        return $this->render('test-order-parsing', [
+            'model' => $model,
+            'parsedOrder' => $parsedOrder,
+            'htmlBody' => $htmlBody,
+            'parsingError' => $parsingError,
+        ]);
+    }
+
+    public function actionTestArticleProcessing()
+    {
+        $testResults = MarketplaceService::testArticleProcessing();
+
+        return $this->render('test-article-processing', [
+            'testResults' => $testResults,
+        ]);
+    }
 }
index 0781b17cfde3a44c3cf65caf0100aa69f26d7e90..2a0bd24b1a35fe241dd381f60a31c99aa1ce0a6b 100644 (file)
@@ -684,6 +684,78 @@ class MarketplaceService
         return $name;
     }
 
+    /**
+     * Тестовый метод для проверки обработки артикулов
+     */
+    public static function testArticleProcessing()
+    {
+        $testCases = [
+            // Тесты для normalizeArticleInName (убираем дефис)
+            [
+                'input' => 'Букет из 11 розовых пионов (FW-0076)',
+                'expected' => 'Букет из 11 розовых пионов (FW0076)',
+                'method' => 'normalizeArticleInName'
+            ],
+            [
+                'input' => 'Букет из 11 розовых пионов (FW0076)',
+                'expected' => 'Букет из 11 розовых пионов (FW0076)',
+                'method' => 'normalizeArticleInName'
+            ],
+            [
+                'input' => 'Букет из 11 розовых пионов',
+                'expected' => 'Букет из 11 розовых пионов',
+                'method' => 'normalizeArticleInName'
+            ],
+            [
+                'input' => 'Букет из 11 розовых пионов (ABC-123)',
+                'expected' => 'Букет из 11 розовых пионов (ABC123)',
+                'method' => 'normalizeArticleInName'
+            ],
+
+            // Тесты для processDisplayName (добавляем дефис)
+            [
+                'input' => 'Букет из 11 розовых пионов (FW0076)',
+                'expected' => 'Букет из 11 розовых пионов (FW-0076)',
+                'method' => 'processDisplayName'
+            ],
+            [
+                'input' => 'Букет из 11 розовых пионов (FW-0076)',
+                'expected' => 'Букет из 11 розовых пионов (FW-0076)',
+                'method' => 'processDisplayName'
+            ],
+            [
+                'input' => 'Букет из 11 розовых пионов',
+                'expected' => 'Букет из 11 розовых пионов',
+                'method' => 'processDisplayName'
+            ],
+            [
+                'input' => 'Букет из 11 розовых пионов (ABC123)',
+                'expected' => 'Букет из 11 розовых пионов (ABC-123)',
+                'method' => 'processDisplayName'
+            ],
+        ];
+
+        $results = [];
+        foreach ($testCases as $testCase) {
+            $method = $testCase['method'];
+            $input = $testCase['input'];
+            $expected = $testCase['expected'];
+
+            $actual = self::$method($input);
+            $passed = $actual === $expected;
+
+            $results[] = [
+                'method' => $method,
+                'input' => $input,
+                'expected' => $expected,
+                'actual' => $actual,
+                'passed' => $passed
+            ];
+        }
+
+        return $results;
+    }
+
     public static function getProductImageUrl($imageId)
     {
         $image = Images::findOne($imageId);
@@ -1919,185 +1991,232 @@ class MarketplaceService
         $html = $message['body'];
         $orderDetails = null;
         $order = null;
-        if (!empty($html)) {
-            $html = preg_replace('/\s+/', ' ', $html);
-            // Декодируем HTML-сущности
-            $html = html_entity_decode($html, ENT_COMPAT, 'UTF-8');
-
-            $doc = new HtmlDomParser($html);
-            $orderNumber = '';
-            $main = $doc->findOneOrFalse("body");
-
-            if ($main !== false) {
-                $orderTitleNode = $main->findOne("h1");
-                if ($orderTitleNode && preg_match('/№(\d+)/', $orderTitleNode->innertext, $matches)) {
-                    $orderNumber = (int)$matches[1];
-                }
-            }
 
-            $orderDetails['number'] = $orderNumber;
-            $orderDetails['date'] = $message['date'];
-            $deliveryText = '';
-            $clientText = '';
-            $orderItems = [];
-            $totalSum = 0;
-            $deliverySum = 0;
-
-            $linkBlock = $main->findOneOrFalse('a:contains("Перейти в заказ ")');
-            if ($linkBlock) {
-                $link = $linkBlock->getAttribute('href');
-                $orderDetails['orderLink'] = $link;
-            }
+        // Детальная проверка HTML
+        if (empty($html)) {
+            Yii::error('HTML тело письма пустое', __METHOD__);
+            return [];
+        }
 
-            $deliveryLabel = $main->findOne('p:contains("Доставить")');
-            $pickupLabel = $main->findOne('p:contains("Самовывоз")');
+        if (empty(trim($html))) {
+            Yii::error('HTML тело письма содержит только пробелы', __METHOD__);
+            return [];
+        }
 
-            $targetLabel = $deliveryLabel ?? $pickupLabel;
-            $labelPrefix = $deliveryLabel ? 'Доставка:' : ($pickupLabel ? 'Самовывоз:' : null);
+        // Очищаем от лишних пробелов, но сохраняем структуру
+        $html = preg_replace('/\s+/', ' ', $html);
+        $html = trim($html);
 
-            if ($targetLabel && $targetLabel->nextNonWhitespaceSibling()) {
-                $infoBlock = $targetLabel->nextNonWhitespaceSibling();
-                $textParts = [];
+        if (empty($html)) {
+            Yii::error('HTML тело письма стало пустым после очистки', __METHOD__);
+            return [];
+        }
 
-                $lines = explode('<br>', $infoBlock->innerHtml());
+        // Декодируем HTML-сущности
+        $html = html_entity_decode($html, ENT_COMPAT, 'UTF-8');
 
-                foreach ($lines as $line) {
-                    $clean = trim(strip_tags($line));
-                    if ($clean !== '') {
-                        $textParts[] = $clean;
-                    }
-                }
+        // Проверяем наличие основных HTML тегов
+        if (stripos($html, '<body') === false && stripos($html, '<html') === false) {
+            Yii::warning('HTML не содержит основных тегов body или html', __METHOD__);
+        }
 
-                if (!empty($textParts)) {
-                    $deliveryText = $labelPrefix . ' ' . $textParts[0];
+        // Парсим HTML и извлекаем данные заказа
+        $doc = new HtmlDomParser($html);
+        $orderNumber = '';
+        $main = $doc->findOneOrFalse("body");
 
-                    if (isset($textParts[1])) {
-                        $deliveryText .= ' ' . $textParts[1];
-                    }
+        // Если не найден body, пробуем найти по другим селекторам
+        if ($main === false) {
+            Yii::warning('Тег body не найден, пробуем найти контент по другим селекторам', __METHOD__);
 
-                    if (isset($textParts[2])) {
-                        $deliveryText .= ', ' . $textParts[2];
-                    }
-                }
-            }
+            // Пробуем найти div или другой контейнер с контентом
+            $main = $doc->findOneOrFalse("div") ?: $doc->findOneOrFalse("*");
 
-            if (!empty($deliveryText)) {
-                $deliveryText = preg_replace('/\s+/', ' ', $deliveryText);
-                $deliveryText = trim($deliveryText);
-                $orderDetails['delivery'] = $deliveryText;
+            if ($main === false) {
+                Yii::error('Не удалось найти подходящий контейнер для парсинга HTML', __METHOD__);
+                return [];
             }
+        }
+
+        $orderTitleNode = $main->findOne("h1");
+        if ($orderTitleNode && preg_match('/№(\d+)/', $orderTitleNode->innertext, $matches)) {
+            $orderNumber = (int)$matches[1];
+        } elseif (!$orderTitleNode) {
+            Yii::warning('Не найден заголовок заказа (h1) в HTML', __METHOD__);
+        }
+
+        $orderDetails['number'] = $orderNumber;
+        $orderDetails['date'] = $message['date'];
+        $deliveryText = '';
+        $clientText = '';
+        $orderItems = [];
+        $totalSum = 0;
+        $deliverySum = 0;
+
+        $linkBlock = $main->findOneOrFalse('a:contains("Перейти в заказ ")');
+        if ($linkBlock) {
+            $link = $linkBlock->getAttribute('href');
+            $orderDetails['orderLink'] = $link;
+        }
+
+        $deliveryLabel = $main->findOne('p:contains("Доставить")');
+        $pickupLabel = $main->findOne('p:contains("Самовывоз")');
 
-            $commentBlock = $main->findOne('p:contains("Комментарий")');
-            if ($commentBlock && $commentBlock->nextNonWhitespaceSibling()) {
-                $commentBlock = $commentBlock->nextNonWhitespaceSibling();
-                $commentText = preg_replace('/\s+/', ' ', $commentBlock->innerText());
-                $orderDetails['comment'] = trim($commentText);
+        $targetLabel = $deliveryLabel ?? $pickupLabel;
+        $labelPrefix = $deliveryLabel ? 'Доставка:' : ($pickupLabel ? 'Самовывоз:' : null);
+
+        if ($targetLabel && $targetLabel->nextNonWhitespaceSibling()) {
+            $infoBlock = $targetLabel->nextNonWhitespaceSibling();
+            $textParts = [];
+
+            $lines = explode('<br>', $infoBlock->innerHtml());
+
+            foreach ($lines as $line) {
+                $clean = trim(strip_tags($line));
+                if ($clean !== '') {
+                    $textParts[] = $clean;
+                }
             }
 
-            $clientBlock = $main->findOne('p:contains("Клиент")');
-            $senderBlock = $main->findOne('p:contains("Отправитель")');
+            if (!empty($textParts)) {
+                $deliveryText = $labelPrefix . ' ' . $textParts[0];
 
-            if ($clientBlock && $clientBlock->nextNonWhitespaceSibling()) {
-                $clientBlock = $clientBlock->nextNonWhitespaceSibling();
-                $clientText = "Клиент: " . strip_tags($clientBlock->innerText());
-                $phoneLink = $clientBlock->find('a', 0);
-                if ($phoneLink) {
-                    $clientText .= ' ' . preg_replace('/tel:/', ' ', $phoneLink->getAttribute('href'));
+                if (isset($textParts[1])) {
+                    $deliveryText .= ' ' . $textParts[1];
                 }
-            } elseif ($senderBlock && $senderBlock->nextNonWhitespaceSibling()) {
-                $senderBlock = $senderBlock->nextNonWhitespaceSibling();
-                $clientText = "Отправитель: " . strip_tags($senderBlock->innerText());
-                $phoneLink = $senderBlock->find('a', 0);
-                if ($phoneLink) {
-                    $clientText .= ' ' . preg_replace('/tel:/', ' ', $phoneLink->getAttribute('href'));
+
+                if (isset($textParts[2])) {
+                    $deliveryText .= ', ' . $textParts[2];
                 }
             }
+        }
 
-            if ($clientText) {
-                $orderDetails['client'] = str_replace('Позвонить', '', $clientText);
-            }
+        if (!empty($deliveryText)) {
+            $deliveryText = preg_replace('/\s+/', ' ', $deliveryText);
+            $deliveryText = trim($deliveryText);
+            $orderDetails['delivery'] = $deliveryText;
+        }
 
-            $recipientBlock = $main->findOne('p:contains("Получатель")');
-            if ($recipientBlock && $recipientBlock->nextNonWhitespaceSibling()) {
-                $recipientBlock = $recipientBlock->nextNonWhitespaceSibling();
-                $recipientText = strip_tags(
-                        str_replace('Позвонить', '', $recipientBlock->innerText())
-                    ) . ' ' . preg_replace('/tel:/', ' ', $recipientBlock->find('a', 0)->getAttribute('href'));
-                $orderDetails['recipient'] = $recipientText;
-            }
+        $commentBlock = $main->findOne('p:contains("Комментарий")');
+        if ($commentBlock && $commentBlock->nextNonWhitespaceSibling()) {
+            $commentBlock = $commentBlock->nextNonWhitespaceSibling();
+            $commentText = preg_replace('/\s+/', ' ', $commentBlock->innerText());
+            $orderDetails['comment'] = trim($commentText);
+        }
+
+        $clientBlock = $main->findOne('p:contains("Клиент")');
+        $senderBlock = $main->findOne('p:contains("Отправитель")');
 
-            $itemsBlock = false;
-            if ($main->findOneOrFalse('table h2:contains("Детали заказа")') != false) {
-                $itemsBlock = $main->findOneOrFalse('table h2:contains("Детали заказа")');
-            } elseif ($main->findOneOrFalse('table p:contains("Детали заказа")') != false) {
-                $itemsBlock = $main->findOneOrFalse('table p:contains("Детали заказа")');
+        if ($clientBlock && $clientBlock->nextNonWhitespaceSibling()) {
+            $clientBlock = $clientBlock->nextNonWhitespaceSibling();
+            $clientText = "Клиент: " . strip_tags($clientBlock->innerText());
+            $phoneLink = $clientBlock->find('a', 0);
+            if ($phoneLink) {
+                $clientText .= ' ' . preg_replace('/tel:/', ' ', $phoneLink->getAttribute('href'));
             }
+        } elseif ($senderBlock && $senderBlock->nextNonWhitespaceSibling()) {
+            $senderBlock = $senderBlock->nextNonWhitespaceSibling();
+            $clientText = "Отправитель: " . strip_tags($senderBlock->innerText());
+            $phoneLink = $senderBlock->find('a', 0);
+            if ($phoneLink) {
+                $clientText .= ' ' . preg_replace('/tel:/', ' ', $phoneLink->getAttribute('href'));
+            }
+        }
 
-            if ($itemsBlock) {
-                $itemsTable = $itemsBlock->parentNode();
+        if ($clientText) {
+            $orderDetails['client'] = str_replace('Позвонить', '', $clientText);
+        }
 
-                $itemsRows = $itemsTable->find('tr');
-                foreach ($itemsRows as $itemsRow) {
-                    $itemData = [
-                        'name' => '',
-                        'count' => '',
-                        'price' => '',
-                    ];
+        $recipientBlock = $main->findOne('p:contains("Получатель")');
+        if ($recipientBlock && $recipientBlock->nextNonWhitespaceSibling()) {
+            $recipientBlock = $recipientBlock->nextNonWhitespaceSibling();
+            $recipientText = strip_tags(
+                    str_replace('Позвонить', '', $recipientBlock->innerText())
+                ) . ' ' . preg_replace('/tel:/', ' ', $recipientBlock->find('a', 0)->getAttribute('href'));
+            $orderDetails['recipient'] = $recipientText;
+        }
 
-                    // Извлекаем название и количество из второго <td>
-                    $tds = $itemsRow->find('td');
-                    if (count($tds) >= 2) {
-                        $rawName = trim(str_replace("\u{00A0}", ' ', strip_tags(preg_replace('/\s+/', ' ', $tds[1]->find('p', 0)->innerText()))));
-                        $itemData['name'] = self::normalizeArticleInName($rawName);
-                        $itemData['count'] = trim(str_replace(["\u{00A0}", 'шт.'], '', strip_tags(preg_replace('/\s+/', '', $tds[1]->find('p', 1)->innerText()))));
-                    }
-                    // Извлекаем цену из третьего <td>
-                    if (count($tds) >= 3) {
-                        $itemData['price'] = (float)trim(str_replace(["\u{00A0}", '₽', ' '], '', strip_tags(preg_replace('/\s+/', ' ', $tds[2]->find('p', 0)->innerText()))));
-                    }
-                    // Добавляем данные в массив
-                    $orderItems[] = $itemData;
-                }
+        $itemsBlock = false;
+        if ($main->findOneOrFalse('table h2:contains("Детали заказа")') != false) {
+            $itemsBlock = $main->findOneOrFalse('table h2:contains("Детали заказа")');
+        } elseif ($main->findOneOrFalse('p:contains("Детали заказа")') != false) {
+            $itemsBlock = $main->findOneOrFalse('p:contains("Детали заказа")');
+        }
+
+        if ($itemsBlock) {
+            $itemsTable = $itemsBlock->parentNode();
+
+            $itemsRows = $itemsTable->find('tr');
+            foreach ($itemsRows as $itemsRow) {
+                $itemData = [
+                    'name' => '',
+                    'count' => '',
+                    'price' => '',
+                ];
 
-                $sumBlock = $main->findOneOrFalse('p:contains("Итого оплачено")');
+                // Извлекаем название и количество из второго <td>
+                $tds = $itemsRow->find('td');
+                if (count($tds) >= 2) {
+                    $rawName = trim(str_replace("\u{00A0}", ' ', strip_tags(preg_replace('/\s+/', ' ', $tds[1]->find('p', 0)->innerText()))));
+                    $itemData['name'] = self::normalizeArticleInName($rawName);
+                    $itemData['count'] = trim(str_replace(["\u{00A0}", 'шт.'], '', strip_tags(preg_replace('/\s+/', '', $tds[1]->find('p', 1)->innerText()))));
+                }
+                // Извлекаем цену из третьего <td>
+                if (count($tds) >= 3) {
+                    $itemData['price'] = (float)trim(str_replace(["\u{00A0}", '₽', ' '], '', strip_tags(preg_replace('/\s+/', ' ', $tds[2]->find('p', 0)->innerText()))));
+                }
+                // Добавляем данные в массив
+                $orderItems[] = $itemData;
+            }
 
-                if ($sumBlock) {
-                    $sumBlock = $sumBlock->parentNode()->nextNonWhitespaceSibling();
-                    $totalSum = (float)trim
-                    (
-                        str_replace(
-                            ["\u{00A0}", '₽', ' '],
-                            '',
-                            strip_tags
+            $sumBlock = $main->findOneOrFalse('p:contains("Итого оплачено")');
+
+            if ($sumBlock) {
+                $sumBlock = $sumBlock->parentNode()->nextNonWhitespaceSibling();
+                $totalSum = (float)trim
+                (
+                    str_replace(
+                        ["\u{00A0}", '₽', ' '],
+                        '',
+                        strip_tags
+                        (
+                            preg_replace
                             (
-                                preg_replace
-                                (
-                                    '/\s+/',
-                                    ' ',
-                                    $sumBlock->innerText()
-                                )
+                                '/\s+/',
+                                ' ',
+                                $sumBlock->innerText()
                             )
                         )
-                    );
-                }
+                    )
+                );
+            }
 
-                $devSumBlock = $main->findOneOrFalse('p:contains("Доставка")');
+            $devSumBlock = $main->findOneOrFalse('p:contains("Доставка")');
 
-                if ($devSumBlock) {
-                    $devSumBlock = $devSumBlock->parentNode()->nextNonWhitespaceSibling();
-                    $deliverySum = (float)trim(
-                        str_replace(["\u{00A0}", '₽', ' '],
-                            '',
-                            strip_tags(preg_replace('/\s+/', ' ', $devSumBlock->innerText())))
-                    );
-                }
+            if ($devSumBlock) {
+                $devSumBlock = $devSumBlock->parentNode()->nextNonWhitespaceSibling();
+                $deliverySum = (float)trim(
+                    str_replace(["\u{00A0}", '₽', ' '],
+                        '',
+                        strip_tags(preg_replace('/\s+/', ' ', $devSumBlock->innerText())))
+                );
             }
-            $orderDetails['items'] = $orderItems;
-            $orderDetails['deliverySum'] = $deliverySum;
-            $orderDetails['totalSum'] = $totalSum;
-            $order[$orderNumber] = $orderDetails;
-            return $order;
         }
+        $orderDetails['items'] = $orderItems;
+        $orderDetails['deliverySum'] = $deliverySum;
+        $orderDetails['totalSum'] = $totalSum;
+        $order[$orderNumber] = $orderDetails;
+
+        if (empty($order)) {
+            Yii::warning('Не удалось извлечь данные заказа из HTML', __METHOD__);
+        } else {
+            Yii::info('Успешно распарен заказ №' . $orderNumber, __METHOD__);
+        }
+
+        return $order;
+
+        // Если дошли до сюда, значит HTML не содержит валидный контент для парсинга
+        Yii::error('HTML не содержит валидный контент для парсинга заказов', __METHOD__);
         return [];
     }
 
diff --git a/erp24/views/marketplace-orders/test-article-processing.php b/erp24/views/marketplace-orders/test-article-processing.php
new file mode 100644 (file)
index 0000000..4657f42
--- /dev/null
@@ -0,0 +1,158 @@
+<?php
+
+use yii\helpers\Html;
+
+/* @var $this yii\web\View */
+/* @var $testResults array */
+
+$this->title = 'Тестирование обработки артикулов';
+$this->params['breadcrumbs'][] = ['label' => 'Заказы маркетплейсов', 'url' => ['index']];
+$this->params['breadcrumbs'][] = ['label' => 'Тестирование разбора заказов', 'url' => ['test-order-parsing']];
+$this->params['breadcrumbs'][] = $this->title;
+?>
+
+<div class="marketplace-orders-test-article-processing p-4">
+
+    <h1><?= Html::encode($this->title) ?></h1>
+
+    <p>
+        Результаты тестирования методов обработки артикулов в названиях товаров.
+    </p>
+
+    <div class="row">
+        <div class="col-md-12">
+            <?php
+            $normalizeTests = array_filter($testResults, function($test) {
+                return $test['method'] === 'normalizeArticleInName';
+            });
+            $processTests = array_filter($testResults, function($test) {
+                return $test['method'] === 'processDisplayName';
+            });
+
+            $allPassed = array_reduce($testResults, function($carry, $test) {
+                return $carry && $test['passed'];
+            }, true);
+            ?>
+
+            <div class="alert alert-<?= $allPassed ? 'success' : 'warning' ?>">
+                <strong>Общий результат:</strong>
+                <?= $allPassed ? 'Все тесты пройдены!' : 'Некоторые тесты не пройдены.' ?>
+            </div>
+
+            <div class="accordion" id="testResultsAccordion">
+
+                <!-- Тесты для normalizeArticleInName -->
+                <div class="accordion-item">
+                    <h2 class="accordion-header">
+                        <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#normalizeTests">
+                            Метод normalizeArticleInName (убирает дефис из артикула при разборе заказов)
+                            <span class="badge bg-<?= count(array_filter($normalizeTests, fn($t) => $t['passed'])) === count($normalizeTests) ? 'success' : 'warning' ?> ms-2">
+                                <?= count(array_filter($normalizeTests, fn($t) => $t['passed'])) ?>/<?= count($normalizeTests) ?> пройдено
+                            </span>
+                        </button>
+                    </h2>
+                    <div id="normalizeTests" class="accordion-collapse collapse show" data-bs-parent="#testResultsAccordion">
+                        <div class="accordion-body">
+                            <div class="table-responsive">
+                                <table class="table table-striped">
+                                    <thead>
+                                        <tr>
+                                            <th>Входные данные</th>
+                                            <th>Ожидаемый результат</th>
+                                            <th>Фактический результат</th>
+                                            <th>Статус</th>
+                                        </tr>
+                                    </thead>
+                                    <tbody>
+                                        <?php foreach ($normalizeTests as $test): ?>
+                                            <tr class="<?= $test['passed'] ? 'table-success' : 'table-danger' ?>">
+                                                <td><code><?= Html::encode($test['input']) ?></code></td>
+                                                <td><code><?= Html::encode($test['expected']) ?></code></td>
+                                                <td><code><?= Html::encode($test['actual']) ?></code></td>
+                                                <td>
+                                                    <?php if ($test['passed']): ?>
+                                                        <span class="badge bg-success">✓ Пройден</span>
+                                                    <?php else: ?>
+                                                        <span class="badge bg-danger">✗ Не пройден</span>
+                                                    <?php endif; ?>
+                                                </td>
+                                            </tr>
+                                        <?php endforeach; ?>
+                                    </tbody>
+                                </table>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+
+                <!-- Тесты для processDisplayName -->
+                <div class="accordion-item">
+                    <h2 class="accordion-header">
+                        <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#processTests">
+                            Метод processDisplayName (добавляет дефис в артикул при формировании ответа)
+                            <span class="badge bg-<?= count(array_filter($processTests, fn($t) => $t['passed'])) === count($processTests) ? 'success' : 'warning' ?> ms-2">
+                                <?= count(array_filter($processTests, fn($t) => $t['passed'])) ?>/<?= count($processTests) ?> пройдено
+                            </span>
+                        </button>
+                    </h2>
+                    <div id="processTests" class="accordion-collapse collapse" data-bs-parent="#testResultsAccordion">
+                        <div class="accordion-body">
+                            <div class="table-responsive">
+                                <table class="table table-striped">
+                                    <thead>
+                                        <tr>
+                                            <th>Входные данные</th>
+                                            <th>Ожидаемый результат</th>
+                                            <th>Фактический результат</th>
+                                            <th>Статус</th>
+                                        </tr>
+                                    </thead>
+                                    <tbody>
+                                        <?php foreach ($processTests as $test): ?>
+                                            <tr class="<?= $test['passed'] ? 'table-success' : 'table-danger' ?>">
+                                                <td><code><?= Html::encode($test['input']) ?></code></td>
+                                                <td><code><?= Html::encode($test['expected']) ?></code></td>
+                                                <td><code><?= Html::encode($test['actual']) ?></code></td>
+                                                <td>
+                                                    <?php if ($test['passed']): ?>
+                                                        <span class="badge bg-success">✓ Пройден</span>
+                                                    <?php else: ?>
+                                                        <span class="badge bg-danger">✗ Не пройден</span>
+                                                    <?php endif; ?>
+                                                </td>
+                                            </tr>
+                                        <?php endforeach; ?>
+                                    </tbody>
+                                </table>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+
+            </div>
+
+            <div class="mt-4">
+                <h4>Описание методов:</h4>
+                <ul class="list-group">
+                    <li class="list-group-item">
+                        <strong>normalizeArticleInName()</strong> - используется при разборе заказов из email.
+                        Если в названии товара есть артикул в скобках с дефисом (FW-0076), убирает дефис (FW0076).
+                        Это обеспечивает корректное сопоставление с товарами в базе данных.
+                    </li>
+                    <li class="list-group-item">
+                        <strong>processDisplayName()</strong> - используется при формировании ответа API.
+                        Если в названии товара есть артикул в скобках без дефиса (FW0076), добавляет дефис (FW-0076).
+                        Это обеспечивает красивый формат отображения для внешних систем.
+                    </li>
+                </ul>
+            </div>
+
+            <div class="mt-4">
+                <?= Html::a('Вернуться к тестированию разбора заказов', ['test-order-parsing'], ['class' => 'btn btn-secondary']) ?>
+                <?= Html::a('Вернуться к списку заказов', ['index'], ['class' => 'btn btn-primary']) ?>
+            </div>
+        </div>
+    </div>
+
+</div>
+
diff --git a/erp24/views/marketplace-orders/test-order-parsing.php b/erp24/views/marketplace-orders/test-order-parsing.php
new file mode 100644 (file)
index 0000000..d06bea9
--- /dev/null
@@ -0,0 +1,226 @@
+<?php
+
+use yii\helpers\Html;
+use yii\widgets\ActiveForm;
+
+/* @var $this yii\web\View */
+/* @var $model \yii\base\DynamicModel */
+/* @var $parsedOrder array */
+/* @var $htmlBody string */
+/* @var $parsingError string */
+
+$this->title = 'Тестирование разбора заказов';
+$this->params['breadcrumbs'][] = ['label' => 'Заказы маркетплейсов', 'url' => ['index']];
+$this->params['breadcrumbs'][] = $this->title;
+?>
+
+<div class="marketplace-orders-test-order-parsing p-4">
+
+    <h1><?= Html::encode($this->title) ?></h1>
+
+    <p>
+        Этот инструмент позволяет протестировать обработку артикулов при разборе заказов.
+        Вставьте HTML тела письма с заказом и нажмите "Разобрать заказ".
+    </p>
+
+    <div class="row">
+        <div class="col-md-6">
+            <?php $form = ActiveForm::begin(['method' => 'post']); ?>
+
+                <?= $form->field($model, 'html_body')->textarea([
+                    'rows' => 20,
+                    'placeholder' => 'Вставьте HTML тела письма с заказом...'
+                ])->label('HTML тела письма') ?>
+
+                <div class="form-group">
+                    <?= Html::submitButton('Разобрать заказ', ['class' => 'btn btn-success']) ?>
+                </div>
+
+            <?php ActiveForm::end(); ?>
+        </div>
+
+        <div class="col-md-6">
+            <?php if (Yii::$app->request->isPost): ?>
+                <?php if ($parsingError): ?>
+                    <div class="alert alert-danger">
+                        <strong>Ошибка разбора:</strong> <?= Html::encode($parsingError) ?>
+                    </div>
+                <?php elseif ($parsedOrder && !empty($parsedOrder)): ?>
+                    <div class="alert alert-success">
+                        Заказ успешно разобран!
+                    </div>
+
+                    <h4>Информация о заказе:</h4>
+                    <pre style="background: #f8f9fa; padding: 10px; border-radius: 4px; font-size: 12px; overflow-x: auto; max-height: 400px;">
+<?= Html::encode(json_encode($parsedOrder, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)) ?>
+                    </pre>
+
+                    <?php
+                    $firstOrder = reset($parsedOrder);
+                    if (isset($firstOrder['items']) && is_array($firstOrder['items'])):
+                    ?>
+                        <h4>Элементы заказа:</h4>
+                        <div class="table-responsive">
+                            <table class="table table-striped table-bordered">
+                                <thead>
+                                    <tr>
+                                        <th>Название товара</th>
+                                        <th>Количество</th>
+                                        <th>Цена</th>
+                                    </tr>
+                                </thead>
+                                <tbody>
+                                    <?php foreach ($firstOrder['items'] as $item): ?>
+                                        <tr>
+                                            <td>
+                                                <?= Html::encode($item['name']) ?>
+                                                <?php
+                                                // Проверяем, есть ли артикул в названии
+                                                if (preg_match('/\(([^)]+)\)/', $item['name'], $matches)) {
+                                                    $article = $matches[1];
+                                                    echo '<br><small class="text-muted">Артикул: ' . Html::encode($article) . '</small>';
+                                                }
+                                                ?>
+                                            </td>
+                                            <td><?= Html::encode($item['count']) ?></td>
+                                            <td><?= Html::encode($item['price']) ?> ₽</td>
+                                        </tr>
+                                    <?php endforeach; ?>
+                                </tbody>
+                            </table>
+                        </div>
+                    <?php endif; ?>
+                <?php else: ?>
+                    <div class="alert alert-warning">
+                        Заказы не найдены в предоставленном HTML. Возможно, структура HTML не соответствует ожидаемой или заказы отсутствуют.
+                    </div>
+                <?php endif; ?>
+            <?php else: ?>
+                <div class="alert alert-info">
+                    Заполните форму слева и нажмите "Разобрать заказ" для тестирования.
+                </div>
+            <?php endif; ?>
+        </div>
+    </div>
+
+    <hr>
+
+    <div class="row">
+        <div class="col-md-12">
+            <h3>Примеры HTML для тестирования:</h3>
+            <div class="accordion" id="examplesAccordion">
+
+                <div class="accordion-item">
+                    <h2 class="accordion-header">
+                        <button class="accordion-button" type="button" data-bs-toggle="collapse" data-bs-target="#example1">
+                            Пример 1: Реальная структура Flowwow (с артикулом FW-0076)
+                        </button>
+                    </h2>
+                    <div id="example1" class="accordion-collapse collapse show" data-bs-parent="#examplesAccordion">
+                        <div class="accordion-body">
+                            <button class="btn btn-sm btn-outline-primary mb-2" onclick="copyToTextarea(this.nextElementSibling)">Копировать в форму</button>
+                            <pre style="background: #f8f9fa; padding: 10px; border-radius: 4px; font-size: 11px; overflow-x: auto; white-space: pre-wrap;"><code>&lt;html&gt;
+&lt;body&gt;
+&lt;table&gt;
+&lt;p style="font-weight: 700;"&gt;Детали заказа &lt;/p&gt;
+&lt;tr style="margin-bottom: 12px;"&gt;
+&lt;td style="width: 56px; padding-right:12px;"&gt;
+&lt;img src="https://example.com/image.jpg" width="" alt="img" border="0" style="max-width:56px;"&gt;
+&lt;/td&gt;
+&lt;td&gt;
+&lt;p style="margin-bottom: 4px;"&gt;Букет из 11 розовых пионов (FW-0076)&lt;/p&gt;
+&lt;p style="color: #8C8C8C;"&gt;1 шт. &lt;/p&gt;
+&lt;/td&gt;
+&lt;td&gt;
+&lt;p style="text-align: right;"&gt;4500&#8381; &lt;/p&gt;
+&lt;/td&gt;
+&lt;/tr&gt;
+&lt;/table&gt;
+&lt;/body&gt;
+&lt;/html&gt;</code></pre>
+                        </div>
+                    </div>
+                </div>
+
+                <div class="accordion-item">
+                    <h2 class="accordion-header">
+                        <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#example2">
+                            Пример 2: С артикулом FW0076 (уже без разделителя)
+                        </button>
+                    </h2>
+                    <div id="example2" class="accordion-collapse collapse" data-bs-parent="#examplesAccordion">
+                        <div class="accordion-body">
+                            <button class="btn btn-sm btn-outline-primary mb-2" onclick="copyToTextarea(this.nextElementSibling)">Копировать в форму</button>
+                            <pre style="background: #f8f9fa; padding: 10px; border-radius: 4px; font-size: 11px; overflow-x: auto; white-space: pre-wrap;"><code>&lt;html&gt;
+&lt;body&gt;
+&lt;table&gt;
+&lt;p style="font-weight: 700;"&gt;Детали заказа &lt;/p&gt;
+&lt;tr style="margin-bottom: 12px;"&gt;
+&lt;td style="width: 56px; padding-right:12px;"&gt;
+&lt;img src="https://example.com/image.jpg" width="" alt="img" border="0" style="max-width:56px;"&gt;
+&lt;/td&gt;
+&lt;td&gt;
+&lt;p style="margin-bottom: 4px;"&gt;Букет из 11 розовых пионов (FW0076)&lt;/p&gt;
+&lt;p style="color: #8C8C8C;"&gt;1 шт. &lt;/p&gt;
+&lt;/td&gt;
+&lt;td&gt;
+&lt;p style="text-align: right;"&gt;4500&#8381; &lt;/p&gt;
+&lt;/td&gt;
+&lt;/tr&gt;
+&lt;/table&gt;
+&lt;/body&gt;
+&lt;/html&gt;</code></pre>
+                        </div>
+                    </div>
+                </div>
+
+                <div class="accordion-item">
+                    <h2 class="accordion-header">
+                        <button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#example3">
+                            Пример 3: Без артикула
+                        </button>
+                    </h2>
+                    <div id="example3" class="accordion-collapse collapse" data-bs-parent="#examplesAccordion">
+                        <div class="accordion-body">
+                            <button class="btn btn-sm btn-outline-primary mb-2" onclick="copyToTextarea(this.nextElementSibling)">Копировать в форму</button>
+                            <pre style="background: #f8f9fa; padding: 10px; border-radius: 4px; font-size: 11px; overflow-x: auto; white-space: pre-wrap;"><code>&lt;html&gt;
+&lt;body&gt;
+&lt;table&gt;
+&lt;p style="font-weight: 700;"&gt;Детали заказа &lt;/p&gt;
+&lt;tr style="margin-bottom: 12px;"&gt;
+&lt;td style="width: 56px; padding-right:12px;"&gt;
+&lt;img src="https://example.com/image.jpg" width="" alt="img" border="0" style="max-width:56px;"&gt;
+&lt;/td&gt;
+&lt;td&gt;
+&lt;p style="margin-bottom: 4px;"&gt;Букет из 11 розовых пионов&lt;/p&gt;
+&lt;p style="color: #8C8C8C;"&gt;1 шт. &lt;/p&gt;
+&lt;/td&gt;
+&lt;td&gt;
+&lt;p style="text-align: right;"&gt;4500&#8381; &lt;/p&gt;
+&lt;/td&gt;
+&lt;/tr&gt;
+&lt;/table&gt;
+&lt;/body&gt;
+&lt;/html&gt;</code></pre>
+                        </div>
+                    </div>
+                </div>
+
+            </div>
+        </div>
+    </div>
+
+    <div class="mt-4 text-center">
+        <?= Html::a('Посмотреть результаты тестирования методов обработки артикулов', ['test-article-processing'], ['class' => 'btn btn-info']) ?>
+        <?= Html::a('Вернуться к списку заказов', ['index'], ['class' => 'btn btn-primary ms-2']) ?>
+    </div>
+
+</div>
+
+<script>
+function copyToTextarea(preElement) {
+    const codeElement = preElement.querySelector('code');
+    const text = codeElement.textContent || codeElement.innerText;
+    document.querySelector('textarea[name="DynamicModel[html_body]"]').value = text;
+}
+</script>