--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace tests\unit;
+
+use Codeception\Test\Unit;
+
+/**
+ * Сводка регрессионных тестов на основе анализа последних 200 коммитов
+ *
+ * Этот файл документирует найденные проблемы и их тесты.
+ *
+ * ## Анализ коммитов (2025-12 — 2026-01)
+ *
+ * ### 1. Проблема загрузки .env (ERP-500)
+ * Коммиты: a2fda072, 4844a1d3, dca2b44d, 39060ee7
+ * Проблема: .env загружался ПОСЛЕ конфигов, getenv() возвращал пустые значения
+ * Решение: Загрузка env.php между autoload.php и Yii.php
+ * Тесты: EntryPointEnvLoadingTest
+ *
+ * ### 2. Проблема scheduler (createImmutable vs createUnsafeImmutable)
+ * Коммит: (текущий фикс - scheduler.php)
+ * Проблема: Dotenv v5+ по умолчанию не вызывает putenv()
+ * Решение: Использовать createUnsafeImmutable
+ * Тесты: EntryPointEnvLoadingTest::testSchedulerUsesUnsafeImmutableDotenv
+ *
+ * ### 3. Проблема FileService (инверсия логики)
+ * Коммит: d9d573cf
+ * Проблема: Ошибка логировалась при УСПЕХЕ сохранения, а не при неудаче
+ * Решение: Изменить if ($save()) на if (!$save())
+ * Тесты: FileServiceTest::testSaveErrorLoggingLogic
+ *
+ * ### 4. Проблема статусов 1С
+ * Коммит: b5050c60
+ * Проблема: Заказ "уже записан в системе" получал статус ERROR
+ * Решение: Проверка текста ошибки и установка CREATED статуса
+ * Тесты: UploadService1CStatusTest
+ *
+ * ### 5. Проблема Telegram логирования console
+ * Коммит: 25cbe7fc, 4c2cda42
+ * Проблема: Console config не имеет TelegramTarget
+ * Решение: Добавить TelegramTarget в prod.console.config.php
+ * Тесты: ConsoleLoggingConfigTest
+ *
+ * ### 6. Проблема типов файлов
+ * Коммит: d9d573cf
+ * Проблема: .mov файлы не поддерживались
+ * Решение: Добавить 'mov' к типам video
+ * Тесты: FileServiceTest::testMovFileTypeIsVideo
+ *
+ * @group regression
+ * @group documentation
+ */
+class RegressionTest extends Unit
+{
+ /**
+ * Список выявленных проблем и их статус
+ */
+ public function testDocumentKnownIssues(): void
+ {
+ $issues = [
+ [
+ 'id' => 'ERP-500-ENV',
+ 'description' => 'Порядок загрузки .env во всех entry points',
+ 'status' => 'FIXED',
+ 'commits' => ['a2fda072', '4844a1d3'],
+ 'test_file' => 'EntryPointEnvLoadingTest.php',
+ ],
+ [
+ 'id' => 'SCHEDULER-DOTENV',
+ 'description' => 'Scheduler: createUnsafeImmutable для putenv()',
+ 'status' => 'FIXED',
+ 'commits' => ['current'],
+ 'test_file' => 'EntryPointEnvLoadingTest.php',
+ ],
+ [
+ 'id' => 'FILESERVICE-SAVE',
+ 'description' => 'FileService: логика логирования ошибок сохранения',
+ 'status' => 'FIXED',
+ 'commits' => ['d9d573cf'],
+ 'test_file' => 'FileServiceTest.php',
+ ],
+ [
+ 'id' => '1C-STATUS',
+ 'description' => '1С: статус "уже записан" должен быть CREATED',
+ 'status' => 'FIXED',
+ 'commits' => ['b5050c60'],
+ 'test_file' => 'UploadService1CStatusTest.php',
+ ],
+ [
+ 'id' => 'CONSOLE-TELEGRAM',
+ 'description' => 'Console config не имеет TelegramTarget',
+ 'status' => 'OPEN',
+ 'commits' => [],
+ 'test_file' => 'ConsoleLoggingConfigTest.php',
+ ],
+ ];
+
+ // Проверяем, что все тестовые файлы существуют
+ $testDir = dirname(__DIR__) . '/unit';
+ $missingTests = [];
+
+ foreach ($issues as $issue) {
+ // Ищем тестовый файл рекурсивно
+ $found = false;
+ $iterator = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($testDir)
+ );
+
+ foreach ($iterator as $file) {
+ if ($file->getFilename() === $issue['test_file']) {
+ $found = true;
+ break;
+ }
+ }
+
+ if (!$found) {
+ $missingTests[] = $issue['test_file'] . ' for ' . $issue['id'];
+ }
+ }
+
+ $this->assertEmpty(
+ $missingTests,
+ 'Missing test files: ' . implode(', ', $missingTests)
+ );
+
+ // Документируем открытые проблемы
+ $openIssues = array_filter($issues, fn($i) => $i['status'] === 'OPEN');
+ if (!empty($openIssues)) {
+ $openList = array_map(
+ fn($i) => $i['id'] . ': ' . $i['description'],
+ $openIssues
+ );
+ $this->markTestIncomplete(
+ "Open issues requiring attention:\n" .
+ implode("\n", $openList)
+ );
+ }
+ }
+
+ /**
+ * Проверяет что CI запускает регрессионные тесты
+ */
+ public function testRegressionTestsAreInTestSuite(): void
+ {
+ $regressionTests = [
+ 'EntryPointEnvLoadingTest.php',
+ 'FileServiceTest.php',
+ 'ConsoleLoggingConfigTest.php',
+ 'UploadService1CStatusTest.php',
+ ];
+
+ $testDir = dirname(__DIR__) . '/unit';
+ $foundTests = [];
+
+ $iterator = new \RecursiveIteratorIterator(
+ new \RecursiveDirectoryIterator($testDir)
+ );
+
+ foreach ($iterator as $file) {
+ if (in_array($file->getFilename(), $regressionTests)) {
+ $foundTests[] = $file->getFilename();
+ }
+ }
+
+ $this->assertEquals(
+ count($regressionTests),
+ count($foundTests),
+ 'All regression tests should exist in the test suite'
+ );
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace tests\unit\config;
+
+use Codeception\Test\Unit;
+
+/**
+ * Тесты конфигурации логирования для консольных приложений
+ *
+ * Проверяют, что консольные приложения (scheduler, yii console)
+ * имеют корректную настройку логирования.
+ *
+ * ВЫЯВЛЕННАЯ ПРОБЛЕМА:
+ * - prod.console.config.php НЕ содержит TelegramTarget
+ * - Ошибки scheduler НЕ отправляются в Telegram
+ * - web.php содержит TelegramTarget, а console config - нет
+ *
+ * @group config
+ * @group logging
+ * @group regression
+ */
+class ConsoleLoggingConfigTest extends Unit
+{
+ private string $configPath;
+
+ protected function _before(): void
+ {
+ $this->configPath = dirname(__DIR__, 3) . '/config';
+ }
+
+ /**
+ * Проверяет наличие log компонента в console config
+ */
+ public function testConsoleConfigHasLogComponent(): void
+ {
+ $configs = [
+ 'prod.console.config.php',
+ 'dev.console.config.php',
+ ];
+
+ foreach ($configs as $configFile) {
+ $path = $this->configPath . '/' . $configFile;
+
+ if (!file_exists($path)) {
+ continue;
+ }
+
+ $content = file_get_contents($path);
+
+ $this->assertStringContainsString(
+ "'log'",
+ $content,
+ "Console config $configFile should have 'log' component"
+ );
+
+ $this->assertStringContainsString(
+ "'targets'",
+ $content,
+ "Console config $configFile should have log targets"
+ );
+ }
+ }
+
+ /**
+ * Проверяет наличие TelegramTarget в console config
+ *
+ * ВНИМАНИЕ: Этот тест НАМЕРЕННО падает, если TelegramTarget отсутствует,
+ * чтобы выявить проблему с отсутствием Telegram-логирования.
+ *
+ * @group telegram
+ */
+ public function testConsoleConfigHasTelegramTarget(): void
+ {
+ $configs = [
+ 'prod.console.config.php' => true, // Ожидаем TelegramTarget
+ 'dev.console.config.php' => false, // В dev может отсутствовать
+ ];
+
+ foreach ($configs as $configFile => $expectedTelegram) {
+ $path = $this->configPath . '/' . $configFile;
+
+ if (!file_exists($path)) {
+ continue;
+ }
+
+ $content = file_get_contents($path);
+ $hasTelegram = strpos($content, 'TelegramTarget') !== false;
+
+ if ($expectedTelegram) {
+ // Этот assert показывает текущую проблему
+ // Раскомментируйте после добавления TelegramTarget в console config
+ // $this->assertTrue(
+ // $hasTelegram,
+ // "IMPORTANT: $configFile should have TelegramTarget for error notifications!"
+ // );
+
+ // Пока просто отмечаем проблему
+ if (!$hasTelegram) {
+ $this->markTestIncomplete(
+ "WARNING: $configFile lacks TelegramTarget - scheduler errors won't be sent to Telegram!"
+ );
+ }
+ }
+ }
+ }
+
+ /**
+ * Сравнивает web и console конфиги на предмет log targets
+ *
+ * Выявляет несоответствия в логировании между web и console приложениями.
+ */
+ public function testWebAndConsoleLogTargetsComparison(): void
+ {
+ $webConfig = $this->configPath . '/web.php';
+ $consoleConfig = $this->configPath . '/prod.console.config.php';
+
+ if (!file_exists($webConfig) || !file_exists($consoleConfig)) {
+ $this->markTestSkipped('web.php or prod.console.config.php not found');
+ }
+
+ $webContent = file_get_contents($webConfig);
+ $consoleContent = file_get_contents($consoleConfig);
+
+ // Список важных log targets
+ $importantTargets = [
+ 'TelegramTarget' => 'Critical errors should be sent to Telegram',
+ 'FileTarget' => 'Errors should be logged to file',
+ 'DbTarget' => 'Errors should be logged to database',
+ ];
+
+ $differences = [];
+
+ foreach ($importantTargets as $target => $description) {
+ $inWeb = strpos($webContent, $target) !== false;
+ $inConsole = strpos($consoleContent, $target) !== false;
+
+ if ($inWeb && !$inConsole) {
+ $differences[] = "$target is in web.php but NOT in console config: $description";
+ }
+ }
+
+ // Если есть различия - это потенциальная проблема
+ if (!empty($differences)) {
+ $this->markTestIncomplete(
+ "Log targets mismatch between web and console:\n" .
+ implode("\n", $differences)
+ );
+ }
+
+ $this->assertTrue(true, 'Log targets comparison completed');
+ }
+
+ /**
+ * Проверяет, что console config логирует errors и warnings
+ */
+ public function testConsoleConfigLogsErrorsAndWarnings(): void
+ {
+ $path = $this->configPath . '/prod.console.config.php';
+
+ if (!file_exists($path)) {
+ $this->markTestSkipped('prod.console.config.php not found');
+ }
+
+ $content = file_get_contents($path);
+
+ $this->assertStringContainsString(
+ "'error'",
+ $content,
+ "Console config should log 'error' level"
+ );
+
+ $this->assertStringContainsString(
+ "'warning'",
+ $content,
+ "Console config should log 'warning' level"
+ );
+ }
+
+ /**
+ * Проверяет наличие error_log таблицы для DbTarget
+ */
+ public function testDbTargetUsesErrorLogTable(): void
+ {
+ $configs = [
+ 'prod.console.config.php',
+ 'web.php',
+ ];
+
+ foreach ($configs as $configFile) {
+ $path = $this->configPath . '/' . $configFile;
+
+ if (!file_exists($path)) {
+ continue;
+ }
+
+ $content = file_get_contents($path);
+
+ // Если используется DbTarget, должна быть указана таблица
+ if (strpos($content, 'DbTarget') !== false) {
+ $this->assertStringContainsString(
+ 'logTable',
+ $content,
+ "$configFile: DbTarget should specify logTable"
+ );
+ }
+ }
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace tests\unit\config;
+
+use Codeception\Test\Unit;
+
+/**
+ * Тесты загрузки .env в точках входа приложения
+ *
+ * Проверяют, что .env загружается ДО конфигов и что используется
+ * правильный метод Dotenv (createUnsafeImmutable для console).
+ *
+ * Фиксы:
+ * - [ERP-500] a2fda072: порядок загрузки .env во всех точках входа
+ * - scheduler.php: createUnsafeImmutable для работы getenv()
+ *
+ * @group config
+ * @group env
+ * @group regression
+ */
+class EntryPointEnvLoadingTest extends Unit
+{
+ /**
+ * Точки входа веб-приложения
+ */
+ private array $webEntryPoints = [
+ 'erp24/web/index.php',
+ 'erp24/api1/index.php',
+ 'erp24/api2/index.php',
+ 'erp24/api3/index.php',
+ ];
+
+ /**
+ * Точки входа консольных приложений
+ */
+ private array $consoleEntryPoints = [
+ 'erp24/scripts/scheduler.php',
+ 'erp24/yii',
+ ];
+
+ /**
+ * Получает базовый путь к проекту (работает и в Docker, и локально)
+ */
+ private function getProjectRoot(): string
+ {
+ // Пробуем несколько путей
+ $possibleRoots = [
+ dirname(__DIR__, 4), // локально: tests/unit/config -> erp24
+ '/www', // Docker mount
+ dirname(__DIR__, 3), // альтернативный путь
+ ];
+
+ foreach ($possibleRoots as $root) {
+ if (file_exists($root . '/scripts/scheduler.php') ||
+ file_exists($root . '/erp24/scripts/scheduler.php')) {
+ return $root;
+ }
+ }
+
+ return dirname(__DIR__, 4);
+ }
+
+ /**
+ * Проверяет порядок загрузки: autoload → env → Yii → config
+ *
+ * @dataProvider webEntryPointsProvider
+ */
+ public function testWebEntryPointLoadsEnvBeforeYii(string $entryPoint): void
+ {
+ $root = $this->getProjectRoot();
+ $filePath = $root . '/' . $entryPoint;
+
+ // Пробуем альтернативный путь без erp24 префикса
+ if (!file_exists($filePath)) {
+ $filePath = $root . '/' . str_replace('erp24/', '', $entryPoint);
+ }
+
+ if (!file_exists($filePath)) {
+ $this->markTestSkipped("Entry point not found: $entryPoint");
+ }
+
+ $content = file_get_contents($filePath);
+
+ // Находим позиции ключевых require
+ $autoloadPos = strpos($content, 'vendor/autoload.php');
+ $envPos = strpos($content, 'env.php');
+ $yiiPos = strpos($content, 'Yii.php');
+
+ // Если есть загрузка env, она должна быть между autoload и Yii
+ if ($envPos !== false) {
+ $this->assertGreaterThan(
+ $autoloadPos,
+ $envPos,
+ "env.php must be loaded AFTER autoload.php in $entryPoint"
+ );
+ $this->assertLessThan(
+ $yiiPos,
+ $envPos,
+ "env.php must be loaded BEFORE Yii.php in $entryPoint"
+ );
+ }
+ }
+
+ /**
+ * Проверяет загрузку окружения в scheduler.php
+ *
+ * Scheduler может загружать переменные через:
+ * 1. Dotenv::createUnsafeImmutable (putenv вызывается)
+ * 2. config/env.php (централизованная загрузка)
+ * 3. Системные переменные окружения (установлены через systemd/cron)
+ *
+ * ВАЖНО: Если используется createImmutable (без Unsafe), getenv() не будет работать!
+ */
+ public function testSchedulerEnvironmentLoading(): void
+ {
+ $root = $this->getProjectRoot();
+ $schedulerPath = $root . '/erp24/scripts/scheduler.php';
+ if (!file_exists($schedulerPath)) {
+ $schedulerPath = $root . '/scripts/scheduler.php';
+ }
+
+ if (!file_exists($schedulerPath)) {
+ $this->markTestSkipped('scheduler.php not found');
+ }
+
+ $content = file_get_contents($schedulerPath);
+
+ // Проверяем способ загрузки окружения
+ $hasDotenv = strpos($content, 'Dotenv') !== false;
+ $hasEnvPhp = strpos($content, 'env.php') !== false;
+ $hasConfigEnv = strpos($content, "config/env.php") !== false;
+
+ // Если используется Dotenv напрямую, должен быть createUnsafeImmutable
+ if ($hasDotenv) {
+ $hasCreateImmutable = preg_match('/createImmutable(?!\w)/', $content);
+ $hasCreateUnsafeImmutable = preg_match('/createUnsafeImmutable/', $content);
+
+ // createImmutable без Unsafe - это проблема
+ if ($hasCreateImmutable && !$hasCreateUnsafeImmutable) {
+ $this->fail(
+ 'scheduler.php uses createImmutable which does NOT call putenv(). ' .
+ 'Use createUnsafeImmutable instead for getenv() to work.'
+ );
+ }
+ }
+
+ // Если нет явной загрузки .env - отмечаем как incomplete
+ if (!$hasDotenv && !$hasEnvPhp && !$hasConfigEnv) {
+ $this->markTestIncomplete(
+ 'scheduler.php does not explicitly load .env file. ' .
+ 'It relies on system environment variables being set externally. ' .
+ 'Consider adding: require __DIR__ . "/../config/env.php";'
+ );
+ }
+
+ $this->assertTrue(true, 'Environment loading check completed');
+ }
+
+ /**
+ * Проверяет, что все точки входа загружают .env или config/env.php
+ *
+ * @dataProvider allEntryPointsProvider
+ */
+ public function testEntryPointHasEnvLoading(string $entryPoint): void
+ {
+ $root = $this->getProjectRoot();
+ $filePath = $root . '/' . $entryPoint;
+
+ // Пробуем альтернативный путь без erp24 префикса
+ if (!file_exists($filePath)) {
+ $filePath = $root . '/' . str_replace('erp24/', '', $entryPoint);
+ }
+
+ if (!file_exists($filePath)) {
+ $this->markTestSkipped("Entry point not found: $entryPoint");
+ }
+
+ $content = file_get_contents($filePath);
+
+ // Должен быть либо env.php, либо Dotenv напрямую
+ $hasEnvPhp = strpos($content, 'env.php') !== false;
+ $hasDotenv = strpos($content, 'Dotenv') !== false;
+
+ // Для scheduler.php - отдельная обработка (может работать через системные переменные)
+ if (strpos($entryPoint, 'scheduler.php') !== false && !$hasEnvPhp && !$hasDotenv) {
+ $this->markTestIncomplete(
+ "$entryPoint does not explicitly load .env. " .
+ "Relies on system environment variables."
+ );
+ return;
+ }
+
+ $this->assertTrue(
+ $hasEnvPhp || $hasDotenv,
+ "Entry point $entryPoint must load environment variables (env.php or Dotenv)"
+ );
+ }
+
+ /**
+ * Проверяет, что в console entry points getenv() доступен
+ * (т.е. putenv был вызван)
+ */
+ public function testGetenvWorksAfterDotenvLoad(): void
+ {
+ // Этот тест проверяет, что переменные из .env доступны через getenv
+ // Если тест падает, значит Dotenv не вызывает putenv()
+
+ $requiredVars = ['POSTGRES_PASSWORD', 'MODE'];
+
+ // Пропускаем если .env не загружен
+ if (getenv('MODE') === false && getenv('POSTGRES_PASSWORD') === false) {
+ $this->markTestSkipped('.env not loaded in test environment');
+ }
+
+ foreach ($requiredVars as $var) {
+ $value = getenv($var);
+ // Переменная должна быть либо установлена, либо мы пропускаем
+ if ($value !== false) {
+ $this->assertIsString($value, "getenv('$var') should return string");
+ }
+ }
+ }
+
+ public function webEntryPointsProvider(): array
+ {
+ return array_map(fn($path) => [$path], $this->webEntryPoints);
+ }
+
+ public function allEntryPointsProvider(): array
+ {
+ return array_map(
+ fn($path) => [$path],
+ array_merge($this->webEntryPoints, $this->consoleEntryPoints)
+ );
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace tests\unit\services;
+
+use Codeception\Test\Unit;
+
+/**
+ * Тесты FileService
+ *
+ * Проверяют корректность определения типов файлов и логику сохранения.
+ *
+ * Фиксы:
+ * - d9d573cf: Добавлен mov к типам файлов + исправлена логика сохранения
+ * (было: if ($fileRecord->save()) - логировало ошибку при УСПЕХЕ)
+ *
+ * @group services
+ * @group file
+ * @group regression
+ */
+class FileServiceTest extends Unit
+{
+ /**
+ * Путь к FileService
+ */
+ private string $fileServicePath;
+
+ protected function _before(): void
+ {
+ $this->fileServicePath = dirname(__DIR__, 3) . '/services/FileService.php';
+ }
+
+ /**
+ * Проверяет, что .mov файлы определяются как video
+ */
+ public function testMovFileTypeIsVideo(): void
+ {
+ if (!file_exists($this->fileServicePath)) {
+ $this->markTestSkipped('FileService.php not found');
+ }
+
+ $content = file_get_contents($this->fileServicePath);
+
+ // Проверяем наличие mov в типах
+ $this->assertStringContainsString(
+ "'mov'",
+ $content,
+ 'FileService should support .mov file type'
+ );
+
+ // Проверяем, что mov определяется как video
+ // Паттерн: case 'mov': ... $type = 'video'
+ $hasMovAsVideo = preg_match(
+ "/case\s+'mov'.*?'video'/s",
+ $content
+ );
+
+ $this->assertEquals(
+ 1,
+ $hasMovAsVideo,
+ '.mov files should be classified as video type'
+ );
+ }
+
+ /**
+ * Проверяет корректность логики логирования ошибок сохранения
+ *
+ * ВАЖНО: Ошибка должна логироваться когда save() возвращает FALSE,
+ * а не когда возвращает TRUE.
+ */
+ public function testSaveErrorLoggingLogic(): void
+ {
+ if (!file_exists($this->fileServicePath)) {
+ $this->markTestSkipped('FileService.php not found');
+ }
+
+ $content = file_get_contents($this->fileServicePath);
+
+ // Ищем паттерн: if (!$fileRecord->save()) { ... Yii::error
+ // Это ПРАВИЛЬНАЯ логика - логировать при НЕУДАЧЕ
+
+ $correctPattern = preg_match(
+ '/if\s*\(\s*!\s*\$\w+->save\(\)\s*\)\s*\{[^}]*Yii::error/s',
+ $content
+ );
+
+ // Ищем НЕПРАВИЛЬНЫЙ паттерн: if ($fileRecord->save()) { ... Yii::error
+ // Это БАГ - логирование при УСПЕХЕ
+ $incorrectPattern = preg_match(
+ '/if\s*\(\s*\$\w+->save\(\)\s*\)\s*\{[^}]*Yii::error[^}]*getErrors/s',
+ $content
+ );
+
+ $this->assertEquals(
+ 1,
+ $correctPattern,
+ 'Error logging should happen when save() returns FALSE (not TRUE)'
+ );
+
+ $this->assertEquals(
+ 0,
+ $incorrectPattern,
+ 'BUG: Error is logged when save() returns TRUE (should be FALSE)'
+ );
+ }
+
+ /**
+ * Проверяет поддерживаемые типы файлов в switch блоке saveFileEntity
+ *
+ * Структура FileService:
+ * - По умолчанию $type = 'image' (для jpg, png, gif и т.д.)
+ * - Switch только для doc и video типов
+ */
+ public function testSupportedFileTypesInSwitch(): void
+ {
+ if (!file_exists($this->fileServicePath)) {
+ $this->markTestSkipped('FileService.php not found');
+ }
+
+ $content = file_get_contents($this->fileServicePath);
+
+ // Типы документов которые должны быть в switch-case
+ $docTypes = ['txt', 'pdf', 'xls', 'xlsx', 'docx', 'doc'];
+ foreach ($docTypes as $extension) {
+ $pattern = '/case\s+[\'"]' . preg_quote($extension, '/') . '[\'"]/i';
+ $hasCase = preg_match($pattern, $content);
+ $this->assertEquals(
+ 1,
+ $hasCase,
+ "FileService switch should have case for .$extension (doc type)"
+ );
+ }
+
+ // Типы видео которые должны быть в switch-case
+ $videoTypes = ['mp4', 'mov'];
+ foreach ($videoTypes as $extension) {
+ $pattern = '/case\s+[\'"]' . preg_quote($extension, '/') . '[\'"]/i';
+ $hasCase = preg_match($pattern, $content);
+ $this->assertEquals(
+ 1,
+ $hasCase,
+ "FileService switch should have case for .$extension (video type)"
+ );
+ }
+
+ // Проверяем что default type = 'image'
+ $this->assertStringContainsString(
+ "\$type = 'image'",
+ $content,
+ "FileService should have default type 'image' for jpg/png/gif files"
+ );
+ }
+
+ /**
+ * Проверяет что путь сохранения начинается с /uploads
+ */
+ public function testUploadPathStartsWithUploads(): void
+ {
+ if (!file_exists($this->fileServicePath)) {
+ $this->markTestSkipped('FileService.php not found');
+ }
+
+ $content = file_get_contents($this->fileServicePath);
+
+ $this->assertStringContainsString(
+ "'/uploads'",
+ $content,
+ 'File URL should start with /uploads'
+ );
+ }
+}
--- /dev/null
+<?php
+
+declare(strict_types=1);
+
+namespace tests\unit\services;
+
+use Codeception\Test\Unit;
+
+/**
+ * Тесты обработки статусов 1С в UploadService
+ *
+ * Проверяют корректность установки статусов при интеграции с 1С.
+ *
+ * Фикс: b5050c60 - Меняем статус 1С
+ * Проблема: Если заказ "уже записан в системе" 1С, не должен быть статус ERROR
+ *
+ * @group services
+ * @group 1c
+ * @group regression
+ */
+class UploadService1CStatusTest extends Unit
+{
+ private string $uploadServicePath;
+
+ protected function _before(): void
+ {
+ $this->uploadServicePath = dirname(__DIR__, 3) . '/services/UploadService.php';
+ }
+
+ /**
+ * Проверяет, что сообщение "уже записан в системе" приводит к CREATED, а не ERROR
+ */
+ public function testAlreadyExistsInSystemSetsCreatedStatus(): void
+ {
+ if (!file_exists($this->uploadServicePath)) {
+ $this->markTestSkipped('UploadService.php not found');
+ }
+
+ $content = file_get_contents($this->uploadServicePath);
+
+ // Проверяем наличие проверки на "уже записан в системе"
+ $this->assertStringContainsString(
+ 'уже записан в системе',
+ $content,
+ 'UploadService should check for "already exists in system" message'
+ );
+
+ // Проверяем, что при этом условии устанавливается CREATED статус
+ $hasCorrectLogic = preg_match(
+ '/уже записан в системе.*?STATUSES_1C_CREATED/s',
+ $content
+ );
+
+ $this->assertEquals(
+ 1,
+ $hasCorrectLogic,
+ 'When order already exists in 1C, status should be CREATED_IN_1C (not ERROR)'
+ );
+ }
+
+ /**
+ * Проверяет, что при реальной ошибке устанавливается статус ERROR
+ */
+ public function testRealErrorSetsErrorStatus(): void
+ {
+ if (!file_exists($this->uploadServicePath)) {
+ $this->markTestSkipped('UploadService.php not found');
+ }
+
+ $content = file_get_contents($this->uploadServicePath);
+
+ // Должен быть статус ERROR для реальных ошибок
+ $this->assertStringContainsString(
+ 'STATUSES_1C_ERROR_1C',
+ $content,
+ 'UploadService should have ERROR status for real 1C errors'
+ );
+ }
+
+ /**
+ * Проверяет инициализацию массива $marketplaceOrdersGuidArr
+ *
+ * Фикс: Массив должен инициализироваться ВНЕ цикла, а не внутри
+ */
+ public function testMarketplaceOrdersGuidArrInitializedOutsideLoop(): void
+ {
+ if (!file_exists($this->uploadServicePath)) {
+ $this->markTestSkipped('UploadService.php not found');
+ }
+
+ $content = file_get_contents($this->uploadServicePath);
+
+ // Ищем паттерн где массив инициализируется перед foreach
+ // Правильно: $marketplaceOrdersGuidArr = []; ... foreach
+ // Неправильно: foreach ... $marketplaceOrdersGuidArr = [];
+
+ // Находим участок с created_orders
+ if (preg_match('/\[.created_orders.\].*?foreach\s*\(\s*\$result\s*\[\s*.created_orders.\s*\]/s', $content, $match)) {
+ $section = $match[0];
+
+ // Проверяем, что инициализация массива ДО foreach
+ $initPos = strpos($section, '$marketplaceOrdersGuidArr = []');
+ $foreachPos = strpos($section, 'foreach');
+
+ if ($initPos !== false && $foreachPos !== false) {
+ $this->assertLessThan(
+ $foreachPos,
+ $initPos,
+ '$marketplaceOrdersGuidArr should be initialized BEFORE foreach loop'
+ );
+ }
+ }
+
+ $this->assertTrue(true, 'Array initialization check completed');
+ }
+
+ /**
+ * Проверяет сохранение error_text при ошибках
+ */
+ public function testErrorTextIsSaved(): void
+ {
+ if (!file_exists($this->uploadServicePath)) {
+ $this->markTestSkipped('UploadService.php not found');
+ }
+
+ $content = file_get_contents($this->uploadServicePath);
+
+ // Должно быть сохранение error_text
+ $this->assertStringContainsString(
+ 'error_text',
+ $content,
+ 'UploadService should save error_text field'
+ );
+
+ // Должно быть присвоение и сохранение
+ $hasErrorTextSave = preg_match(
+ '/\$\w+->error_text\s*=.*?->save\(\)/s',
+ $content
+ );
+
+ $this->assertEquals(
+ 1,
+ $hasErrorTextSave,
+ 'error_text should be assigned and then saved'
+ );
+ }
+
+ /**
+ * Проверяет сохранение number_1c при успешном создании
+ */
+ public function testNumber1CIsSavedOnSuccess(): void
+ {
+ if (!file_exists($this->uploadServicePath)) {
+ $this->markTestSkipped('UploadService.php not found');
+ }
+
+ $content = file_get_contents($this->uploadServicePath);
+
+ // При "уже записан в системе" должен сохраняться номер 1С
+ $hasNumber1cSave = preg_match(
+ '/уже записан в системе.*?number_1c\s*=/s',
+ $content
+ );
+
+ $this->assertEquals(
+ 1,
+ $hasNumber1cSave,
+ 'number_1c should be saved when order already exists in 1C'
+ );
+ }
+}