| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254 |
- <?php
- namespace Tests\Unit\Services\Import;
- use App\Models\Import;
- use App\Models\Product;
- use App\Services\ImportCatalogService;
- use Illuminate\Foundation\Testing\RefreshDatabase;
- use Illuminate\Support\Facades\Storage;
- use PhpOffice\PhpSpreadsheet\Spreadsheet;
- use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter;
- use Tests\TestCase;
- class ImportCatalogServiceTest extends TestCase
- {
- use RefreshDatabase;
- protected $seed = true;
- private int $testYear = 2025;
- private array $tempFiles = [];
- protected function tearDown(): void
- {
- foreach ($this->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);
- }
- }
|