OrderControllerTest.php 38 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113
  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_order_show_uses_nav_context_for_back_url(): void
  173. {
  174. $order = Order::factory()->create();
  175. $indexResponse = $this->actingAs($this->managerUser)
  176. ->get(route('order.index', [
  177. 'filters' => ['object_address' => 'Тестовый адрес'],
  178. ]));
  179. $nav = $indexResponse->viewData('nav');
  180. $response = $this->actingAs($this->managerUser)
  181. ->get(route('order.show', [
  182. 'order' => $order,
  183. 'nav' => $nav,
  184. ]));
  185. $response->assertOk();
  186. $response->assertViewHas('nav', $nav);
  187. $response->assertViewHas('back_url', function (string $backUrl): bool {
  188. if (!str_starts_with($backUrl, route('order.index'))) {
  189. return false;
  190. }
  191. $query = parse_url($backUrl, PHP_URL_QUERY);
  192. parse_str((string) $query, $params);
  193. return ($params['filters']['object_address'] ?? null) === 'Тестовый адрес';
  194. });
  195. }
  196. public function test_order_store_redirects_to_show_with_nav_token(): void
  197. {
  198. $district = District::factory()->create();
  199. $area = Area::factory()->create();
  200. $objectType = ObjectType::factory()->create();
  201. $response = $this->actingAs($this->managerUser)
  202. ->withSession([
  203. 'navigation' => [
  204. 'order-nav-token' => [
  205. 'updated_at' => time(),
  206. 'stack' => [
  207. route('order.index'),
  208. ],
  209. ],
  210. ],
  211. ])
  212. ->post(route('order.store'), [
  213. 'nav' => 'order-nav-token',
  214. 'name' => 'Тестовый заказ',
  215. 'user_id' => $this->managerUser->id,
  216. 'district_id' => $district->id,
  217. 'area_id' => $area->id,
  218. 'object_address' => 'ул. Навигационная, д. 5',
  219. 'object_type_id' => $objectType->id,
  220. 'comment' => 'Тестовый комментарий',
  221. ]);
  222. $createdOrder = Order::query()->where('object_address', 'ул. Навигационная, д. 5')->firstOrFail();
  223. $response->assertRedirect(route('order.show', [
  224. 'order' => $createdOrder,
  225. 'nav' => 'order-nav-token',
  226. ]));
  227. }
  228. public function test_creating_order_sets_tg_group_name(): void
  229. {
  230. $district = District::factory()->create(['shortname' => 'ЦАО']);
  231. $area = Area::factory()->create(['name' => 'Тверской']);
  232. $objectType = ObjectType::factory()->create();
  233. $this->actingAs($this->managerUser)
  234. ->post(route('order.store'), [
  235. 'name' => 'Тестовый заказ',
  236. 'user_id' => $this->managerUser->id,
  237. 'district_id' => $district->id,
  238. 'area_id' => $area->id,
  239. 'object_address' => 'ул. Пушкина',
  240. 'object_type_id' => $objectType->id,
  241. ]);
  242. $order = Order::where('object_address', 'ул. Пушкина')->first();
  243. $this->assertStringContainsString('ЦАО', $order->tg_group_name);
  244. $this->assertStringContainsString('Тверской', $order->tg_group_name);
  245. }
  246. public function test_can_update_existing_order(): void
  247. {
  248. $order = Order::factory()->create([
  249. 'object_address' => 'Старый адрес',
  250. ]);
  251. $response = $this->actingAs($this->managerUser)
  252. ->post(route('order.store'), [
  253. 'id' => $order->id,
  254. 'name' => $order->name,
  255. 'user_id' => $order->user_id,
  256. 'district_id' => $order->district_id,
  257. 'area_id' => $order->area_id,
  258. 'object_address' => 'Новый адрес',
  259. 'object_type_id' => $order->object_type_id,
  260. ]);
  261. $response->assertRedirect();
  262. $this->assertDatabaseHas('orders', [
  263. 'id' => $order->id,
  264. 'object_address' => 'Новый адрес',
  265. ]);
  266. }
  267. public function test_cannot_set_in_mount_status_when_order_has_unlinked_maf(): void
  268. {
  269. $product = Product::factory()->create();
  270. $order = Order::factory()->readyToMount()->withBrigadier($this->brigadierUser)->create([
  271. 'installation_date' => '2026-04-25',
  272. ]);
  273. ProductSKU::factory()->create([
  274. 'order_id' => $order->id,
  275. 'product_id' => $product->id,
  276. 'maf_order_id' => null,
  277. ]);
  278. $response = $this->actingAs($this->adminUser)
  279. ->post(route('order.update'), [
  280. 'id' => $order->id,
  281. 'order_status_id' => Order::STATUS_IN_MOUNT,
  282. ]);
  283. $response->assertRedirect();
  284. $response->assertSessionHas('danger', function ($messages) {
  285. return in_array('МАФ не привязан к заказу', (array) $messages, true);
  286. });
  287. $this->assertDatabaseHas('orders', [
  288. 'id' => $order->id,
  289. 'order_status_id' => Order::STATUS_READY_TO_MOUNT,
  290. ]);
  291. }
  292. public function test_ajax_cannot_set_in_mount_status_when_maf_order_number_is_empty(): void
  293. {
  294. $product = Product::factory()->create();
  295. $order = Order::factory()->readyToMount()->withBrigadier($this->brigadierUser)->create([
  296. 'installation_date' => '2026-04-25',
  297. ]);
  298. $mafOrder = MafOrder::factory()->create([
  299. 'product_id' => $product->id,
  300. 'order_number' => '',
  301. ]);
  302. ProductSKU::factory()->create([
  303. 'order_id' => $order->id,
  304. 'product_id' => $product->id,
  305. 'maf_order_id' => $mafOrder->id,
  306. ]);
  307. $response = $this->actingAs($this->adminUser)
  308. ->withHeader('X-Requested-With', 'XMLHttpRequest')
  309. ->post(route('order.update'), [
  310. 'id' => $order->id,
  311. 'order_status_id' => Order::STATUS_IN_MOUNT,
  312. ]);
  313. $response->assertStatus(422);
  314. $response->assertJson([
  315. 'message' => 'МАФ не привязан к заказу',
  316. ]);
  317. $this->assertDatabaseHas('orders', [
  318. 'id' => $order->id,
  319. 'order_status_id' => Order::STATUS_READY_TO_MOUNT,
  320. ]);
  321. }
  322. public function test_can_set_in_mount_status_when_all_mafs_are_linked_to_order_numbers(): void
  323. {
  324. $product = Product::factory()->create();
  325. $order = Order::factory()->readyToMount()->withBrigadier($this->brigadierUser)->create([
  326. 'installation_date' => '2026-04-25',
  327. ]);
  328. $mafOrder = MafOrder::factory()->create([
  329. 'product_id' => $product->id,
  330. 'order_number' => 'MO-7788',
  331. ]);
  332. ProductSKU::factory()->create([
  333. 'order_id' => $order->id,
  334. 'product_id' => $product->id,
  335. 'maf_order_id' => $mafOrder->id,
  336. ]);
  337. $response = $this->actingAs($this->adminUser)
  338. ->post(route('order.update'), [
  339. 'id' => $order->id,
  340. 'order_status_id' => Order::STATUS_IN_MOUNT,
  341. ]);
  342. $response->assertRedirect();
  343. $this->assertDatabaseHas('orders', [
  344. 'id' => $order->id,
  345. 'order_status_id' => Order::STATUS_IN_MOUNT,
  346. ]);
  347. }
  348. public function test_manager_can_update_ready_date(): void
  349. {
  350. $order = Order::factory()->create([
  351. 'ready_date' => '2026-04-01',
  352. ]);
  353. $response = $this->actingAs($this->managerUser)
  354. ->post(route('order.store'), [
  355. 'id' => $order->id,
  356. 'ready_date' => '2026-04-25',
  357. ]);
  358. $response->assertRedirect();
  359. $this->assertDatabaseHas('orders', [
  360. 'id' => $order->id,
  361. 'ready_date' => '2026-04-25',
  362. ]);
  363. }
  364. // ==================== Show ====================
  365. public function test_can_view_order_details(): void
  366. {
  367. $order = Order::factory()->create([
  368. 'object_address' => 'ул. Детальная, д. 5',
  369. ]);
  370. $response = $this->actingAs($this->managerUser)
  371. ->get(route('order.show', $order));
  372. $response->assertStatus(200);
  373. $response->assertViewIs('orders.show');
  374. $response->assertSee($order->object_address);
  375. }
  376. public function test_order_details_show_area_and_district(): void
  377. {
  378. $district = District::factory()->create(['name' => 'Центральный округ']);
  379. $area = Area::factory()->create([
  380. 'district_id' => $district->id,
  381. 'name' => 'Тверской район',
  382. ]);
  383. $order = Order::factory()->create([
  384. 'district_id' => $district->id,
  385. 'area_id' => $area->id,
  386. ]);
  387. $response = $this->actingAs($this->managerUser)
  388. ->get(route('order.show', $order));
  389. $response->assertOk();
  390. $response->assertSee('Центральный округ');
  391. $response->assertSee('Тверской район');
  392. }
  393. public function test_brigadier_cannot_view_handed_over_order_details(): void
  394. {
  395. $order = Order::factory()->create([
  396. 'brigadier_id' => $this->brigadierUser->id,
  397. 'order_status_id' => Order::STATUS_HANDED_OVER,
  398. ]);
  399. $response = $this->actingAs($this->brigadierUser)
  400. ->get(route('order.show', $order));
  401. $response->assertStatus(403);
  402. }
  403. // ==================== Edit ====================
  404. public function test_can_view_edit_order_form(): void
  405. {
  406. $order = Order::factory()->create();
  407. $response = $this->actingAs($this->managerUser)
  408. ->get(route('order.edit', $order));
  409. $response->assertStatus(200);
  410. $response->assertViewIs('orders.edit');
  411. }
  412. public function test_manager_can_edit_ready_date_in_order_edit_form(): void
  413. {
  414. $order = Order::factory()->create();
  415. $response = $this->actingAs($this->managerUser)
  416. ->get(route('order.edit', $order));
  417. $response->assertOk();
  418. $this->assertMatchesRegularExpression(
  419. '/<input[^>]*name="ready_date"(?:(?!disabled).)*>/s',
  420. $response->getContent()
  421. );
  422. }
  423. // ==================== Destroy ====================
  424. public function test_can_delete_order(): void
  425. {
  426. $order = Order::factory()->create();
  427. $orderId = $order->id;
  428. $response = $this->actingAs($this->adminUser)
  429. ->delete(route('order.destroy', $order));
  430. $response->assertRedirect(route('order.index'));
  431. // Order uses SoftDeletes, so check deleted_at is set
  432. $this->assertSoftDeleted('orders', ['id' => $orderId]);
  433. }
  434. public function test_deleting_order_removes_related_product_skus(): void
  435. {
  436. $order = Order::factory()->create();
  437. $product = Product::factory()->create();
  438. $sku = ProductSKU::factory()->create([
  439. 'order_id' => $order->id,
  440. 'product_id' => $product->id,
  441. ]);
  442. $this->actingAs($this->adminUser)
  443. ->delete(route('order.destroy', $order));
  444. // ProductSKU uses SoftDeletes, so check deleted_at is set
  445. $this->assertSoftDeleted('products_sku', ['id' => $sku->id]);
  446. }
  447. // ==================== Search ====================
  448. public function test_search_returns_matching_orders(): void
  449. {
  450. $order = Order::factory()->create([
  451. 'object_address' => 'ул. Уникальная Тестовая, д. 999',
  452. ]);
  453. $otherOrder = Order::factory()->create([
  454. 'object_address' => 'ул. Другая, д. 1',
  455. ]);
  456. $response = $this->actingAs($this->managerUser)
  457. ->get(route('order.index', ['s' => 'Уникальная Тестовая']));
  458. $response->assertStatus(200);
  459. $response->assertSee($order->object_address);
  460. $response->assertDontSee($otherOrder->object_address);
  461. }
  462. public function test_order_search_route_can_search_by_manager_name(): void
  463. {
  464. $manager = User::factory()->create([
  465. 'role' => Role::MANAGER,
  466. 'name' => 'Менеджер Поиска',
  467. ]);
  468. $matchedOrder = Order::factory()->create([
  469. 'user_id' => $manager->id,
  470. 'object_address' => 'ул. Найденная, д. 7',
  471. ]);
  472. $otherOrder = Order::factory()->create([
  473. 'object_address' => 'ул. Не должна попасть, д. 8',
  474. ]);
  475. $response = $this->actingAs($this->adminUser)
  476. ->getJson(route('order.search', ['s' => 'Менеджер Поиска']));
  477. $response->assertOk();
  478. $response->assertJsonPath((string) $matchedOrder->id, $matchedOrder->move_maf_name);
  479. $response->assertJsonMissing([$otherOrder->id => $otherOrder->move_maf_name]);
  480. }
  481. public function test_order_search_route_includes_site_name_and_excludes_current_order(): void
  482. {
  483. $district = District::factory()->create(['name' => 'Северный округ']);
  484. $area = Area::factory()->create([
  485. 'district_id' => $district->id,
  486. 'name' => 'Левобережный',
  487. ]);
  488. $currentOrder = Order::factory()->create([
  489. 'district_id' => $district->id,
  490. 'area_id' => $area->id,
  491. 'name' => 'Текущая площадка',
  492. 'object_address' => 'ул. Проверочная, д. 1',
  493. ]);
  494. $targetOrder = Order::factory()->create([
  495. 'district_id' => $district->id,
  496. 'area_id' => $area->id,
  497. 'name' => 'Площадка назначения',
  498. 'object_address' => 'ул. Проверочная, д. 2',
  499. ]);
  500. $response = $this->actingAs($this->adminUser)
  501. ->getJson(route('order.search', [
  502. 's' => 'Проверочная',
  503. 'current_order_id' => $currentOrder->id,
  504. ]));
  505. $response->assertOk();
  506. $response->assertJsonPath((string) $targetOrder->id, $targetOrder->move_maf_name);
  507. $response->assertJsonMissing([(string) $currentOrder->id => $currentOrder->move_maf_name]);
  508. }
  509. // ==================== MAF Operations ====================
  510. public function test_get_maf_to_order_assigns_available_maf(): void
  511. {
  512. $product = Product::factory()->create();
  513. $order = Order::factory()->create();
  514. $productSku = ProductSKU::factory()->create([
  515. 'order_id' => $order->id,
  516. 'product_id' => $product->id,
  517. 'maf_order_id' => null,
  518. 'status' => 'требуется',
  519. ]);
  520. $mafOrder = MafOrder::factory()->create([
  521. 'product_id' => $product->id,
  522. 'in_stock' => 5,
  523. ]);
  524. // This route requires admin role
  525. $response = $this->actingAs($this->adminUser)
  526. ->get(route('order.get-maf', $order));
  527. $response->assertRedirect(route('order.show', $order));
  528. $response->assertSessionHas('success', function ($messages) {
  529. return str_contains(implode(' ', (array) $messages), 'Привязано МАФ: 1.');
  530. });
  531. $productSku->refresh();
  532. $this->assertEquals($mafOrder->id, $productSku->maf_order_id);
  533. $this->assertEquals('отгружен', $productSku->status);
  534. $mafOrder->refresh();
  535. $this->assertEquals(4, $mafOrder->in_stock);
  536. }
  537. public function test_get_maf_to_order_shows_error_when_not_enough_stock(): void
  538. {
  539. $product = Product::factory()->create();
  540. $order = Order::factory()->create();
  541. $productSku = ProductSKU::factory()->create([
  542. 'order_id' => $order->id,
  543. 'product_id' => $product->id,
  544. 'maf_order_id' => null,
  545. 'status' => 'требуется',
  546. ]);
  547. $response = $this->actingAs($this->adminUser)
  548. ->get(route('order.get-maf', $order));
  549. $response->assertRedirect(route('order.show', $order));
  550. $response->assertSessionHas('danger', function ($messages) {
  551. return str_contains(implode(' ', (array) $messages), 'Не удалось привязать 1 шт. МАФ');
  552. });
  553. $productSku->refresh();
  554. $this->assertNull($productSku->maf_order_id);
  555. $this->assertEquals('требуется', $productSku->status);
  556. }
  557. public function test_revert_maf_returns_maf_to_stock(): void
  558. {
  559. $product = Product::factory()->create();
  560. $order = Order::factory()->create();
  561. $mafOrder = MafOrder::factory()->create([
  562. 'product_id' => $product->id,
  563. 'in_stock' => 3,
  564. ]);
  565. $productSku = ProductSKU::factory()->create([
  566. 'order_id' => $order->id,
  567. 'product_id' => $product->id,
  568. 'maf_order_id' => $mafOrder->id,
  569. 'status' => 'отгружен',
  570. ]);
  571. // This route requires admin role
  572. $response = $this->actingAs($this->adminUser)
  573. ->get(route('order.revert-maf', $order));
  574. $response->assertRedirect(route('order.show', $order));
  575. $response->assertSessionHas('success', function ($messages) {
  576. return str_contains(implode(' ', (array) $messages), 'Отвязано МАФ: 1.');
  577. });
  578. $productSku->refresh();
  579. $this->assertNull($productSku->maf_order_id);
  580. $this->assertEquals('требуется', $productSku->status);
  581. $mafOrder->refresh();
  582. $this->assertEquals(4, $mafOrder->in_stock);
  583. }
  584. public function test_revert_maf_shows_error_when_nothing_is_attached(): void
  585. {
  586. $order = Order::factory()->create();
  587. ProductSKU::factory()->create([
  588. 'order_id' => $order->id,
  589. 'maf_order_id' => null,
  590. 'status' => 'требуется',
  591. ]);
  592. $response = $this->actingAs($this->adminUser)
  593. ->get(route('order.revert-maf', $order));
  594. $response->assertRedirect(route('order.show', $order));
  595. $response->assertSessionHas('danger', function ($messages) {
  596. return str_contains(implode(' ', (array) $messages), 'Нет МАФ для отвязки');
  597. });
  598. }
  599. public function test_store_order_cannot_remove_attached_maf_from_order_list(): void
  600. {
  601. $attachedProduct = Product::factory()->create();
  602. $otherProduct = Product::factory()->create();
  603. $order = Order::factory()->create([
  604. 'order_status_id' => Order::STATUS_NEW,
  605. ]);
  606. $mafOrder = MafOrder::factory()->create([
  607. 'product_id' => $attachedProduct->id,
  608. 'in_stock' => 1,
  609. ]);
  610. $attachedSku = ProductSKU::factory()->create([
  611. 'order_id' => $order->id,
  612. 'product_id' => $attachedProduct->id,
  613. 'maf_order_id' => $mafOrder->id,
  614. 'status' => 'отгружен',
  615. ]);
  616. ProductSKU::factory()->create([
  617. 'order_id' => $order->id,
  618. 'product_id' => $otherProduct->id,
  619. 'maf_order_id' => null,
  620. 'status' => 'требуется',
  621. ]);
  622. $response = $this->actingAs($this->adminUser)
  623. ->post(route('order.store'), [
  624. 'id' => $order->id,
  625. 'products' => [$otherProduct->id],
  626. 'quantity' => [1],
  627. ]);
  628. $response->assertRedirect();
  629. $response->assertSessionHas('danger', function ($messages) {
  630. return str_contains(implode(' ', (array) $messages), 'Нельзя удалить привязанные МАФ');
  631. });
  632. $this->assertDatabaseHas('products_sku', [
  633. 'id' => $attachedSku->id,
  634. 'maf_order_id' => $mafOrder->id,
  635. 'deleted_at' => null,
  636. ]);
  637. }
  638. public function test_store_order_cannot_add_new_positions_when_has_attached_maf(): void
  639. {
  640. $attachedProduct = Product::factory()->create();
  641. $existingProduct = Product::factory()->create();
  642. $newProduct = Product::factory()->create();
  643. $order = Order::factory()->create([
  644. 'order_status_id' => Order::STATUS_NEW,
  645. ]);
  646. $mafOrder = MafOrder::factory()->create([
  647. 'product_id' => $attachedProduct->id,
  648. 'in_stock' => 1,
  649. ]);
  650. ProductSKU::factory()->create([
  651. 'order_id' => $order->id,
  652. 'product_id' => $attachedProduct->id,
  653. 'maf_order_id' => $mafOrder->id,
  654. 'status' => 'отгружен',
  655. ]);
  656. ProductSKU::factory()->create([
  657. 'order_id' => $order->id,
  658. 'product_id' => $existingProduct->id,
  659. 'maf_order_id' => null,
  660. 'status' => 'требуется',
  661. ]);
  662. $response = $this->actingAs($this->adminUser)
  663. ->post(route('order.store'), [
  664. 'id' => $order->id,
  665. 'products' => [$attachedProduct->id, $existingProduct->id, $newProduct->id],
  666. 'quantity' => [1, 1, 1],
  667. ]);
  668. $response->assertRedirect();
  669. $response->assertSessionHas('danger', function ($messages) {
  670. return str_contains(implode(' ', (array) $messages), 'Нельзя добавлять новые позиции МАФ');
  671. });
  672. $this->assertDatabaseMissing('products_sku', [
  673. 'order_id' => $order->id,
  674. 'product_id' => $newProduct->id,
  675. 'deleted_at' => null,
  676. ]);
  677. }
  678. public function test_move_maf_transfers_sku_to_another_order(): void
  679. {
  680. $product = Product::factory()->create();
  681. $order1 = Order::factory()->create();
  682. $order2 = Order::factory()->create();
  683. $productSku = ProductSKU::factory()->create([
  684. 'order_id' => $order1->id,
  685. 'product_id' => $product->id,
  686. ]);
  687. // This route requires admin role
  688. $response = $this->actingAs($this->adminUser)
  689. ->post(route('order.move-maf'), [
  690. 'new_order_id' => $order2->id,
  691. 'ids' => json_encode([$productSku->id]),
  692. ]);
  693. $response->assertStatus(200);
  694. $response->assertJson(['success' => true]);
  695. $productSku->refresh();
  696. $this->assertEquals($order2->id, $productSku->order_id);
  697. }
  698. // ==================== Photo Management ====================
  699. public function test_can_upload_photo_to_order(): void
  700. {
  701. Storage::fake('public');
  702. $order = Order::factory()->create();
  703. // Use create() instead of image() to avoid GD extension requirement
  704. $photo = UploadedFile::fake()->create('photo.jpg', 100, 'image/jpeg');
  705. $response = $this->actingAs($this->managerUser)
  706. ->post(route('order.upload-photo', $order), [
  707. 'photo' => [$photo],
  708. ]);
  709. $response->assertRedirect(route('order.show', $order));
  710. $this->assertCount(1, $order->fresh()->photos);
  711. }
  712. public function test_can_delete_photo_from_order(): void
  713. {
  714. Storage::fake('public');
  715. $order = Order::factory()->create();
  716. $file = File::factory()->create();
  717. $order->photos()->attach($file);
  718. // This route requires admin role
  719. $response = $this->actingAs($this->adminUser)
  720. ->delete(route('order.delete-photo', [$order, $file]));
  721. $response->assertRedirect(route('order.show', $order));
  722. $this->assertCount(0, $order->fresh()->photos);
  723. }
  724. public function test_can_delete_all_photos_from_order(): void
  725. {
  726. Storage::fake('public');
  727. $order = Order::factory()->create();
  728. $files = File::factory()->count(3)->create();
  729. $order->photos()->attach($files->pluck('id'));
  730. // This route requires admin role
  731. $response = $this->actingAs($this->adminUser)
  732. ->delete(route('order.delete-all-photos', $order));
  733. $response->assertRedirect(route('order.show', $order));
  734. $this->assertCount(0, $order->fresh()->photos);
  735. }
  736. // ==================== Document Management ====================
  737. public function test_can_upload_document_to_order(): void
  738. {
  739. Storage::fake('public');
  740. $order = Order::factory()->create();
  741. $document = UploadedFile::fake()->create('document.pdf', 100);
  742. $response = $this->actingAs($this->managerUser)
  743. ->post(route('order.upload-document', $order), [
  744. 'document' => [$document],
  745. ]);
  746. $response->assertRedirect(route('order.show', $order));
  747. $this->assertCount(1, $order->fresh()->documents);
  748. }
  749. public function test_upload_document_preserves_unicode_and_quotes_filename(): void
  750. {
  751. Storage::fake('public');
  752. $order = Order::factory()->create();
  753. $filename = "Документ «смета» 'финал' \"v2\".pdf";
  754. $document = UploadedFile::fake()->create($filename, 100, 'application/pdf');
  755. $response = $this->actingAs($this->managerUser)
  756. ->post(route('order.upload-document', $order), [
  757. 'document' => [$document],
  758. ]);
  759. $response->assertRedirect(route('order.show', $order));
  760. $saved = $order->fresh()->documents->first();
  761. $this->assertNotNull($saved);
  762. $this->assertEquals($filename, $saved->original_name);
  763. $this->assertEquals('orders/' . $order->id . '/document/' . $filename, $saved->path);
  764. Storage::disk('public')->assertExists($saved->path);
  765. }
  766. public function test_upload_statement_preserves_unicode_and_quotes_filename(): void
  767. {
  768. Storage::fake('public');
  769. $order = Order::factory()->create();
  770. $filename = "Акт «приемки» 'этап 1' \"финал\".pdf";
  771. $statement = UploadedFile::fake()->create($filename, 100, 'application/pdf');
  772. $response = $this->actingAs($this->managerUser)
  773. ->post(route('order.upload-statement', $order), [
  774. 'statement' => [$statement],
  775. ]);
  776. $response->assertRedirect(route('order.show', $order));
  777. $saved = $order->fresh()->statements->first();
  778. $this->assertNotNull($saved);
  779. $this->assertEquals($filename, $saved->original_name);
  780. $this->assertEquals('orders/' . $order->id . '/statement/' . $filename, $saved->path);
  781. Storage::disk('public')->assertExists($saved->path);
  782. }
  783. public function test_upload_document_limits_to_5_files(): void
  784. {
  785. Storage::fake('public');
  786. $order = Order::factory()->create();
  787. $documents = [];
  788. for ($i = 0; $i < 7; $i++) {
  789. $documents[] = UploadedFile::fake()->create("document{$i}.pdf", 100);
  790. }
  791. $this->actingAs($this->managerUser)
  792. ->post(route('order.upload-document', $order), [
  793. 'document' => $documents,
  794. ]);
  795. $this->assertCount(5, $order->fresh()->documents);
  796. }
  797. public function test_can_upload_webp_photo(): void
  798. {
  799. Storage::fake('public');
  800. $order = Order::factory()->create();
  801. $photo = UploadedFile::fake()->create('photo.webp', 100, 'image/webp');
  802. $response = $this->actingAs($this->managerUser)
  803. ->post(route('order.upload-photo', $order), [
  804. 'photo' => [$photo],
  805. ]);
  806. $response->assertRedirect();
  807. $saved = $order->fresh()->photos->first();
  808. $this->assertNotNull($saved);
  809. $this->assertSame('photo.webp', $saved->original_name);
  810. }
  811. // ==================== Generation ====================
  812. public function test_generate_installation_pack_is_allowed_for_any_status_when_data_is_valid(): void
  813. {
  814. Bus::fake();
  815. $product = Product::factory()->create();
  816. $mafOrder = MafOrder::factory()->create(['product_id' => $product->id]);
  817. $order = Order::factory()->create([
  818. 'order_status_id' => Order::STATUS_NEW,
  819. ]);
  820. ProductSKU::factory()->create([
  821. 'order_id' => $order->id,
  822. 'product_id' => $product->id,
  823. 'maf_order_id' => $mafOrder->id,
  824. ]);
  825. $response = $this->actingAs($this->managerUser)
  826. ->get(route('order.generate-installation-pack', $order));
  827. $response->assertRedirect(route('order.show', $order));
  828. $response->assertSessionHas('success');
  829. Bus::assertDispatched(GenerateInstallationPack::class);
  830. }
  831. public function test_generate_installation_pack_still_requires_connected_maf(): void
  832. {
  833. Bus::fake();
  834. $product = Product::factory()->create();
  835. $order = Order::factory()->create([
  836. 'order_status_id' => Order::STATUS_IN_MOUNT,
  837. ]);
  838. ProductSKU::factory()->create([
  839. 'order_id' => $order->id,
  840. 'product_id' => $product->id,
  841. 'maf_order_id' => null,
  842. ]);
  843. $response = $this->actingAs($this->managerUser)
  844. ->get(route('order.generate-installation-pack', $order));
  845. $response->assertRedirect(route('order.show', $order));
  846. $response->assertSessionHas('danger');
  847. Bus::assertNotDispatched(GenerateInstallationPack::class);
  848. }
  849. public function test_admin_can_create_ttn_with_departure_date_and_increment_counter(): void
  850. {
  851. Bus::fake();
  852. Setting::set(Setting::KEY_TTN_NEXT_NUMBER, 10);
  853. $product = Product::factory()->create();
  854. $order = Order::factory()->create([
  855. 'installation_date' => '2026-04-15',
  856. ]);
  857. $sku = ProductSKU::factory()->create([
  858. 'order_id' => $order->id,
  859. 'product_id' => $product->id,
  860. ]);
  861. $response = $this->actingAs($this->adminUser)
  862. ->post(route('order.create-ttn'), [
  863. 'order_number' => 'З-100',
  864. 'order_date' => '2026-04-07',
  865. 'departure_date' => '2026-04-08',
  866. 'order_sum' => '150000',
  867. 'skus' => [$sku->id],
  868. ]);
  869. $response->assertRedirect();
  870. $response->assertSessionHas('success');
  871. $this->assertDatabaseHas('ttns', [
  872. 'ttn_number' => 10,
  873. 'order_number' => 'З-100',
  874. 'order_date' => '2026-04-07',
  875. 'departure_date' => '2026-04-08',
  876. 'order_sum' => '150000',
  877. ]);
  878. $this->assertSame(11, Setting::getInt(Setting::KEY_TTN_NEXT_NUMBER));
  879. $ttn = Ttn::query()->where('ttn_number', 10)->firstOrFail();
  880. $this->assertSame([(string) $sku->id], array_map('strval', json_decode($ttn->skus, true)));
  881. Bus::assertDispatched(GenerateTtnPack::class);
  882. }
  883. // ==================== Export ====================
  884. public function test_can_export_orders(): void
  885. {
  886. Order::factory()->count(3)->create();
  887. // This route requires admin role
  888. $response = $this->actingAs($this->adminUser)
  889. ->post(route('order.export'));
  890. $response->assertRedirect(route('order.index'));
  891. $response->assertSessionHas('success');
  892. }
  893. public function test_can_export_single_order(): void
  894. {
  895. $order = Order::factory()->create();
  896. // This route requires admin role
  897. $response = $this->actingAs($this->adminUser)
  898. ->post(route('order.export-one', $order));
  899. $response->assertRedirect(route('order.show', $order));
  900. $response->assertSessionHas('success');
  901. }
  902. }