OrderControllerTest.php 31 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907
  1. <?php
  2. namespace Tests\Feature;
  3. use App\Jobs\GenerateInstallationPack;
  4. use App\Models\Dictionary\Area;
  5. use App\Models\Dictionary\District;
  6. use App\Models\File;
  7. use App\Models\MafOrder;
  8. use App\Models\ObjectType;
  9. use App\Models\Order;
  10. use App\Models\OrderStatus;
  11. use App\Models\Product;
  12. use App\Models\ProductSKU;
  13. use App\Models\Role;
  14. use App\Models\User;
  15. use Illuminate\Foundation\Testing\RefreshDatabase;
  16. use Illuminate\Http\UploadedFile;
  17. use Illuminate\Support\Facades\Bus;
  18. use Illuminate\Support\Facades\Storage;
  19. use Tests\TestCase;
  20. class OrderControllerTest extends TestCase
  21. {
  22. use RefreshDatabase;
  23. protected $seed = true;
  24. private User $adminUser;
  25. private User $managerUser;
  26. private User $brigadierUser;
  27. protected function setUp(): void
  28. {
  29. parent::setUp();
  30. $this->adminUser = User::factory()->create(['role' => Role::ADMIN]);
  31. $this->managerUser = User::factory()->create(['role' => Role::MANAGER]);
  32. $this->brigadierUser = User::factory()->create(['role' => Role::BRIGADIER]);
  33. }
  34. // ==================== Authentication ====================
  35. public function test_guest_cannot_access_orders_index(): void
  36. {
  37. $response = $this->get(route('order.index'));
  38. $response->assertRedirect(route('login'));
  39. }
  40. public function test_authenticated_user_can_access_orders_index(): void
  41. {
  42. $response = $this->actingAs($this->managerUser)
  43. ->get(route('order.index'));
  44. $response->assertStatus(200);
  45. $response->assertViewIs('orders.index');
  46. }
  47. // ==================== Index ====================
  48. public function test_orders_index_displays_orders(): void
  49. {
  50. $order = Order::factory()->create([
  51. 'object_address' => 'ул. Проверочная, д. 101',
  52. ]);
  53. $response = $this->actingAs($this->managerUser)
  54. ->get(route('order.index'));
  55. $response->assertStatus(200);
  56. $response->assertSee($order->object_address);
  57. }
  58. public function test_orders_index_displays_products_total_column_value(): void
  59. {
  60. $order = Order::factory()->create([
  61. 'object_address' => 'ул. Колоночная, д. 10',
  62. ]);
  63. $product = Product::factory()->create();
  64. ProductSKU::factory()->count(3)->create([
  65. 'order_id' => $order->id,
  66. 'product_id' => $product->id,
  67. ]);
  68. $response = $this->actingAs($this->managerUser)
  69. ->get(route('order.index'));
  70. $response->assertOk();
  71. $response->assertSee('Всего МАФ');
  72. $response->assertSee('ул. Колоночная, д. 10');
  73. $response->assertViewHas('orders', static function ($orders) use ($order) {
  74. return (int) $orders->firstWhere('id', $order->id)?->products_total === 3;
  75. });
  76. }
  77. public function test_orders_index_displays_filtered_footer_stats(): void
  78. {
  79. $district = District::factory()->create(['shortname' => 'ЦАО']);
  80. $area = Area::factory()->create(['district_id' => $district->id, 'name' => 'Тверской']);
  81. $matchingOrder = Order::factory()->create([
  82. 'district_id' => $district->id,
  83. 'area_id' => $area->id,
  84. 'object_address' => 'ул. Итоговая, д. 1',
  85. ]);
  86. $otherOrder = Order::factory()->create([
  87. 'object_address' => 'ул. Неучтенная, д. 2',
  88. ]);
  89. $product = Product::factory()->create();
  90. ProductSKU::factory()->count(2)->create([
  91. 'order_id' => $matchingOrder->id,
  92. 'product_id' => $product->id,
  93. ]);
  94. ProductSKU::factory()->count(5)->create([
  95. 'order_id' => $otherOrder->id,
  96. 'product_id' => $product->id,
  97. ]);
  98. $response = $this->actingAs($this->managerUser)
  99. ->get(route('order.index', ['s' => 'Итоговая']));
  100. $response->assertOk();
  101. $response->assertSee('Адресов: 1', false);
  102. $response->assertSee('МАФ: 2', false);
  103. $response->assertDontSee('МАФ: 7', false);
  104. }
  105. public function test_brigadier_sees_only_assigned_orders(): void
  106. {
  107. $assignedOrder = Order::factory()->create([
  108. 'brigadier_id' => $this->brigadierUser->id,
  109. 'object_address' => 'ул. Бригадирская, д. 10',
  110. ]);
  111. $otherOrder = Order::factory()->create([
  112. 'brigadier_id' => User::factory()->create(['role' => Role::BRIGADIER])->id,
  113. 'object_address' => 'ул. Чужая, д. 20',
  114. ]);
  115. $response = $this->actingAs($this->brigadierUser)
  116. ->get(route('order.index'));
  117. $response->assertStatus(200);
  118. $response->assertSee($assignedOrder->object_address);
  119. $response->assertDontSee($otherOrder->object_address);
  120. }
  121. public function test_brigadier_does_not_see_handed_over_orders_in_index(): void
  122. {
  123. $visibleOrder = Order::factory()->create([
  124. 'brigadier_id' => $this->brigadierUser->id,
  125. 'order_status_id' => Order::STATUS_IN_MOUNT,
  126. 'object_address' => 'ул. Видимая, д. 11',
  127. ]);
  128. $hiddenOrder = Order::factory()->create([
  129. 'brigadier_id' => $this->brigadierUser->id,
  130. 'order_status_id' => Order::STATUS_HANDED_OVER,
  131. 'object_address' => 'ул. Сданная, д. 12',
  132. ]);
  133. $response = $this->actingAs($this->brigadierUser)
  134. ->get(route('order.index'));
  135. $response->assertStatus(200);
  136. $response->assertSee($visibleOrder->object_address);
  137. $response->assertDontSee($hiddenOrder->object_address);
  138. }
  139. // ==================== Create ====================
  140. public function test_can_view_create_order_form(): void
  141. {
  142. $response = $this->actingAs($this->adminUser)
  143. ->get(route('order.create'));
  144. $response->assertStatus(200);
  145. $response->assertViewIs('orders.edit');
  146. }
  147. // ==================== Store ====================
  148. public function test_can_create_new_order(): void
  149. {
  150. $district = District::factory()->create();
  151. $area = Area::factory()->create();
  152. $objectType = ObjectType::factory()->create();
  153. $response = $this->actingAs($this->managerUser)
  154. ->post(route('order.store'), [
  155. 'name' => 'Тестовый заказ',
  156. 'user_id' => $this->managerUser->id,
  157. 'district_id' => $district->id,
  158. 'area_id' => $area->id,
  159. 'object_address' => 'ул. Тестовая, д. 1',
  160. 'object_type_id' => $objectType->id,
  161. 'comment' => 'Тестовый комментарий',
  162. ]);
  163. $response->assertRedirect();
  164. $this->assertDatabaseHas('orders', [
  165. 'object_address' => 'ул. Тестовая, д. 1',
  166. 'order_status_id' => Order::STATUS_NEW,
  167. ]);
  168. }
  169. public function test_creating_order_sets_tg_group_name(): void
  170. {
  171. $district = District::factory()->create(['shortname' => 'ЦАО']);
  172. $area = Area::factory()->create(['name' => 'Тверской']);
  173. $objectType = ObjectType::factory()->create();
  174. $this->actingAs($this->managerUser)
  175. ->post(route('order.store'), [
  176. 'name' => 'Тестовый заказ',
  177. 'user_id' => $this->managerUser->id,
  178. 'district_id' => $district->id,
  179. 'area_id' => $area->id,
  180. 'object_address' => 'ул. Пушкина',
  181. 'object_type_id' => $objectType->id,
  182. ]);
  183. $order = Order::where('object_address', 'ул. Пушкина')->first();
  184. $this->assertStringContainsString('ЦАО', $order->tg_group_name);
  185. $this->assertStringContainsString('Тверской', $order->tg_group_name);
  186. }
  187. public function test_can_update_existing_order(): void
  188. {
  189. $order = Order::factory()->create([
  190. 'object_address' => 'Старый адрес',
  191. ]);
  192. $response = $this->actingAs($this->managerUser)
  193. ->post(route('order.store'), [
  194. 'id' => $order->id,
  195. 'name' => $order->name,
  196. 'user_id' => $order->user_id,
  197. 'district_id' => $order->district_id,
  198. 'area_id' => $order->area_id,
  199. 'object_address' => 'Новый адрес',
  200. 'object_type_id' => $order->object_type_id,
  201. ]);
  202. $response->assertRedirect();
  203. $this->assertDatabaseHas('orders', [
  204. 'id' => $order->id,
  205. 'object_address' => 'Новый адрес',
  206. ]);
  207. }
  208. public function test_manager_can_update_ready_date(): void
  209. {
  210. $order = Order::factory()->create([
  211. 'ready_date' => '2026-04-01',
  212. ]);
  213. $response = $this->actingAs($this->managerUser)
  214. ->post(route('order.store'), [
  215. 'id' => $order->id,
  216. 'ready_date' => '2026-04-25',
  217. ]);
  218. $response->assertRedirect();
  219. $this->assertDatabaseHas('orders', [
  220. 'id' => $order->id,
  221. 'ready_date' => '2026-04-25',
  222. ]);
  223. }
  224. // ==================== Show ====================
  225. public function test_can_view_order_details(): void
  226. {
  227. $order = Order::factory()->create([
  228. 'object_address' => 'ул. Детальная, д. 5',
  229. ]);
  230. $response = $this->actingAs($this->managerUser)
  231. ->get(route('order.show', $order));
  232. $response->assertStatus(200);
  233. $response->assertViewIs('orders.show');
  234. $response->assertSee($order->object_address);
  235. }
  236. public function test_order_details_show_area_and_district(): void
  237. {
  238. $district = District::factory()->create(['name' => 'Центральный округ']);
  239. $area = Area::factory()->create([
  240. 'district_id' => $district->id,
  241. 'name' => 'Тверской район',
  242. ]);
  243. $order = Order::factory()->create([
  244. 'district_id' => $district->id,
  245. 'area_id' => $area->id,
  246. ]);
  247. $response = $this->actingAs($this->managerUser)
  248. ->get(route('order.show', $order));
  249. $response->assertOk();
  250. $response->assertSee('Центральный округ');
  251. $response->assertSee('Тверской район');
  252. }
  253. public function test_brigadier_cannot_view_handed_over_order_details(): void
  254. {
  255. $order = Order::factory()->create([
  256. 'brigadier_id' => $this->brigadierUser->id,
  257. 'order_status_id' => Order::STATUS_HANDED_OVER,
  258. ]);
  259. $response = $this->actingAs($this->brigadierUser)
  260. ->get(route('order.show', $order));
  261. $response->assertStatus(403);
  262. }
  263. // ==================== Edit ====================
  264. public function test_can_view_edit_order_form(): void
  265. {
  266. $order = Order::factory()->create();
  267. $response = $this->actingAs($this->managerUser)
  268. ->get(route('order.edit', $order));
  269. $response->assertStatus(200);
  270. $response->assertViewIs('orders.edit');
  271. }
  272. public function test_manager_can_edit_ready_date_in_order_edit_form(): void
  273. {
  274. $order = Order::factory()->create();
  275. $response = $this->actingAs($this->managerUser)
  276. ->get(route('order.edit', $order));
  277. $response->assertOk();
  278. $this->assertMatchesRegularExpression(
  279. '/<input[^>]*name="ready_date"(?:(?!disabled).)*>/s',
  280. $response->getContent()
  281. );
  282. }
  283. // ==================== Destroy ====================
  284. public function test_can_delete_order(): void
  285. {
  286. $order = Order::factory()->create();
  287. $orderId = $order->id;
  288. $response = $this->actingAs($this->adminUser)
  289. ->delete(route('order.destroy', $order));
  290. $response->assertRedirect(route('order.index'));
  291. // Order uses SoftDeletes, so check deleted_at is set
  292. $this->assertSoftDeleted('orders', ['id' => $orderId]);
  293. }
  294. public function test_deleting_order_removes_related_product_skus(): void
  295. {
  296. $order = Order::factory()->create();
  297. $product = Product::factory()->create();
  298. $sku = ProductSKU::factory()->create([
  299. 'order_id' => $order->id,
  300. 'product_id' => $product->id,
  301. ]);
  302. $this->actingAs($this->adminUser)
  303. ->delete(route('order.destroy', $order));
  304. // ProductSKU uses SoftDeletes, so check deleted_at is set
  305. $this->assertSoftDeleted('products_sku', ['id' => $sku->id]);
  306. }
  307. // ==================== Search ====================
  308. public function test_search_returns_matching_orders(): void
  309. {
  310. $order = Order::factory()->create([
  311. 'object_address' => 'ул. Уникальная Тестовая, д. 999',
  312. ]);
  313. $otherOrder = Order::factory()->create([
  314. 'object_address' => 'ул. Другая, д. 1',
  315. ]);
  316. $response = $this->actingAs($this->managerUser)
  317. ->get(route('order.index', ['s' => 'Уникальная Тестовая']));
  318. $response->assertStatus(200);
  319. $response->assertSee($order->object_address);
  320. $response->assertDontSee($otherOrder->object_address);
  321. }
  322. public function test_order_search_route_can_search_by_manager_name(): void
  323. {
  324. $manager = User::factory()->create([
  325. 'role' => Role::MANAGER,
  326. 'name' => 'Менеджер Поиска',
  327. ]);
  328. $matchedOrder = Order::factory()->create([
  329. 'user_id' => $manager->id,
  330. 'object_address' => 'ул. Найденная, д. 7',
  331. ]);
  332. $otherOrder = Order::factory()->create([
  333. 'object_address' => 'ул. Не должна попасть, д. 8',
  334. ]);
  335. $response = $this->actingAs($this->adminUser)
  336. ->getJson(route('order.search', ['s' => 'Менеджер Поиска']));
  337. $response->assertOk();
  338. $response->assertJsonPath((string) $matchedOrder->id, $matchedOrder->move_maf_name);
  339. $response->assertJsonMissing([$otherOrder->id => $otherOrder->move_maf_name]);
  340. }
  341. public function test_order_search_route_includes_site_name_and_excludes_current_order(): void
  342. {
  343. $district = District::factory()->create(['name' => 'Северный округ']);
  344. $area = Area::factory()->create([
  345. 'district_id' => $district->id,
  346. 'name' => 'Левобережный',
  347. ]);
  348. $currentOrder = Order::factory()->create([
  349. 'district_id' => $district->id,
  350. 'area_id' => $area->id,
  351. 'name' => 'Текущая площадка',
  352. 'object_address' => 'ул. Проверочная, д. 1',
  353. ]);
  354. $targetOrder = Order::factory()->create([
  355. 'district_id' => $district->id,
  356. 'area_id' => $area->id,
  357. 'name' => 'Площадка назначения',
  358. 'object_address' => 'ул. Проверочная, д. 2',
  359. ]);
  360. $response = $this->actingAs($this->adminUser)
  361. ->getJson(route('order.search', [
  362. 's' => 'Проверочная',
  363. 'current_order_id' => $currentOrder->id,
  364. ]));
  365. $response->assertOk();
  366. $response->assertJsonPath((string) $targetOrder->id, $targetOrder->move_maf_name);
  367. $response->assertJsonMissing([(string) $currentOrder->id => $currentOrder->move_maf_name]);
  368. }
  369. // ==================== MAF Operations ====================
  370. public function test_get_maf_to_order_assigns_available_maf(): void
  371. {
  372. $product = Product::factory()->create();
  373. $order = Order::factory()->create();
  374. $productSku = ProductSKU::factory()->create([
  375. 'order_id' => $order->id,
  376. 'product_id' => $product->id,
  377. 'maf_order_id' => null,
  378. 'status' => 'требуется',
  379. ]);
  380. $mafOrder = MafOrder::factory()->create([
  381. 'product_id' => $product->id,
  382. 'in_stock' => 5,
  383. ]);
  384. // This route requires admin role
  385. $response = $this->actingAs($this->adminUser)
  386. ->get(route('order.get-maf', $order));
  387. $response->assertRedirect(route('order.show', $order));
  388. $response->assertSessionHas('success', function ($messages) {
  389. return str_contains(implode(' ', (array) $messages), 'Привязано МАФ: 1.');
  390. });
  391. $productSku->refresh();
  392. $this->assertEquals($mafOrder->id, $productSku->maf_order_id);
  393. $this->assertEquals('отгружен', $productSku->status);
  394. $mafOrder->refresh();
  395. $this->assertEquals(4, $mafOrder->in_stock);
  396. }
  397. public function test_get_maf_to_order_shows_error_when_not_enough_stock(): void
  398. {
  399. $product = Product::factory()->create();
  400. $order = Order::factory()->create();
  401. $productSku = ProductSKU::factory()->create([
  402. 'order_id' => $order->id,
  403. 'product_id' => $product->id,
  404. 'maf_order_id' => null,
  405. 'status' => 'требуется',
  406. ]);
  407. $response = $this->actingAs($this->adminUser)
  408. ->get(route('order.get-maf', $order));
  409. $response->assertRedirect(route('order.show', $order));
  410. $response->assertSessionHas('danger', function ($messages) {
  411. return str_contains(implode(' ', (array) $messages), 'Не удалось привязать 1 шт. МАФ');
  412. });
  413. $productSku->refresh();
  414. $this->assertNull($productSku->maf_order_id);
  415. $this->assertEquals('требуется', $productSku->status);
  416. }
  417. public function test_revert_maf_returns_maf_to_stock(): void
  418. {
  419. $product = Product::factory()->create();
  420. $order = Order::factory()->create();
  421. $mafOrder = MafOrder::factory()->create([
  422. 'product_id' => $product->id,
  423. 'in_stock' => 3,
  424. ]);
  425. $productSku = ProductSKU::factory()->create([
  426. 'order_id' => $order->id,
  427. 'product_id' => $product->id,
  428. 'maf_order_id' => $mafOrder->id,
  429. 'status' => 'отгружен',
  430. ]);
  431. // This route requires admin role
  432. $response = $this->actingAs($this->adminUser)
  433. ->get(route('order.revert-maf', $order));
  434. $response->assertRedirect(route('order.show', $order));
  435. $response->assertSessionHas('success', function ($messages) {
  436. return str_contains(implode(' ', (array) $messages), 'Отвязано МАФ: 1.');
  437. });
  438. $productSku->refresh();
  439. $this->assertNull($productSku->maf_order_id);
  440. $this->assertEquals('требуется', $productSku->status);
  441. $mafOrder->refresh();
  442. $this->assertEquals(4, $mafOrder->in_stock);
  443. }
  444. public function test_revert_maf_shows_error_when_nothing_is_attached(): void
  445. {
  446. $order = Order::factory()->create();
  447. ProductSKU::factory()->create([
  448. 'order_id' => $order->id,
  449. 'maf_order_id' => null,
  450. 'status' => 'требуется',
  451. ]);
  452. $response = $this->actingAs($this->adminUser)
  453. ->get(route('order.revert-maf', $order));
  454. $response->assertRedirect(route('order.show', $order));
  455. $response->assertSessionHas('danger', function ($messages) {
  456. return str_contains(implode(' ', (array) $messages), 'Нет МАФ для отвязки');
  457. });
  458. }
  459. public function test_store_order_cannot_remove_attached_maf_from_order_list(): void
  460. {
  461. $attachedProduct = Product::factory()->create();
  462. $otherProduct = Product::factory()->create();
  463. $order = Order::factory()->create([
  464. 'order_status_id' => Order::STATUS_NEW,
  465. ]);
  466. $mafOrder = MafOrder::factory()->create([
  467. 'product_id' => $attachedProduct->id,
  468. 'in_stock' => 1,
  469. ]);
  470. $attachedSku = ProductSKU::factory()->create([
  471. 'order_id' => $order->id,
  472. 'product_id' => $attachedProduct->id,
  473. 'maf_order_id' => $mafOrder->id,
  474. 'status' => 'отгружен',
  475. ]);
  476. ProductSKU::factory()->create([
  477. 'order_id' => $order->id,
  478. 'product_id' => $otherProduct->id,
  479. 'maf_order_id' => null,
  480. 'status' => 'требуется',
  481. ]);
  482. $response = $this->actingAs($this->adminUser)
  483. ->post(route('order.store'), [
  484. 'id' => $order->id,
  485. 'products' => [$otherProduct->id],
  486. 'quantity' => [1],
  487. ]);
  488. $response->assertRedirect();
  489. $response->assertSessionHas('danger', function ($messages) {
  490. return str_contains(implode(' ', (array) $messages), 'Нельзя удалить привязанные МАФ');
  491. });
  492. $this->assertDatabaseHas('products_sku', [
  493. 'id' => $attachedSku->id,
  494. 'maf_order_id' => $mafOrder->id,
  495. 'deleted_at' => null,
  496. ]);
  497. }
  498. public function test_store_order_cannot_add_new_positions_when_has_attached_maf(): void
  499. {
  500. $attachedProduct = Product::factory()->create();
  501. $existingProduct = Product::factory()->create();
  502. $newProduct = Product::factory()->create();
  503. $order = Order::factory()->create([
  504. 'order_status_id' => Order::STATUS_NEW,
  505. ]);
  506. $mafOrder = MafOrder::factory()->create([
  507. 'product_id' => $attachedProduct->id,
  508. 'in_stock' => 1,
  509. ]);
  510. ProductSKU::factory()->create([
  511. 'order_id' => $order->id,
  512. 'product_id' => $attachedProduct->id,
  513. 'maf_order_id' => $mafOrder->id,
  514. 'status' => 'отгружен',
  515. ]);
  516. ProductSKU::factory()->create([
  517. 'order_id' => $order->id,
  518. 'product_id' => $existingProduct->id,
  519. 'maf_order_id' => null,
  520. 'status' => 'требуется',
  521. ]);
  522. $response = $this->actingAs($this->adminUser)
  523. ->post(route('order.store'), [
  524. 'id' => $order->id,
  525. 'products' => [$attachedProduct->id, $existingProduct->id, $newProduct->id],
  526. 'quantity' => [1, 1, 1],
  527. ]);
  528. $response->assertRedirect();
  529. $response->assertSessionHas('danger', function ($messages) {
  530. return str_contains(implode(' ', (array) $messages), 'Нельзя добавлять новые позиции МАФ');
  531. });
  532. $this->assertDatabaseMissing('products_sku', [
  533. 'order_id' => $order->id,
  534. 'product_id' => $newProduct->id,
  535. 'deleted_at' => null,
  536. ]);
  537. }
  538. public function test_move_maf_transfers_sku_to_another_order(): void
  539. {
  540. $product = Product::factory()->create();
  541. $order1 = Order::factory()->create();
  542. $order2 = Order::factory()->create();
  543. $productSku = ProductSKU::factory()->create([
  544. 'order_id' => $order1->id,
  545. 'product_id' => $product->id,
  546. ]);
  547. // This route requires admin role
  548. $response = $this->actingAs($this->adminUser)
  549. ->post(route('order.move-maf'), [
  550. 'new_order_id' => $order2->id,
  551. 'ids' => json_encode([$productSku->id]),
  552. ]);
  553. $response->assertStatus(200);
  554. $response->assertJson(['success' => true]);
  555. $productSku->refresh();
  556. $this->assertEquals($order2->id, $productSku->order_id);
  557. }
  558. // ==================== Photo Management ====================
  559. public function test_can_upload_photo_to_order(): void
  560. {
  561. Storage::fake('public');
  562. $order = Order::factory()->create();
  563. // Use create() instead of image() to avoid GD extension requirement
  564. $photo = UploadedFile::fake()->create('photo.jpg', 100, 'image/jpeg');
  565. $response = $this->actingAs($this->managerUser)
  566. ->post(route('order.upload-photo', $order), [
  567. 'photo' => [$photo],
  568. ]);
  569. $response->assertRedirect(route('order.show', $order));
  570. $this->assertCount(1, $order->fresh()->photos);
  571. }
  572. public function test_can_delete_photo_from_order(): void
  573. {
  574. Storage::fake('public');
  575. $order = Order::factory()->create();
  576. $file = File::factory()->create();
  577. $order->photos()->attach($file);
  578. // This route requires admin role
  579. $response = $this->actingAs($this->adminUser)
  580. ->delete(route('order.delete-photo', [$order, $file]));
  581. $response->assertRedirect(route('order.show', $order));
  582. $this->assertCount(0, $order->fresh()->photos);
  583. }
  584. public function test_can_delete_all_photos_from_order(): void
  585. {
  586. Storage::fake('public');
  587. $order = Order::factory()->create();
  588. $files = File::factory()->count(3)->create();
  589. $order->photos()->attach($files->pluck('id'));
  590. // This route requires admin role
  591. $response = $this->actingAs($this->adminUser)
  592. ->delete(route('order.delete-all-photos', $order));
  593. $response->assertRedirect(route('order.show', $order));
  594. $this->assertCount(0, $order->fresh()->photos);
  595. }
  596. // ==================== Document Management ====================
  597. public function test_can_upload_document_to_order(): void
  598. {
  599. Storage::fake('public');
  600. $order = Order::factory()->create();
  601. $document = UploadedFile::fake()->create('document.pdf', 100);
  602. $response = $this->actingAs($this->managerUser)
  603. ->post(route('order.upload-document', $order), [
  604. 'document' => [$document],
  605. ]);
  606. $response->assertRedirect(route('order.show', $order));
  607. $this->assertCount(1, $order->fresh()->documents);
  608. }
  609. public function test_upload_document_preserves_unicode_and_quotes_filename(): void
  610. {
  611. Storage::fake('public');
  612. $order = Order::factory()->create();
  613. $filename = "Документ «смета» 'финал' \"v2\".pdf";
  614. $document = UploadedFile::fake()->create($filename, 100, 'application/pdf');
  615. $response = $this->actingAs($this->managerUser)
  616. ->post(route('order.upload-document', $order), [
  617. 'document' => [$document],
  618. ]);
  619. $response->assertRedirect(route('order.show', $order));
  620. $saved = $order->fresh()->documents->first();
  621. $this->assertNotNull($saved);
  622. $this->assertEquals($filename, $saved->original_name);
  623. $this->assertEquals('orders/' . $order->id . '/document/' . $filename, $saved->path);
  624. Storage::disk('public')->assertExists($saved->path);
  625. }
  626. public function test_upload_statement_preserves_unicode_and_quotes_filename(): void
  627. {
  628. Storage::fake('public');
  629. $order = Order::factory()->create();
  630. $filename = "Акт «приемки» 'этап 1' \"финал\".pdf";
  631. $statement = UploadedFile::fake()->create($filename, 100, 'application/pdf');
  632. $response = $this->actingAs($this->managerUser)
  633. ->post(route('order.upload-statement', $order), [
  634. 'statement' => [$statement],
  635. ]);
  636. $response->assertRedirect(route('order.show', $order));
  637. $saved = $order->fresh()->statements->first();
  638. $this->assertNotNull($saved);
  639. $this->assertEquals($filename, $saved->original_name);
  640. $this->assertEquals('orders/' . $order->id . '/statement/' . $filename, $saved->path);
  641. Storage::disk('public')->assertExists($saved->path);
  642. }
  643. public function test_upload_document_limits_to_5_files(): void
  644. {
  645. Storage::fake('public');
  646. $order = Order::factory()->create();
  647. $documents = [];
  648. for ($i = 0; $i < 7; $i++) {
  649. $documents[] = UploadedFile::fake()->create("document{$i}.pdf", 100);
  650. }
  651. $this->actingAs($this->managerUser)
  652. ->post(route('order.upload-document', $order), [
  653. 'document' => $documents,
  654. ]);
  655. $this->assertCount(5, $order->fresh()->documents);
  656. }
  657. public function test_can_upload_webp_photo(): void
  658. {
  659. Storage::fake('public');
  660. $order = Order::factory()->create();
  661. $photo = UploadedFile::fake()->create('photo.webp', 100, 'image/webp');
  662. $response = $this->actingAs($this->managerUser)
  663. ->post(route('order.upload-photo', $order), [
  664. 'photo' => [$photo],
  665. ]);
  666. $response->assertRedirect();
  667. $saved = $order->fresh()->photos->first();
  668. $this->assertNotNull($saved);
  669. $this->assertSame('photo.webp', $saved->original_name);
  670. }
  671. // ==================== Generation ====================
  672. public function test_generate_installation_pack_is_allowed_for_any_status_when_data_is_valid(): void
  673. {
  674. Bus::fake();
  675. $product = Product::factory()->create();
  676. $mafOrder = MafOrder::factory()->create(['product_id' => $product->id]);
  677. $order = Order::factory()->create([
  678. 'order_status_id' => Order::STATUS_NEW,
  679. ]);
  680. ProductSKU::factory()->create([
  681. 'order_id' => $order->id,
  682. 'product_id' => $product->id,
  683. 'maf_order_id' => $mafOrder->id,
  684. ]);
  685. $response = $this->actingAs($this->managerUser)
  686. ->get(route('order.generate-installation-pack', $order));
  687. $response->assertRedirect(route('order.show', $order));
  688. $response->assertSessionHas('success');
  689. Bus::assertDispatched(GenerateInstallationPack::class);
  690. }
  691. public function test_generate_installation_pack_still_requires_connected_maf(): void
  692. {
  693. Bus::fake();
  694. $product = Product::factory()->create();
  695. $order = Order::factory()->create([
  696. 'order_status_id' => Order::STATUS_IN_MOUNT,
  697. ]);
  698. ProductSKU::factory()->create([
  699. 'order_id' => $order->id,
  700. 'product_id' => $product->id,
  701. 'maf_order_id' => null,
  702. ]);
  703. $response = $this->actingAs($this->managerUser)
  704. ->get(route('order.generate-installation-pack', $order));
  705. $response->assertRedirect(route('order.show', $order));
  706. $response->assertSessionHas('danger');
  707. Bus::assertNotDispatched(GenerateInstallationPack::class);
  708. }
  709. // ==================== Export ====================
  710. public function test_can_export_orders(): void
  711. {
  712. Order::factory()->count(3)->create();
  713. // This route requires admin role
  714. $response = $this->actingAs($this->adminUser)
  715. ->post(route('order.export'));
  716. $response->assertRedirect(route('order.index'));
  717. $response->assertSessionHas('success');
  718. }
  719. public function test_can_export_single_order(): void
  720. {
  721. $order = Order::factory()->create();
  722. // This route requires admin role
  723. $response = $this->actingAs($this->adminUser)
  724. ->post(route('order.export-one', $order));
  725. $response->assertRedirect(route('order.show', $order));
  726. $response->assertSessionHas('success');
  727. }
  728. }