--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace yii_app\services\automark;
+
+class RuleBasedParser
+{
+ private const SPECIES_SREZY = [
+ 'роза' => 'Роза', 'хризантема' => 'Хризантема', 'гербера' => 'Гербера',
+ 'тюльпан' => 'Тюльпан', 'лилия' => 'Лилия', 'пион' => 'Пион',
+ 'эустома' => 'Эустома', 'альстромерия' => 'Альстромерия',
+ 'гипсофила' => 'Гипсофила', 'ирис' => 'Ирис', 'гвоздика' => 'Гвоздика',
+ 'нарцисс' => 'Нарцисс', 'фрезия' => 'Фрезия', 'лизиантус' => 'Лизиантус',
+ ];
+
+ private const SORT_KEYWORDS = [
+ 'premium' => 'Premium', 'экстра' => 'Экстра', 'extra' => 'Extra',
+ 'select' => 'Select', 'стандарт' => 'Стандарт', 'премиум' => 'Премиум',
+ 'супер' => 'Супер', 'элит' => 'Элит',
+ ];
+
+ private const COLOR_KEYWORDS = [
+ 'красная' => 'Красная', 'красный' => 'Красный',
+ 'белая' => 'Белая', 'белый' => 'Белый',
+ 'розовая' => 'Розовая', 'розовый' => 'Розовый',
+ 'желтая' => 'Желтая', 'желтый' => 'Желтый',
+ 'оранжевая' => 'Оранжевая', 'оранжевый' => 'Оранжевый',
+ 'фиолетовая' => 'Фиолетовая', 'фиолетовый' => 'Фиолетовый',
+ 'синяя' => 'Синяя', 'синий' => 'Синий',
+ 'кремовая' => 'Кремовая', 'кремовый' => 'Кремовый',
+ 'бордовая' => 'Бордовая', 'бордовый' => 'Бордовый',
+ 'микс' => 'Микс', 'коралловая' => 'Коралловая',
+ ];
+
+ private const POTTED_KEYWORDS = ['горшок', 'горшечн', 'кашпо', 'вазон'];
+
+ private const PACKAGING_KEYWORDS = ['пакет', 'коробк', 'упаков', 'лента', 'бумага', 'сетка', 'сизаль'];
+
+ public function parse(string $name): ?ParseResult
+ {
+ $nameLower = mb_strtolower($name);
+
+ $category = $this->detectCategory($nameLower);
+ $species = $this->detectSpecies($nameLower);
+ $sort = $this->detectSort($nameLower);
+ $color = $this->detectColor($nameLower);
+ $size = $this->detectSize($name);
+
+ if ($category === null && $species === null) {
+ return null;
+ }
+
+ if ($species !== null && $category === null) {
+ $category = 'Срезы';
+ }
+
+ $confidence = $this->calcConfidence($category, $species, $sort, $color, $size);
+
+ return new ParseResult(
+ category: $category,
+ subcategory: null,
+ species: $species,
+ sort: $sort,
+ type: null,
+ size: $size,
+ color: $color,
+ confidence: $confidence,
+ method: 'rule',
+ );
+ }
+
+ private function detectCategory(string $nameLower): ?string
+ {
+ foreach (self::POTTED_KEYWORDS as $kw) {
+ if (str_contains($nameLower, $kw)) {
+ return 'Горшечные';
+ }
+ }
+ foreach (self::PACKAGING_KEYWORDS as $kw) {
+ if (str_contains($nameLower, $kw)) {
+ return 'Упаковка';
+ }
+ }
+ return null;
+ }
+
+ private function detectSpecies(string $nameLower): ?string
+ {
+ foreach (self::SPECIES_SREZY as $keyword => $label) {
+ if (str_contains($nameLower, $keyword)) {
+ return $label;
+ }
+ }
+ return null;
+ }
+
+ private function detectSort(string $nameLower): ?string
+ {
+ foreach (self::SORT_KEYWORDS as $keyword => $label) {
+ if (str_contains($nameLower, $keyword)) {
+ return $label;
+ }
+ }
+ return null;
+ }
+
+ private function detectColor(string $nameLower): ?string
+ {
+ foreach (self::COLOR_KEYWORDS as $keyword => $label) {
+ if (str_contains($nameLower, $keyword)) {
+ return $label;
+ }
+ }
+ return null;
+ }
+
+ private function detectSize(string $name): ?int
+ {
+ if (preg_match('/(\d+)\s*(?:см|cm|СМ|CM)/iu', $name, $m)) {
+ return (int) $m[1];
+ }
+ return null;
+ }
+
+ private function calcConfidence(
+ ?string $category,
+ ?string $species,
+ ?string $sort,
+ ?string $color,
+ ?int $size
+ ): float {
+ $score = 0.5;
+ if ($category !== null) $score += 0.2;
+ if ($species !== null) $score += 0.15;
+ if ($color !== null) $score += 0.1;
+ if ($size !== null) $score += 0.05;
+ if ($sort !== null) $score += 0.05;
+ return min(1.0, $score);
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace tests\unit\services\automark;
+
+use Codeception\Test\Unit;
+use yii_app\services\automark\RuleBasedParser;
+use yii_app\services\automark\ParseResult;
+
+/**
+ * @covers \yii_app\services\automark\RuleBasedParser
+ */
+class RuleBasedParserTest extends Unit
+{
+ private RuleBasedParser $parser;
+
+ protected function setUp(): void
+ {
+ $this->parser = new RuleBasedParser();
+ }
+
+ public function testParsesRoseWithSizeAndColor(): void
+ {
+ $result = $this->parser->parse('Роза Premium 50см Красная');
+
+ $this->assertInstanceOf(ParseResult::class, $result);
+ $this->assertSame('Срезы', $result->category);
+ $this->assertSame('Роза', $result->species);
+ $this->assertSame('Premium', $result->sort);
+ $this->assertSame(50, $result->size);
+ $this->assertSame('Красная', $result->color);
+ $this->assertGreaterThanOrEqual(0.9, $result->confidence);
+ $this->assertSame('rule', $result->method);
+ }
+
+ public function testParsesPottedPlant(): void
+ {
+ $result = $this->parser->parse('Орхидея Фаленопсис горшок 12');
+
+ $this->assertSame('Горшечные', $result->category);
+ $this->assertGreaterThanOrEqual(0.7, $result->confidence);
+ }
+
+ public function testParsesPackaging(): void
+ {
+ $result = $this->parser->parse('Пакет целлофановый 60х80');
+
+ $this->assertSame('Упаковка', $result->category);
+ $this->assertGreaterThanOrEqual(0.7, $result->confidence);
+ }
+
+ public function testExtractsSizeInCm(): void
+ {
+ $result = $this->parser->parse('Хризантема Экстра 70 см Белая');
+
+ $this->assertSame(70, $result->size);
+ $this->assertSame('Белая', $result->color);
+ $this->assertSame('Хризантема', $result->species);
+ }
+
+ public function testReturnsNullForUnknownName(): void
+ {
+ $result = $this->parser->parse('XYZ-неизвестный-товар-123');
+
+ $this->assertNull($result);
+ }
+
+ public function testChrysanthemumIsSrezy(): void
+ {
+ $result = $this->parser->parse('Хризантема Белая 60см');
+
+ $this->assertSame('Срезы', $result->category);
+ $this->assertSame('Хризантема', $result->species);
+ }
+}