ProductionOrderDocumentControllerTest.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. <?php
  2. declare(strict_types=1);
  3. namespace Tests\Feature;
  4. use App\Jobs\GenerateProductionOrderFileJob;
  5. use App\Models\CommonCatalogItem;
  6. use App\Models\File;
  7. use App\Models\ProductionOrder;
  8. use App\Models\ProductionOrderDelivery;
  9. use App\Models\ProductionOrderInstallation;
  10. use App\Models\ProductionOrderItem;
  11. use App\Models\User;
  12. use App\Services\ProductionOrderDocumentService;
  13. use Illuminate\Foundation\Testing\RefreshDatabase;
  14. use Illuminate\Support\Facades\Bus;
  15. use Illuminate\Support\Facades\Storage;
  16. use PhpOffice\PhpSpreadsheet\IOFactory;
  17. use Tests\TestCase;
  18. use ZipArchive;
  19. class ProductionOrderDocumentControllerTest extends TestCase
  20. {
  21. use RefreshDatabase;
  22. protected bool $seed = true;
  23. private User $admin;
  24. private User $manager;
  25. private User $driver;
  26. private User $brigadier;
  27. private ProductionOrder $order;
  28. private ProductionOrderItem $item;
  29. protected function setUp(): void
  30. {
  31. parent::setUp();
  32. Storage::fake('local');
  33. Storage::fake('public');
  34. Storage::fake('base');
  35. $this->admin = User::factory()->admin()->create();
  36. $this->manager = User::factory()->manager()->create(['phone' => '+7 900 123-45-67']);
  37. $this->driver = User::factory()->driver()->create();
  38. $this->brigadier = User::factory()->brigadier()->create();
  39. $this->order = ProductionOrder::factory()->create([
  40. 'order_number' => 'ДОК-001',
  41. 'customer_name' => 'ООО Заказчик',
  42. 'object_address' => 'г. Москва, ул. Доставки, 1',
  43. 'invoice_number' => 'СЧ-55',
  44. 'invoice_date' => '2026-09-01',
  45. 'contract_number' => 'Д-77',
  46. 'contract_date' => '2026-08-20',
  47. 'manager_id' => $this->manager->id,
  48. ]);
  49. $catalogItem = CommonCatalogItem::factory()->create([
  50. 'article' => 'ART-100',
  51. 'calculator_name' => 'Качели',
  52. ]);
  53. $this->item = ProductionOrderItem::factory()->create([
  54. 'production_order_id' => $this->order->id,
  55. 'common_catalog_item_id' => $catalogItem->id,
  56. 'order_item_number' => 'МАФ-100',
  57. 'factory_number' => 'ЗН-100',
  58. 'manufacture_date' => '2026-09-02',
  59. ]);
  60. ProductionOrderItem::factory()->create([
  61. 'production_order_id' => $this->order->id,
  62. 'common_catalog_item_id' => $catalogItem->id,
  63. 'order_item_number' => 'МАФ-101',
  64. ]);
  65. }
  66. public function test_export_items_contains_order_header_and_each_item(): void
  67. {
  68. $file = app(ProductionOrderDocumentService::class)->generate(
  69. ProductionOrderDocumentService::TYPE_ITEMS,
  70. $this->order->id,
  71. $this->admin->id,
  72. );
  73. Storage::disk('local')->assertExists($file->path);
  74. $spreadsheet = IOFactory::load(Storage::disk('local')->path($file->path));
  75. $sheet = $spreadsheet->getActiveSheet();
  76. $this->assertSame('Оборудование заказа №ДОК-001', $sheet->getCell('A1')->getValue());
  77. $this->assertStringContainsString('ООО Заказчик', (string) $sheet->getCell('A2')->getValue());
  78. $this->assertSame('ART-100', $sheet->getCell('A5')->getValue());
  79. $this->assertSame('МАФ-101', $sheet->getCell('C6')->getValue());
  80. }
  81. public function test_delivery_request_fills_provided_template_and_preserves_print_layout(): void
  82. {
  83. $delivery = ProductionOrderDelivery::factory()->create([
  84. 'production_order_id' => $this->order->id,
  85. 'driver_id' => $this->driver->id,
  86. 'delivery_date' => '2026-09-15',
  87. 'request_number' => 'ЗД-15',
  88. 'request_date' => '2026-09-10',
  89. 'contact_name' => 'Иван Иванов',
  90. 'contact_phone' => '+7 999 000-00-00',
  91. 'has_passports_and_certificates' => true,
  92. 'note' => 'Позвонить за час',
  93. ]);
  94. $file = app(ProductionOrderDocumentService::class)->generate(
  95. ProductionOrderDocumentService::TYPE_DELIVERY,
  96. $this->order->id,
  97. $this->admin->id,
  98. $delivery->id,
  99. );
  100. $spreadsheet = IOFactory::load(Storage::disk('local')->path($file->path));
  101. $sheet = $spreadsheet->getActiveSheet();
  102. $this->assertSame('B1:AG94', $sheet->getPageSetup()->getPrintArea());
  103. $this->assertCount(2, $sheet->getDrawingCollection());
  104. $this->assertSame('СЧ-55', $sheet->getCell('G4')->getValue());
  105. $this->assertSame('ДОК-001', $sheet->getCell('G6')->getValue());
  106. $this->assertSame('Иван Иванов', $sheet->getCell('L13')->getValue());
  107. $this->assertStringContainsString('ART-100 — Качели — 2 шт.', (string) $sheet->getCell('G25')->getValue());
  108. }
  109. public function test_installation_and_technical_archives_include_catalog_documents(): void
  110. {
  111. Storage::disk('base')->put('public/images/tech-docs/ART-100/manual.pdf', 'legacy-manual');
  112. Storage::disk('base')->put('public/images/tech-docs/ART-100/assembly.pdf', 'legacy-assembly');
  113. $duplicateDocument = File::factory()->pdf()->create([
  114. 'user_id' => $this->admin->id,
  115. 'path' => 'catalog/ART-100/manual.pdf',
  116. 'original_name' => 'manual.pdf',
  117. ]);
  118. $additionalDocument = File::factory()->pdf()->create([
  119. 'user_id' => $this->admin->id,
  120. 'path' => 'catalog/ART-100/additional.pdf',
  121. 'original_name' => 'additional.pdf',
  122. ]);
  123. Storage::disk('public')->put($duplicateDocument->path, 'common-catalog-manual');
  124. Storage::disk('public')->put($additionalDocument->path, 'common-catalog-additional');
  125. $this->item->catalogItem->documents()->attach([
  126. $duplicateDocument->id,
  127. $additionalDocument->id,
  128. ]);
  129. $installation = ProductionOrderInstallation::factory()->create([
  130. 'production_order_id' => $this->order->id,
  131. 'brigadier_id' => $this->brigadier->id,
  132. 'installation_date' => '2026-09-20',
  133. ]);
  134. $technicalFile = app(ProductionOrderDocumentService::class)->generate(
  135. ProductionOrderDocumentService::TYPE_TECHNICAL_DOCUMENTS,
  136. $this->order->id,
  137. $this->admin->id,
  138. itemIds: [$this->item->id],
  139. );
  140. $installationFile = app(ProductionOrderDocumentService::class)->generate(
  141. ProductionOrderDocumentService::TYPE_INSTALLATION,
  142. $this->order->id,
  143. $this->admin->id,
  144. $installation->id,
  145. );
  146. $technicalEntries = $this->zipEntries(Storage::disk('local')->path($technicalFile->path));
  147. $installationEntries = $this->zipEntries(Storage::disk('local')->path($installationFile->path));
  148. $this->assertContains('Техдокументация/ART-100/manual.pdf', $technicalEntries);
  149. $this->assertContains('Техдокументация/ART-100/assembly.pdf', $technicalEntries);
  150. $this->assertContains('Техдокументация/ART-100/additional.pdf', $technicalEntries);
  151. $this->assertSame(1, count(array_filter(
  152. $technicalEntries,
  153. static fn (string $name): bool => $name === 'Техдокументация/ART-100/manual.pdf',
  154. )));
  155. $this->assertContains('Техдокументация/ART-100/manual.pdf', $installationEntries);
  156. $this->assertContains('Техдокументация/ART-100/assembly.pdf', $installationEntries);
  157. $this->assertContains('Техдокументация/ART-100/additional.pdf', $installationEntries);
  158. $this->assertSame(1, count(array_filter(
  159. $installationEntries,
  160. static fn (string $name): bool => $name === 'Техдокументация/ART-100/manual.pdf',
  161. )));
  162. $this->assertTrue(collect($installationEntries)->contains(fn (string $name): bool => str_ends_with($name, '.xlsx')));
  163. }
  164. public function test_document_actions_are_queued_and_related_records_are_scoped_to_order(): void
  165. {
  166. Bus::fake();
  167. $delivery = ProductionOrderDelivery::factory()->create([
  168. 'production_order_id' => $this->order->id,
  169. 'driver_id' => $this->driver->id,
  170. ]);
  171. $otherOrder = ProductionOrder::factory()->create(['manager_id' => $this->manager->id]);
  172. $otherDelivery = ProductionOrderDelivery::factory()->create([
  173. 'production_order_id' => $otherOrder->id,
  174. 'driver_id' => $this->driver->id,
  175. ]);
  176. $this->actingAs($this->admin)
  177. ->post(route('schedule.orders.export-items', $this->order))
  178. ->assertRedirect();
  179. $this->actingAs($this->admin)
  180. ->post(route('schedule.orders.deliveries.document', [$this->order, $delivery]))
  181. ->assertRedirect();
  182. $this->actingAs($this->admin)
  183. ->post(route('schedule.orders.technical-documents', $this->order), ['item_ids' => [$this->item->id]])
  184. ->assertRedirect();
  185. Bus::assertDispatchedTimes(GenerateProductionOrderFileJob::class, 3);
  186. $this->actingAs($this->admin)
  187. ->post(route('schedule.orders.deliveries.document', [$this->order, $otherDelivery]))
  188. ->assertNotFound();
  189. $this->actingAs($this->manager)
  190. ->post(route('schedule.orders.export-items', $this->order))
  191. ->assertForbidden();
  192. }
  193. public function test_generated_file_is_private_to_owner_and_admin(): void
  194. {
  195. $otherManager = User::factory()->manager()->create();
  196. $file = app(ProductionOrderDocumentService::class)->generate(
  197. ProductionOrderDocumentService::TYPE_ITEMS,
  198. $this->order->id,
  199. $this->manager->id,
  200. );
  201. $this->actingAs($otherManager)
  202. ->get(route('schedule.orders.files.download', $file))
  203. ->assertForbidden();
  204. $this->actingAs($this->manager)
  205. ->get(route('schedule.orders.files.download', $file))
  206. ->assertOk()
  207. ->assertHeader('content-disposition');
  208. $this->actingAs($this->admin)
  209. ->get(route('schedule.orders.files.download', $file))
  210. ->assertOk()
  211. ->assertHeader('content-disposition');
  212. }
  213. /** @return list<string> */
  214. private function zipEntries(string $path): array
  215. {
  216. $zip = new ZipArchive;
  217. $this->assertTrue($zip->open($path) === true);
  218. $entries = [];
  219. for ($index = 0; $index < $zip->numFiles; $index++) {
  220. $entries[] = $zip->getNameIndex($index);
  221. }
  222. $zip->close();
  223. return $entries;
  224. }
  225. }