OrderControllerTest.php 32 KB

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