tempFiles as $path) { if (file_exists($path)) { unlink($path); } } parent::tearDown(); } // ------------------------------------------------------------------------- // Helpers // ------------------------------------------------------------------------- /** * Build an xlsx file with catalog headers and optional data rows, upload it * to Storage::disk('upload') and return the Import model. */ private function createImportWithFile(array $dataRows = [], ?array $customHeaders = null): Import { $import = Import::factory()->create([ 'type' => 'catalog', 'filename' => 'test_catalog_' . uniqid() . '.xlsx', 'status' => 'new', 'year' => $this->testYear, ]); $tempPath = $this->buildXlsxFile( $customHeaders ?? array_keys(ImportCatalogService::HEADERS), $dataRows ); Storage::fake('upload'); Storage::disk('upload')->put($import->filename, file_get_contents($tempPath)); return $import; } private function buildXlsxFile(array $headers, array $dataRows): string { $spreadsheet = new Spreadsheet(); $sheet = $spreadsheet->getActiveSheet(); foreach ($headers as $colIdx => $header) { $sheet->setCellValue(chr(65 + $colIdx) . '1', $header); } foreach ($dataRows as $rowIdx => $row) { foreach ($row as $colIdx => $value) { $sheet->setCellValue(chr(65 + $colIdx) . ($rowIdx + 2), $value); } } $path = sys_get_temp_dir() . '/test_catalog_' . uniqid() . '.xlsx'; (new XlsxWriter($spreadsheet))->save($path); $this->tempFiles[] = $path; return $path; } /** * Build a full 24-column data row matching the catalog headers. * Only nomenclature_number is mandatory for the service logic; all other * fields are filled with neutral defaults. */ private function makeDataRow( string $nomenclatureNumber, string $article = 'ART-001', string $nameTz = 'Горка тестовая' ): array { return [ '', // Фото $article, // Артикул образца $nameTz, // Наименование по ТЗ 'Горка', // Тип по ТЗ $nomenclatureNumber, // № по номенкл. '1000x500x800', // Габаритные размеры 'ООО Завод', // Производитель 'шт', // ед. изм. 'standard', // Тип оборудования 1000.00, // Цена поставки 200.00, // Цена установки 1200.00, // Итого цена 'Завод Тест', // Наименование производителя '', // Примечание 'Горка', // Наименование по паспорту 'Горка', // Наименование в ведомости 10, // Срок службы 'CERT-001', // Номер сертификата 0, // Дата сертификата (0 = no date) 'Орган', // Орган сертификации 'ГОСТ', // Вид сертификата 50.5, // Вес 1.2, // Объем 2, // Мест ]; } // ------------------------------------------------------------------------- // Tests // ------------------------------------------------------------------------- /** * When the upload file does not exist, prepare() must fail and handle() * must return false. */ public function test_handle_returns_false_when_file_not_found(): void { Storage::fake('upload'); $import = Import::factory()->create([ 'type' => 'catalog', 'filename' => 'nonexistent_catalog_' . uniqid() . '.xlsx', 'status' => 'new', 'year' => $this->testYear, ]); $service = new ImportCatalogService($import, $this->testYear); $result = $service->handle(); $this->assertFalse($result); } /** * A file with wrong column headers fails the header check and returns false. */ public function test_handle_returns_false_with_wrong_headers(): void { $import = $this->createImportWithFile([], ['Неверный', 'Заголовок', 'Здесь']); $service = new ImportCatalogService($import, $this->testYear); $result = $service->handle(); $this->assertFalse($result); } /** * A valid file with one data row causes a new Product to be created. */ public function test_handle_creates_product_for_new_nomenclature_number(): void { $import = $this->createImportWithFile([ $this->makeDataRow('TEST.001', 'ART-TEST-001', 'Горка синяя'), ]); $service = new ImportCatalogService($import, $this->testYear); $result = $service->handle(); $this->assertTrue($result); $this->assertDatabaseHas('products', [ 'nomenclature_number' => 'TEST.001', 'year' => $this->testYear, ]); } /** * When a Product with the same nomenclature_number + year already exists, * the service updates it rather than creating a duplicate. */ public function test_handle_updates_existing_product(): void { // Pre-create a product that the importer should find and update. Product::factory()->create([ 'nomenclature_number' => 'UPD.001', 'year' => $this->testYear, 'name_tz' => 'Старое название', 'article' => 'OLD-ART', ]); $import = $this->createImportWithFile([ $this->makeDataRow('UPD.001', 'NEW-ART', 'Новое название'), ]); $service = new ImportCatalogService($import, $this->testYear); $result = $service->handle(); $this->assertTrue($result); // Only one product with this nomenclature/year should exist $this->assertSame( 1, Product::withoutGlobalScopes() ->where('nomenclature_number', 'UPD.001') ->where('year', $this->testYear) ->count() ); $this->assertDatabaseHas('products', [ 'nomenclature_number' => 'UPD.001', 'year' => $this->testYear, 'name_tz' => 'Новое название', ]); } /** * Rows whose nomenclature_number is empty must be silently skipped; the * overall handle() should still return true. */ public function test_handle_skips_empty_rows(): void { $emptyRow = $this->makeDataRow(''); $emptyRow[4] = ''; // explicitly clear nomenclature_number (column index 4) $import = $this->createImportWithFile([$emptyRow]); $service = new ImportCatalogService($import, $this->testYear); $result = $service->handle(); $this->assertTrue($result); // No product should have been created for an empty nomenclature $this->assertSame(0, Product::withoutGlobalScopes()->where('year', $this->testYear)->count()); } /** * A file that has only the header row and no data rows should be processed * without errors and return true. */ public function test_handle_returns_true_with_headers_only(): void { $import = $this->createImportWithFile([]); $service = new ImportCatalogService($import, $this->testYear); $result = $service->handle(); $this->assertTrue($result); } }