OrderControllerTest.php 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674
  1. <?php
  2. namespace Tests\Feature;
  3. use App\Models\Dictionary\Area;
  4. use App\Models\Dictionary\District;
  5. use App\Models\File;
  6. use App\Models\MafOrder;
  7. use App\Models\ObjectType;
  8. use App\Models\Order;
  9. use App\Models\OrderStatus;
  10. use App\Models\Product;
  11. use App\Models\ProductSKU;
  12. use App\Models\Role;
  13. use App\Models\User;
  14. use Illuminate\Foundation\Testing\RefreshDatabase;
  15. use Illuminate\Http\UploadedFile;
  16. use Illuminate\Support\Facades\Storage;
  17. use Tests\TestCase;
  18. class OrderControllerTest extends TestCase
  19. {
  20. use RefreshDatabase;
  21. protected $seed = true;
  22. private User $adminUser;
  23. private User $managerUser;
  24. private User $brigadierUser;
  25. protected function setUp(): void
  26. {
  27. parent::setUp();
  28. $this->adminUser = User::factory()->create(['role' => Role::ADMIN]);
  29. $this->managerUser = User::factory()->create(['role' => Role::MANAGER]);
  30. $this->brigadierUser = User::factory()->create(['role' => Role::BRIGADIER]);
  31. }
  32. // ==================== Authentication ====================
  33. public function test_guest_cannot_access_orders_index(): void
  34. {
  35. $response = $this->get(route('order.index'));
  36. $response->assertRedirect(route('login'));
  37. }
  38. public function test_authenticated_user_can_access_orders_index(): void
  39. {
  40. $response = $this->actingAs($this->managerUser)
  41. ->get(route('order.index'));
  42. $response->assertStatus(200);
  43. $response->assertViewIs('orders.index');
  44. }
  45. // ==================== Index ====================
  46. public function test_orders_index_displays_orders(): void
  47. {
  48. $order = Order::factory()->create();
  49. $response = $this->actingAs($this->managerUser)
  50. ->get(route('order.index'));
  51. $response->assertStatus(200);
  52. $response->assertSee($order->object_address);
  53. }
  54. public function test_brigadier_sees_only_assigned_orders(): void
  55. {
  56. $assignedOrder = Order::factory()->create([
  57. 'brigadier_id' => $this->brigadierUser->id,
  58. ]);
  59. $otherOrder = Order::factory()->create([
  60. 'brigadier_id' => User::factory()->create(['role' => Role::BRIGADIER])->id,
  61. ]);
  62. $response = $this->actingAs($this->brigadierUser)
  63. ->get(route('order.index'));
  64. $response->assertStatus(200);
  65. $response->assertSee($assignedOrder->object_address);
  66. $response->assertDontSee($otherOrder->object_address);
  67. }
  68. // ==================== Create ====================
  69. public function test_can_view_create_order_form(): void
  70. {
  71. $response = $this->actingAs($this->adminUser)
  72. ->get(route('order.create'));
  73. $response->assertStatus(200);
  74. $response->assertViewIs('orders.edit');
  75. }
  76. // ==================== Store ====================
  77. public function test_can_create_new_order(): void
  78. {
  79. $district = District::factory()->create();
  80. $area = Area::factory()->create();
  81. $objectType = ObjectType::factory()->create();
  82. $response = $this->actingAs($this->managerUser)
  83. ->post(route('order.store'), [
  84. 'name' => 'Тестовый заказ',
  85. 'user_id' => $this->managerUser->id,
  86. 'district_id' => $district->id,
  87. 'area_id' => $area->id,
  88. 'object_address' => 'ул. Тестовая, д. 1',
  89. 'object_type_id' => $objectType->id,
  90. 'comment' => 'Тестовый комментарий',
  91. ]);
  92. $response->assertRedirect();
  93. $this->assertDatabaseHas('orders', [
  94. 'object_address' => 'ул. Тестовая, д. 1',
  95. 'order_status_id' => Order::STATUS_NEW,
  96. ]);
  97. }
  98. public function test_creating_order_sets_tg_group_name(): void
  99. {
  100. $district = District::factory()->create(['shortname' => 'ЦАО']);
  101. $area = Area::factory()->create(['name' => 'Тверской']);
  102. $objectType = ObjectType::factory()->create();
  103. $this->actingAs($this->managerUser)
  104. ->post(route('order.store'), [
  105. 'name' => 'Тестовый заказ',
  106. 'user_id' => $this->managerUser->id,
  107. 'district_id' => $district->id,
  108. 'area_id' => $area->id,
  109. 'object_address' => 'ул. Пушкина',
  110. 'object_type_id' => $objectType->id,
  111. ]);
  112. $order = Order::where('object_address', 'ул. Пушкина')->first();
  113. $this->assertStringContainsString('ЦАО', $order->tg_group_name);
  114. $this->assertStringContainsString('Тверской', $order->tg_group_name);
  115. }
  116. public function test_can_update_existing_order(): void
  117. {
  118. $order = Order::factory()->create([
  119. 'object_address' => 'Старый адрес',
  120. ]);
  121. $response = $this->actingAs($this->managerUser)
  122. ->post(route('order.store'), [
  123. 'id' => $order->id,
  124. 'name' => $order->name,
  125. 'user_id' => $order->user_id,
  126. 'district_id' => $order->district_id,
  127. 'area_id' => $order->area_id,
  128. 'object_address' => 'Новый адрес',
  129. 'object_type_id' => $order->object_type_id,
  130. ]);
  131. $response->assertRedirect();
  132. $this->assertDatabaseHas('orders', [
  133. 'id' => $order->id,
  134. 'object_address' => 'Новый адрес',
  135. ]);
  136. }
  137. // ==================== Show ====================
  138. public function test_can_view_order_details(): void
  139. {
  140. $order = Order::factory()->create();
  141. $response = $this->actingAs($this->managerUser)
  142. ->get(route('order.show', $order));
  143. $response->assertStatus(200);
  144. $response->assertViewIs('orders.show');
  145. $response->assertSee($order->object_address);
  146. }
  147. // ==================== Edit ====================
  148. public function test_can_view_edit_order_form(): void
  149. {
  150. $order = Order::factory()->create();
  151. $response = $this->actingAs($this->managerUser)
  152. ->get(route('order.edit', $order));
  153. $response->assertStatus(200);
  154. $response->assertViewIs('orders.edit');
  155. }
  156. // ==================== Destroy ====================
  157. public function test_can_delete_order(): void
  158. {
  159. $order = Order::factory()->create();
  160. $orderId = $order->id;
  161. $response = $this->actingAs($this->adminUser)
  162. ->delete(route('order.destroy', $order));
  163. $response->assertRedirect(route('order.index'));
  164. // Order uses SoftDeletes, so check deleted_at is set
  165. $this->assertSoftDeleted('orders', ['id' => $orderId]);
  166. }
  167. public function test_deleting_order_removes_related_product_skus(): void
  168. {
  169. $order = Order::factory()->create();
  170. $product = Product::factory()->create();
  171. $sku = ProductSKU::factory()->create([
  172. 'order_id' => $order->id,
  173. 'product_id' => $product->id,
  174. ]);
  175. $this->actingAs($this->adminUser)
  176. ->delete(route('order.destroy', $order));
  177. // ProductSKU uses SoftDeletes, so check deleted_at is set
  178. $this->assertSoftDeleted('products_sku', ['id' => $sku->id]);
  179. }
  180. // ==================== Search ====================
  181. public function test_search_returns_matching_orders(): void
  182. {
  183. $order = Order::factory()->create([
  184. 'object_address' => 'ул. Уникальная Тестовая, д. 999',
  185. ]);
  186. $otherOrder = Order::factory()->create([
  187. 'object_address' => 'ул. Другая, д. 1',
  188. ]);
  189. $response = $this->actingAs($this->managerUser)
  190. ->get(route('order.index', ['s' => 'Уникальная Тестовая']));
  191. $response->assertStatus(200);
  192. $response->assertSee($order->object_address);
  193. $response->assertDontSee($otherOrder->object_address);
  194. }
  195. // ==================== MAF Operations ====================
  196. public function test_get_maf_to_order_assigns_available_maf(): void
  197. {
  198. $product = Product::factory()->create();
  199. $order = Order::factory()->create();
  200. $productSku = ProductSKU::factory()->create([
  201. 'order_id' => $order->id,
  202. 'product_id' => $product->id,
  203. 'maf_order_id' => null,
  204. 'status' => 'требуется',
  205. ]);
  206. $mafOrder = MafOrder::factory()->create([
  207. 'product_id' => $product->id,
  208. 'in_stock' => 5,
  209. ]);
  210. // This route requires admin role
  211. $response = $this->actingAs($this->adminUser)
  212. ->get(route('order.get-maf', $order));
  213. $response->assertRedirect(route('order.show', $order));
  214. $response->assertSessionHas('success', function ($messages) {
  215. return str_contains(implode(' ', (array) $messages), 'Привязано МАФ: 1.');
  216. });
  217. $productSku->refresh();
  218. $this->assertEquals($mafOrder->id, $productSku->maf_order_id);
  219. $this->assertEquals('отгружен', $productSku->status);
  220. $mafOrder->refresh();
  221. $this->assertEquals(4, $mafOrder->in_stock);
  222. }
  223. public function test_get_maf_to_order_shows_error_when_not_enough_stock(): void
  224. {
  225. $product = Product::factory()->create();
  226. $order = Order::factory()->create();
  227. $productSku = ProductSKU::factory()->create([
  228. 'order_id' => $order->id,
  229. 'product_id' => $product->id,
  230. 'maf_order_id' => null,
  231. 'status' => 'требуется',
  232. ]);
  233. $response = $this->actingAs($this->adminUser)
  234. ->get(route('order.get-maf', $order));
  235. $response->assertRedirect(route('order.show', $order));
  236. $response->assertSessionHas('danger', function ($messages) {
  237. return str_contains(implode(' ', (array) $messages), 'Не удалось привязать 1 шт. МАФ');
  238. });
  239. $productSku->refresh();
  240. $this->assertNull($productSku->maf_order_id);
  241. $this->assertEquals('требуется', $productSku->status);
  242. }
  243. public function test_revert_maf_returns_maf_to_stock(): void
  244. {
  245. $product = Product::factory()->create();
  246. $order = Order::factory()->create();
  247. $mafOrder = MafOrder::factory()->create([
  248. 'product_id' => $product->id,
  249. 'in_stock' => 3,
  250. ]);
  251. $productSku = ProductSKU::factory()->create([
  252. 'order_id' => $order->id,
  253. 'product_id' => $product->id,
  254. 'maf_order_id' => $mafOrder->id,
  255. 'status' => 'отгружен',
  256. ]);
  257. // This route requires admin role
  258. $response = $this->actingAs($this->adminUser)
  259. ->get(route('order.revert-maf', $order));
  260. $response->assertRedirect(route('order.show', $order));
  261. $response->assertSessionHas('success', function ($messages) {
  262. return str_contains(implode(' ', (array) $messages), 'Отвязано МАФ: 1.');
  263. });
  264. $productSku->refresh();
  265. $this->assertNull($productSku->maf_order_id);
  266. $this->assertEquals('требуется', $productSku->status);
  267. $mafOrder->refresh();
  268. $this->assertEquals(4, $mafOrder->in_stock);
  269. }
  270. public function test_revert_maf_shows_error_when_nothing_is_attached(): void
  271. {
  272. $order = Order::factory()->create();
  273. ProductSKU::factory()->create([
  274. 'order_id' => $order->id,
  275. 'maf_order_id' => null,
  276. 'status' => 'требуется',
  277. ]);
  278. $response = $this->actingAs($this->adminUser)
  279. ->get(route('order.revert-maf', $order));
  280. $response->assertRedirect(route('order.show', $order));
  281. $response->assertSessionHas('danger', function ($messages) {
  282. return str_contains(implode(' ', (array) $messages), 'Нет МАФ для отвязки');
  283. });
  284. }
  285. public function test_store_order_cannot_remove_attached_maf_from_order_list(): void
  286. {
  287. $attachedProduct = Product::factory()->create();
  288. $otherProduct = Product::factory()->create();
  289. $order = Order::factory()->create([
  290. 'order_status_id' => Order::STATUS_NEW,
  291. ]);
  292. $mafOrder = MafOrder::factory()->create([
  293. 'product_id' => $attachedProduct->id,
  294. 'in_stock' => 1,
  295. ]);
  296. $attachedSku = ProductSKU::factory()->create([
  297. 'order_id' => $order->id,
  298. 'product_id' => $attachedProduct->id,
  299. 'maf_order_id' => $mafOrder->id,
  300. 'status' => 'отгружен',
  301. ]);
  302. ProductSKU::factory()->create([
  303. 'order_id' => $order->id,
  304. 'product_id' => $otherProduct->id,
  305. 'maf_order_id' => null,
  306. 'status' => 'требуется',
  307. ]);
  308. $response = $this->actingAs($this->adminUser)
  309. ->post(route('order.store'), [
  310. 'id' => $order->id,
  311. 'products' => [$otherProduct->id],
  312. 'quantity' => [1],
  313. ]);
  314. $response->assertRedirect();
  315. $response->assertSessionHas('danger', function ($messages) {
  316. return str_contains(implode(' ', (array) $messages), 'Нельзя удалить привязанные МАФ');
  317. });
  318. $this->assertDatabaseHas('products_sku', [
  319. 'id' => $attachedSku->id,
  320. 'maf_order_id' => $mafOrder->id,
  321. 'deleted_at' => null,
  322. ]);
  323. }
  324. public function test_store_order_cannot_add_new_positions_when_has_attached_maf(): void
  325. {
  326. $attachedProduct = Product::factory()->create();
  327. $existingProduct = Product::factory()->create();
  328. $newProduct = Product::factory()->create();
  329. $order = Order::factory()->create([
  330. 'order_status_id' => Order::STATUS_NEW,
  331. ]);
  332. $mafOrder = MafOrder::factory()->create([
  333. 'product_id' => $attachedProduct->id,
  334. 'in_stock' => 1,
  335. ]);
  336. ProductSKU::factory()->create([
  337. 'order_id' => $order->id,
  338. 'product_id' => $attachedProduct->id,
  339. 'maf_order_id' => $mafOrder->id,
  340. 'status' => 'отгружен',
  341. ]);
  342. ProductSKU::factory()->create([
  343. 'order_id' => $order->id,
  344. 'product_id' => $existingProduct->id,
  345. 'maf_order_id' => null,
  346. 'status' => 'требуется',
  347. ]);
  348. $response = $this->actingAs($this->adminUser)
  349. ->post(route('order.store'), [
  350. 'id' => $order->id,
  351. 'products' => [$attachedProduct->id, $existingProduct->id, $newProduct->id],
  352. 'quantity' => [1, 1, 1],
  353. ]);
  354. $response->assertRedirect();
  355. $response->assertSessionHas('danger', function ($messages) {
  356. return str_contains(implode(' ', (array) $messages), 'Нельзя добавлять новые позиции МАФ');
  357. });
  358. $this->assertDatabaseMissing('products_sku', [
  359. 'order_id' => $order->id,
  360. 'product_id' => $newProduct->id,
  361. 'deleted_at' => null,
  362. ]);
  363. }
  364. public function test_move_maf_transfers_sku_to_another_order(): void
  365. {
  366. $product = Product::factory()->create();
  367. $order1 = Order::factory()->create();
  368. $order2 = Order::factory()->create();
  369. $productSku = ProductSKU::factory()->create([
  370. 'order_id' => $order1->id,
  371. 'product_id' => $product->id,
  372. ]);
  373. // This route requires admin role
  374. $response = $this->actingAs($this->adminUser)
  375. ->post(route('order.move-maf'), [
  376. 'new_order_id' => $order2->id,
  377. 'ids' => json_encode([$productSku->id]),
  378. ]);
  379. $response->assertStatus(200);
  380. $response->assertJson(['success' => true]);
  381. $productSku->refresh();
  382. $this->assertEquals($order2->id, $productSku->order_id);
  383. }
  384. // ==================== Photo Management ====================
  385. public function test_can_upload_photo_to_order(): void
  386. {
  387. Storage::fake('public');
  388. $order = Order::factory()->create();
  389. // Use create() instead of image() to avoid GD extension requirement
  390. $photo = UploadedFile::fake()->create('photo.jpg', 100, 'image/jpeg');
  391. $response = $this->actingAs($this->managerUser)
  392. ->post(route('order.upload-photo', $order), [
  393. 'photo' => [$photo],
  394. ]);
  395. $response->assertRedirect(route('order.show', $order));
  396. $this->assertCount(1, $order->fresh()->photos);
  397. }
  398. public function test_can_delete_photo_from_order(): void
  399. {
  400. Storage::fake('public');
  401. $order = Order::factory()->create();
  402. $file = File::factory()->create();
  403. $order->photos()->attach($file);
  404. // This route requires admin role
  405. $response = $this->actingAs($this->adminUser)
  406. ->delete(route('order.delete-photo', [$order, $file]));
  407. $response->assertRedirect(route('order.show', $order));
  408. $this->assertCount(0, $order->fresh()->photos);
  409. }
  410. public function test_can_delete_all_photos_from_order(): void
  411. {
  412. Storage::fake('public');
  413. $order = Order::factory()->create();
  414. $files = File::factory()->count(3)->create();
  415. $order->photos()->attach($files->pluck('id'));
  416. // This route requires admin role
  417. $response = $this->actingAs($this->adminUser)
  418. ->delete(route('order.delete-all-photos', $order));
  419. $response->assertRedirect(route('order.show', $order));
  420. $this->assertCount(0, $order->fresh()->photos);
  421. }
  422. // ==================== Document Management ====================
  423. public function test_can_upload_document_to_order(): void
  424. {
  425. Storage::fake('public');
  426. $order = Order::factory()->create();
  427. $document = UploadedFile::fake()->create('document.pdf', 100);
  428. $response = $this->actingAs($this->managerUser)
  429. ->post(route('order.upload-document', $order), [
  430. 'document' => [$document],
  431. ]);
  432. $response->assertRedirect(route('order.show', $order));
  433. $this->assertCount(1, $order->fresh()->documents);
  434. }
  435. public function test_upload_document_preserves_unicode_and_quotes_filename(): void
  436. {
  437. Storage::fake('public');
  438. $order = Order::factory()->create();
  439. $filename = "Документ «смета» 'финал' \"v2\".pdf";
  440. $document = UploadedFile::fake()->create($filename, 100, 'application/pdf');
  441. $response = $this->actingAs($this->managerUser)
  442. ->post(route('order.upload-document', $order), [
  443. 'document' => [$document],
  444. ]);
  445. $response->assertRedirect(route('order.show', $order));
  446. $saved = $order->fresh()->documents->first();
  447. $this->assertNotNull($saved);
  448. $this->assertEquals($filename, $saved->original_name);
  449. $this->assertEquals('orders/' . $order->id . '/document/' . $filename, $saved->path);
  450. Storage::disk('public')->assertExists($saved->path);
  451. }
  452. public function test_upload_statement_preserves_unicode_and_quotes_filename(): void
  453. {
  454. Storage::fake('public');
  455. $order = Order::factory()->create();
  456. $filename = "Акт «приемки» 'этап 1' \"финал\".pdf";
  457. $statement = UploadedFile::fake()->create($filename, 100, 'application/pdf');
  458. $response = $this->actingAs($this->managerUser)
  459. ->post(route('order.upload-statement', $order), [
  460. 'statement' => [$statement],
  461. ]);
  462. $response->assertRedirect(route('order.show', $order));
  463. $saved = $order->fresh()->statements->first();
  464. $this->assertNotNull($saved);
  465. $this->assertEquals($filename, $saved->original_name);
  466. $this->assertEquals('orders/' . $order->id . '/statement/' . $filename, $saved->path);
  467. Storage::disk('public')->assertExists($saved->path);
  468. }
  469. public function test_upload_document_limits_to_5_files(): void
  470. {
  471. Storage::fake('public');
  472. $order = Order::factory()->create();
  473. $documents = [];
  474. for ($i = 0; $i < 7; $i++) {
  475. $documents[] = UploadedFile::fake()->create("document{$i}.pdf", 100);
  476. }
  477. $this->actingAs($this->managerUser)
  478. ->post(route('order.upload-document', $order), [
  479. 'document' => $documents,
  480. ]);
  481. $this->assertCount(5, $order->fresh()->documents);
  482. }
  483. // ==================== Generation ====================
  484. public function test_generate_installation_pack_requires_correct_status(): void
  485. {
  486. $order = Order::factory()->create([
  487. 'order_status_id' => Order::STATUS_NEW,
  488. ]);
  489. $response = $this->actingAs($this->managerUser)
  490. ->get(route('order.generate-installation-pack', $order));
  491. $response->assertRedirect(route('order.show', $order));
  492. $response->assertSessionHas('danger');
  493. }
  494. public function test_generate_installation_pack_succeeds_with_correct_status(): void
  495. {
  496. $product = Product::factory()->create();
  497. $mafOrder = MafOrder::factory()->create(['product_id' => $product->id]);
  498. $order = Order::factory()->create([
  499. 'order_status_id' => Order::STATUS_READY_TO_MOUNT,
  500. ]);
  501. // Create SKU with MAF assigned
  502. ProductSKU::factory()->create([
  503. 'order_id' => $order->id,
  504. 'product_id' => $product->id,
  505. 'maf_order_id' => $mafOrder->id,
  506. ]);
  507. $response = $this->actingAs($this->managerUser)
  508. ->get(route('order.generate-installation-pack', $order));
  509. $response->assertRedirect(route('order.show', $order));
  510. $response->assertSessionHas('success');
  511. }
  512. // ==================== Export ====================
  513. public function test_can_export_orders(): void
  514. {
  515. Order::factory()->count(3)->create();
  516. // This route requires admin role
  517. $response = $this->actingAs($this->adminUser)
  518. ->post(route('order.export'));
  519. $response->assertRedirect(route('order.index'));
  520. $response->assertSessionHas('success');
  521. }
  522. public function test_can_export_single_order(): void
  523. {
  524. $order = Order::factory()->create();
  525. // This route requires admin role
  526. $response = $this->actingAs($this->adminUser)
  527. ->post(route('order.export-one', $order));
  528. $response->assertRedirect(route('order.show', $order));
  529. $response->assertSessionHas('success');
  530. }
  531. }