OrderControllerTest.php 39 KB

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