ImportCatalogServiceTest.php 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254
  1. <?php
  2. namespace Tests\Unit\Services\Import;
  3. use App\Models\Import;
  4. use App\Models\Product;
  5. use App\Services\ImportCatalogService;
  6. use Illuminate\Foundation\Testing\RefreshDatabase;
  7. use Illuminate\Support\Facades\Storage;
  8. use PhpOffice\PhpSpreadsheet\Spreadsheet;
  9. use PhpOffice\PhpSpreadsheet\Writer\Xlsx as XlsxWriter;
  10. use Tests\TestCase;
  11. class ImportCatalogServiceTest extends TestCase
  12. {
  13. use RefreshDatabase;
  14. protected $seed = true;
  15. private int $testYear = 2025;
  16. private array $tempFiles = [];
  17. protected function tearDown(): void
  18. {
  19. foreach ($this->tempFiles as $path) {
  20. if (file_exists($path)) {
  21. unlink($path);
  22. }
  23. }
  24. parent::tearDown();
  25. }
  26. // -------------------------------------------------------------------------
  27. // Helpers
  28. // -------------------------------------------------------------------------
  29. /**
  30. * Build an xlsx file with catalog headers and optional data rows, upload it
  31. * to Storage::disk('upload') and return the Import model.
  32. */
  33. private function createImportWithFile(array $dataRows = [], ?array $customHeaders = null): Import
  34. {
  35. $import = Import::factory()->create([
  36. 'type' => 'catalog',
  37. 'filename' => 'test_catalog_' . uniqid() . '.xlsx',
  38. 'status' => 'new',
  39. 'year' => $this->testYear,
  40. ]);
  41. $tempPath = $this->buildXlsxFile(
  42. $customHeaders ?? array_keys(ImportCatalogService::HEADERS),
  43. $dataRows
  44. );
  45. Storage::fake('upload');
  46. Storage::disk('upload')->put($import->filename, file_get_contents($tempPath));
  47. return $import;
  48. }
  49. private function buildXlsxFile(array $headers, array $dataRows): string
  50. {
  51. $spreadsheet = new Spreadsheet();
  52. $sheet = $spreadsheet->getActiveSheet();
  53. foreach ($headers as $colIdx => $header) {
  54. $sheet->setCellValue(chr(65 + $colIdx) . '1', $header);
  55. }
  56. foreach ($dataRows as $rowIdx => $row) {
  57. foreach ($row as $colIdx => $value) {
  58. $sheet->setCellValue(chr(65 + $colIdx) . ($rowIdx + 2), $value);
  59. }
  60. }
  61. $path = sys_get_temp_dir() . '/test_catalog_' . uniqid() . '.xlsx';
  62. (new XlsxWriter($spreadsheet))->save($path);
  63. $this->tempFiles[] = $path;
  64. return $path;
  65. }
  66. /**
  67. * Build a full 24-column data row matching the catalog headers.
  68. * Only nomenclature_number is mandatory for the service logic; all other
  69. * fields are filled with neutral defaults.
  70. */
  71. private function makeDataRow(
  72. string $nomenclatureNumber,
  73. string $article = 'ART-001',
  74. string $nameTz = 'Горка тестовая'
  75. ): array {
  76. return [
  77. '', // Фото
  78. $article, // Артикул образца
  79. $nameTz, // Наименование по ТЗ
  80. 'Горка', // Тип по ТЗ
  81. $nomenclatureNumber, // № по номенкл.
  82. '1000x500x800', // Габаритные размеры
  83. 'ООО Завод', // Производитель
  84. 'шт', // ед. изм.
  85. 'standard', // Тип оборудования
  86. 1000.00, // Цена поставки
  87. 200.00, // Цена установки
  88. 1200.00, // Итого цена
  89. 'Завод Тест', // Наименование производителя
  90. '', // Примечание
  91. 'Горка', // Наименование по паспорту
  92. 'Горка', // Наименование в ведомости
  93. 10, // Срок службы
  94. 'CERT-001', // Номер сертификата
  95. 0, // Дата сертификата (0 = no date)
  96. 'Орган', // Орган сертификации
  97. 'ГОСТ', // Вид сертификата
  98. 50.5, // Вес
  99. 1.2, // Объем
  100. 2, // Мест
  101. ];
  102. }
  103. // -------------------------------------------------------------------------
  104. // Tests
  105. // -------------------------------------------------------------------------
  106. /**
  107. * When the upload file does not exist, prepare() must fail and handle()
  108. * must return false.
  109. */
  110. public function test_handle_returns_false_when_file_not_found(): void
  111. {
  112. Storage::fake('upload');
  113. $import = Import::factory()->create([
  114. 'type' => 'catalog',
  115. 'filename' => 'nonexistent_catalog_' . uniqid() . '.xlsx',
  116. 'status' => 'new',
  117. 'year' => $this->testYear,
  118. ]);
  119. $service = new ImportCatalogService($import, $this->testYear);
  120. $result = $service->handle();
  121. $this->assertFalse($result);
  122. }
  123. /**
  124. * A file with wrong column headers fails the header check and returns false.
  125. */
  126. public function test_handle_returns_false_with_wrong_headers(): void
  127. {
  128. $import = $this->createImportWithFile([], ['Неверный', 'Заголовок', 'Здесь']);
  129. $service = new ImportCatalogService($import, $this->testYear);
  130. $result = $service->handle();
  131. $this->assertFalse($result);
  132. }
  133. /**
  134. * A valid file with one data row causes a new Product to be created.
  135. */
  136. public function test_handle_creates_product_for_new_nomenclature_number(): void
  137. {
  138. $import = $this->createImportWithFile([
  139. $this->makeDataRow('TEST.001', 'ART-TEST-001', 'Горка синяя'),
  140. ]);
  141. $service = new ImportCatalogService($import, $this->testYear);
  142. $result = $service->handle();
  143. $this->assertTrue($result);
  144. $this->assertDatabaseHas('products', [
  145. 'nomenclature_number' => 'TEST.001',
  146. 'year' => $this->testYear,
  147. ]);
  148. }
  149. /**
  150. * When a Product with the same nomenclature_number + year already exists,
  151. * the service updates it rather than creating a duplicate.
  152. */
  153. public function test_handle_updates_existing_product(): void
  154. {
  155. // Pre-create a product that the importer should find and update.
  156. Product::factory()->create([
  157. 'nomenclature_number' => 'UPD.001',
  158. 'year' => $this->testYear,
  159. 'name_tz' => 'Старое название',
  160. 'article' => 'OLD-ART',
  161. ]);
  162. $import = $this->createImportWithFile([
  163. $this->makeDataRow('UPD.001', 'NEW-ART', 'Новое название'),
  164. ]);
  165. $service = new ImportCatalogService($import, $this->testYear);
  166. $result = $service->handle();
  167. $this->assertTrue($result);
  168. // Only one product with this nomenclature/year should exist
  169. $this->assertSame(
  170. 1,
  171. Product::withoutGlobalScopes()
  172. ->where('nomenclature_number', 'UPD.001')
  173. ->where('year', $this->testYear)
  174. ->count()
  175. );
  176. $this->assertDatabaseHas('products', [
  177. 'nomenclature_number' => 'UPD.001',
  178. 'year' => $this->testYear,
  179. 'name_tz' => 'Новое название',
  180. ]);
  181. }
  182. /**
  183. * Rows whose nomenclature_number is empty must be silently skipped; the
  184. * overall handle() should still return true.
  185. */
  186. public function test_handle_skips_empty_rows(): void
  187. {
  188. $emptyRow = $this->makeDataRow('');
  189. $emptyRow[4] = ''; // explicitly clear nomenclature_number (column index 4)
  190. $import = $this->createImportWithFile([$emptyRow]);
  191. $service = new ImportCatalogService($import, $this->testYear);
  192. $result = $service->handle();
  193. $this->assertTrue($result);
  194. // No product should have been created for an empty nomenclature
  195. $this->assertSame(0, Product::withoutGlobalScopes()->where('year', $this->testYear)->count());
  196. }
  197. /**
  198. * A file that has only the header row and no data rows should be processed
  199. * without errors and return true.
  200. */
  201. public function test_handle_returns_true_with_headers_only(): void
  202. {
  203. $import = $this->createImportWithFile([]);
  204. $service = new ImportCatalogService($import, $this->testYear);
  205. $result = $service->handle();
  206. $this->assertTrue($result);
  207. }
  208. }