ProductionOrderDocumentControllerTest.php 9.1 KB

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