OrderControllerTest.php 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252
  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_order_details_render_inline_maf_fields_and_passport_upload_by_permissions(): void
  414. {
  415. $order = Order::factory()->create();
  416. $product = Product::factory()->create(['article' => 'MAF-INLINE-001']);
  417. $sku = ProductSKU::factory()->create([
  418. 'order_id' => $order->id,
  419. 'product_id' => $product->id,
  420. 'passport_id' => null,
  421. 'rfid' => 'RFID-ORDER-OLD',
  422. 'factory_number' => 'FN-ORDER-OLD',
  423. 'manufacture_date' => '2026-05-19',
  424. 'statement_number' => 'STAT-ORDER-OLD',
  425. 'statement_date' => '2026-05-18',
  426. ]);
  427. $response = $this->actingAs($this->adminUser)
  428. ->get(route('order.show', $order));
  429. $response->assertOk();
  430. $response->assertSee('inline-product-sku-field', false);
  431. $response->assertSee('data-field="rfid"', false);
  432. $response->assertSee('data-field="factory_number"', false);
  433. $response->assertSee('data-field="manufacture_date"', false);
  434. $response->assertSee('data-field="statement_number"', false);
  435. $response->assertSee('data-field="statement_date"', false);
  436. $response->assertSee('Добавить Ведомость');
  437. $response->assertSee(route('order.add-statement-to-mafs', $order), false);
  438. $response->assertSee(route('product_sku.inline-update', $sku->id), false);
  439. $response->assertSee('upload-maf-passport', false);
  440. $response->assertSee(route('product-sku.upload-passport', ['product_sku' => $sku]), false);
  441. }
  442. public function test_order_details_hide_inline_maf_controls_without_permissions(): void
  443. {
  444. $order = Order::factory()->create();
  445. $product = Product::factory()->create(['article' => 'MAF-NO-INLINE-001']);
  446. $sku = ProductSKU::factory()->create([
  447. 'order_id' => $order->id,
  448. 'product_id' => $product->id,
  449. 'passport_id' => null,
  450. 'rfid' => 'RFID-READONLY',
  451. ]);
  452. $response = $this->actingAs($this->managerUser)
  453. ->get(route('order.show', $order));
  454. $response->assertOk();
  455. $response->assertDontSee(route('product_sku.inline-update', $sku->id), false);
  456. $response->assertDontSee(route('product-sku.upload-passport', ['product_sku' => $sku]), false);
  457. }
  458. public function test_can_add_statement_to_selected_mafs_on_order(): void
  459. {
  460. $order = Order::factory()->create();
  461. $otherOrder = Order::factory()->create();
  462. $product = Product::factory()->create();
  463. $selectedSku = ProductSKU::factory()->create([
  464. 'order_id' => $order->id,
  465. 'product_id' => $product->id,
  466. 'statement_number' => null,
  467. 'statement_date' => null,
  468. ]);
  469. $notSelectedSku = ProductSKU::factory()->create([
  470. 'order_id' => $order->id,
  471. 'product_id' => $product->id,
  472. 'statement_number' => null,
  473. 'statement_date' => null,
  474. ]);
  475. $otherOrderSku = ProductSKU::factory()->create([
  476. 'order_id' => $otherOrder->id,
  477. 'product_id' => $product->id,
  478. 'statement_number' => null,
  479. 'statement_date' => null,
  480. ]);
  481. $response = $this->actingAs($this->adminUser)
  482. ->post(route('order.add-statement-to-mafs', $order), [
  483. 'statement_number' => 'STAT-2026-05',
  484. 'statement_date' => '2026-05-19',
  485. 'skus' => [$selectedSku->id, $otherOrderSku->id],
  486. ]);
  487. $response->assertRedirect();
  488. $response->assertSessionHas('success', 'Ведомость добавлена к МАФ: 1.');
  489. $this->assertDatabaseHas('products_sku', [
  490. 'id' => $selectedSku->id,
  491. 'statement_number' => 'STAT-2026-05',
  492. 'statement_date' => '2026-05-19',
  493. ]);
  494. $this->assertDatabaseHas('products_sku', [
  495. 'id' => $notSelectedSku->id,
  496. 'statement_number' => null,
  497. 'statement_date' => null,
  498. ]);
  499. $this->assertDatabaseHas('products_sku', [
  500. 'id' => $otherOrderSku->id,
  501. 'statement_number' => null,
  502. 'statement_date' => null,
  503. ]);
  504. }
  505. public function test_add_statement_to_mafs_requires_field_permissions(): void
  506. {
  507. $order = Order::factory()->create();
  508. $sku = ProductSKU::factory()->create(['order_id' => $order->id]);
  509. $response = $this->actingAs($this->managerUser)
  510. ->post(route('order.add-statement-to-mafs', $order), [
  511. 'statement_number' => 'STAT-DENIED',
  512. 'statement_date' => '2026-05-19',
  513. 'skus' => [$sku->id],
  514. ]);
  515. $response->assertForbidden();
  516. }
  517. public function test_brigadier_cannot_view_handed_over_order_details(): void
  518. {
  519. $order = Order::factory()->create([
  520. 'brigadier_id' => $this->brigadierUser->id,
  521. 'order_status_id' => Order::STATUS_HANDED_OVER,
  522. ]);
  523. $response = $this->actingAs($this->brigadierUser)
  524. ->get(route('order.show', $order));
  525. $response->assertStatus(403);
  526. }
  527. // ==================== Edit ====================
  528. public function test_can_view_edit_order_form(): void
  529. {
  530. $order = Order::factory()->create();
  531. $response = $this->actingAs($this->managerUser)
  532. ->get(route('order.edit', $order));
  533. $response->assertStatus(200);
  534. $response->assertViewIs('orders.edit');
  535. }
  536. public function test_manager_can_edit_ready_date_in_order_edit_form(): void
  537. {
  538. $order = Order::factory()->create();
  539. $response = $this->actingAs($this->managerUser)
  540. ->get(route('order.edit', $order));
  541. $response->assertOk();
  542. $this->assertMatchesRegularExpression(
  543. '/<input[^>]*name="ready_date"(?:(?!disabled).)*>/s',
  544. $response->getContent()
  545. );
  546. }
  547. // ==================== Destroy ====================
  548. public function test_can_delete_order(): void
  549. {
  550. $order = Order::factory()->create();
  551. $orderId = $order->id;
  552. $response = $this->actingAs($this->adminUser)
  553. ->delete(route('order.destroy', $order));
  554. $response->assertRedirect(route('order.index'));
  555. // Order uses SoftDeletes, so check deleted_at is set
  556. $this->assertSoftDeleted('orders', ['id' => $orderId]);
  557. }
  558. public function test_deleting_order_removes_related_product_skus(): void
  559. {
  560. $order = Order::factory()->create();
  561. $product = Product::factory()->create();
  562. $sku = ProductSKU::factory()->create([
  563. 'order_id' => $order->id,
  564. 'product_id' => $product->id,
  565. ]);
  566. $this->actingAs($this->adminUser)
  567. ->delete(route('order.destroy', $order));
  568. // ProductSKU uses SoftDeletes, so check deleted_at is set
  569. $this->assertSoftDeleted('products_sku', ['id' => $sku->id]);
  570. }
  571. // ==================== Search ====================
  572. public function test_search_returns_matching_orders(): void
  573. {
  574. $order = Order::factory()->create([
  575. 'object_address' => 'ул. Уникальная Тестовая, д. 999',
  576. ]);
  577. $otherOrder = Order::factory()->create([
  578. 'object_address' => 'ул. Другая, д. 1',
  579. ]);
  580. $response = $this->actingAs($this->managerUser)
  581. ->get(route('order.index', ['s' => 'Уникальная Тестовая']));
  582. $response->assertStatus(200);
  583. $response->assertSee($order->object_address);
  584. $response->assertDontSee($otherOrder->object_address);
  585. }
  586. public function test_order_search_route_can_search_by_manager_name(): void
  587. {
  588. $manager = User::factory()->create([
  589. 'role' => Role::MANAGER,
  590. 'name' => 'Менеджер Поиска',
  591. ]);
  592. $matchedOrder = Order::factory()->create([
  593. 'user_id' => $manager->id,
  594. 'object_address' => 'ул. Найденная, д. 7',
  595. ]);
  596. $otherOrder = Order::factory()->create([
  597. 'object_address' => 'ул. Не должна попасть, д. 8',
  598. ]);
  599. $response = $this->actingAs($this->adminUser)
  600. ->getJson(route('order.search', ['s' => 'Менеджер Поиска']));
  601. $response->assertOk();
  602. $response->assertJsonPath((string) $matchedOrder->id, $matchedOrder->move_maf_name);
  603. $response->assertJsonMissing([$otherOrder->id => $otherOrder->move_maf_name]);
  604. }
  605. public function test_order_search_route_includes_site_name_and_excludes_current_order(): void
  606. {
  607. $district = District::factory()->create(['name' => 'Северный округ']);
  608. $area = Area::factory()->create([
  609. 'district_id' => $district->id,
  610. 'name' => 'Левобережный',
  611. ]);
  612. $currentOrder = Order::factory()->create([
  613. 'district_id' => $district->id,
  614. 'area_id' => $area->id,
  615. 'name' => 'Текущая площадка',
  616. 'object_address' => 'ул. Проверочная, д. 1',
  617. ]);
  618. $targetOrder = Order::factory()->create([
  619. 'district_id' => $district->id,
  620. 'area_id' => $area->id,
  621. 'name' => 'Площадка назначения',
  622. 'object_address' => 'ул. Проверочная, д. 2',
  623. ]);
  624. $response = $this->actingAs($this->adminUser)
  625. ->getJson(route('order.search', [
  626. 's' => 'Проверочная',
  627. 'current_order_id' => $currentOrder->id,
  628. ]));
  629. $response->assertOk();
  630. $response->assertJsonPath((string) $targetOrder->id, $targetOrder->move_maf_name);
  631. $response->assertJsonMissing([(string) $currentOrder->id => $currentOrder->move_maf_name]);
  632. }
  633. // ==================== MAF Operations ====================
  634. public function test_get_maf_to_order_assigns_available_maf(): void
  635. {
  636. $product = Product::factory()->create();
  637. $order = Order::factory()->create();
  638. $productSku = ProductSKU::factory()->create([
  639. 'order_id' => $order->id,
  640. 'product_id' => $product->id,
  641. 'maf_order_id' => null,
  642. 'status' => 'требуется',
  643. ]);
  644. $mafOrder = MafOrder::factory()->create([
  645. 'product_id' => $product->id,
  646. 'in_stock' => 5,
  647. ]);
  648. // This route requires admin role
  649. $response = $this->actingAs($this->adminUser)
  650. ->get(route('order.get-maf', $order));
  651. $response->assertRedirect(route('order.show', $order));
  652. $response->assertSessionHas('success', function ($messages) {
  653. return str_contains(implode(' ', (array) $messages), 'Привязано МАФ: 1.');
  654. });
  655. $productSku->refresh();
  656. $this->assertEquals($mafOrder->id, $productSku->maf_order_id);
  657. $this->assertEquals('отгружен', $productSku->status);
  658. $mafOrder->refresh();
  659. $this->assertEquals(4, $mafOrder->in_stock);
  660. }
  661. public function test_get_maf_to_order_shows_error_when_not_enough_stock(): void
  662. {
  663. $product = Product::factory()->create();
  664. $order = Order::factory()->create();
  665. $productSku = ProductSKU::factory()->create([
  666. 'order_id' => $order->id,
  667. 'product_id' => $product->id,
  668. 'maf_order_id' => null,
  669. 'status' => 'требуется',
  670. ]);
  671. $response = $this->actingAs($this->adminUser)
  672. ->get(route('order.get-maf', $order));
  673. $response->assertRedirect(route('order.show', $order));
  674. $response->assertSessionHas('danger', function ($messages) {
  675. return str_contains(implode(' ', (array) $messages), 'Не удалось привязать 1 шт. МАФ');
  676. });
  677. $productSku->refresh();
  678. $this->assertNull($productSku->maf_order_id);
  679. $this->assertEquals('требуется', $productSku->status);
  680. }
  681. public function test_revert_maf_returns_maf_to_stock(): void
  682. {
  683. $product = Product::factory()->create();
  684. $order = Order::factory()->create();
  685. $mafOrder = MafOrder::factory()->create([
  686. 'product_id' => $product->id,
  687. 'in_stock' => 3,
  688. ]);
  689. $productSku = ProductSKU::factory()->create([
  690. 'order_id' => $order->id,
  691. 'product_id' => $product->id,
  692. 'maf_order_id' => $mafOrder->id,
  693. 'status' => 'отгружен',
  694. ]);
  695. // This route requires admin role
  696. $response = $this->actingAs($this->adminUser)
  697. ->get(route('order.revert-maf', $order));
  698. $response->assertRedirect(route('order.show', $order));
  699. $response->assertSessionHas('success', function ($messages) {
  700. return str_contains(implode(' ', (array) $messages), 'Отвязано МАФ: 1.');
  701. });
  702. $productSku->refresh();
  703. $this->assertNull($productSku->maf_order_id);
  704. $this->assertEquals('требуется', $productSku->status);
  705. $mafOrder->refresh();
  706. $this->assertEquals(4, $mafOrder->in_stock);
  707. }
  708. public function test_revert_maf_shows_error_when_nothing_is_attached(): void
  709. {
  710. $order = Order::factory()->create();
  711. ProductSKU::factory()->create([
  712. 'order_id' => $order->id,
  713. 'maf_order_id' => null,
  714. 'status' => 'требуется',
  715. ]);
  716. $response = $this->actingAs($this->adminUser)
  717. ->get(route('order.revert-maf', $order));
  718. $response->assertRedirect(route('order.show', $order));
  719. $response->assertSessionHas('danger', function ($messages) {
  720. return str_contains(implode(' ', (array) $messages), 'Нет МАФ для отвязки');
  721. });
  722. }
  723. public function test_store_order_cannot_remove_attached_maf_from_order_list(): void
  724. {
  725. $attachedProduct = Product::factory()->create();
  726. $otherProduct = Product::factory()->create();
  727. $order = Order::factory()->create([
  728. 'order_status_id' => Order::STATUS_NEW,
  729. ]);
  730. $mafOrder = MafOrder::factory()->create([
  731. 'product_id' => $attachedProduct->id,
  732. 'in_stock' => 1,
  733. ]);
  734. $attachedSku = ProductSKU::factory()->create([
  735. 'order_id' => $order->id,
  736. 'product_id' => $attachedProduct->id,
  737. 'maf_order_id' => $mafOrder->id,
  738. 'status' => 'отгружен',
  739. ]);
  740. ProductSKU::factory()->create([
  741. 'order_id' => $order->id,
  742. 'product_id' => $otherProduct->id,
  743. 'maf_order_id' => null,
  744. 'status' => 'требуется',
  745. ]);
  746. $response = $this->actingAs($this->adminUser)
  747. ->post(route('order.store'), [
  748. 'id' => $order->id,
  749. 'products' => [$otherProduct->id],
  750. 'quantity' => [1],
  751. ]);
  752. $response->assertRedirect();
  753. $response->assertSessionHas('danger', function ($messages) {
  754. return str_contains(implode(' ', (array) $messages), 'Нельзя удалить привязанные МАФ');
  755. });
  756. $this->assertDatabaseHas('products_sku', [
  757. 'id' => $attachedSku->id,
  758. 'maf_order_id' => $mafOrder->id,
  759. 'deleted_at' => null,
  760. ]);
  761. }
  762. public function test_store_order_cannot_add_new_positions_when_has_attached_maf(): void
  763. {
  764. $attachedProduct = Product::factory()->create();
  765. $existingProduct = Product::factory()->create();
  766. $newProduct = Product::factory()->create();
  767. $order = Order::factory()->create([
  768. 'order_status_id' => Order::STATUS_NEW,
  769. ]);
  770. $mafOrder = MafOrder::factory()->create([
  771. 'product_id' => $attachedProduct->id,
  772. 'in_stock' => 1,
  773. ]);
  774. ProductSKU::factory()->create([
  775. 'order_id' => $order->id,
  776. 'product_id' => $attachedProduct->id,
  777. 'maf_order_id' => $mafOrder->id,
  778. 'status' => 'отгружен',
  779. ]);
  780. ProductSKU::factory()->create([
  781. 'order_id' => $order->id,
  782. 'product_id' => $existingProduct->id,
  783. 'maf_order_id' => null,
  784. 'status' => 'требуется',
  785. ]);
  786. $response = $this->actingAs($this->adminUser)
  787. ->post(route('order.store'), [
  788. 'id' => $order->id,
  789. 'products' => [$attachedProduct->id, $existingProduct->id, $newProduct->id],
  790. 'quantity' => [1, 1, 1],
  791. ]);
  792. $response->assertRedirect();
  793. $response->assertSessionHas('danger', function ($messages) {
  794. return str_contains(implode(' ', (array) $messages), 'Нельзя добавлять новые позиции МАФ');
  795. });
  796. $this->assertDatabaseMissing('products_sku', [
  797. 'order_id' => $order->id,
  798. 'product_id' => $newProduct->id,
  799. 'deleted_at' => null,
  800. ]);
  801. }
  802. public function test_move_maf_transfers_sku_to_another_order(): void
  803. {
  804. $product = Product::factory()->create();
  805. $order1 = Order::factory()->create();
  806. $order2 = Order::factory()->create();
  807. $productSku = ProductSKU::factory()->create([
  808. 'order_id' => $order1->id,
  809. 'product_id' => $product->id,
  810. ]);
  811. // This route requires admin role
  812. $response = $this->actingAs($this->adminUser)
  813. ->post(route('order.move-maf'), [
  814. 'new_order_id' => $order2->id,
  815. 'ids' => json_encode([$productSku->id]),
  816. ]);
  817. $response->assertStatus(200);
  818. $response->assertJson(['success' => true]);
  819. $productSku->refresh();
  820. $this->assertEquals($order2->id, $productSku->order_id);
  821. }
  822. // ==================== Photo Management ====================
  823. public function test_can_upload_photo_to_order(): void
  824. {
  825. Storage::fake('public');
  826. $order = Order::factory()->create();
  827. // Use create() instead of image() to avoid GD extension requirement
  828. $photo = UploadedFile::fake()->create('photo.jpg', 100, 'image/jpeg');
  829. $response = $this->actingAs($this->managerUser)
  830. ->post(route('order.upload-photo', $order), [
  831. 'photo' => [$photo],
  832. ]);
  833. $response->assertRedirect(route('order.show', $order));
  834. $this->assertCount(1, $order->fresh()->photos);
  835. }
  836. public function test_can_delete_photo_from_order(): void
  837. {
  838. Storage::fake('public');
  839. $order = Order::factory()->create();
  840. $file = File::factory()->create();
  841. $order->photos()->attach($file);
  842. // This route requires admin role
  843. $response = $this->actingAs($this->adminUser)
  844. ->delete(route('order.delete-photo', [$order, $file]));
  845. $response->assertRedirect(route('order.show', $order));
  846. $this->assertCount(0, $order->fresh()->photos);
  847. }
  848. public function test_can_delete_all_photos_from_order(): void
  849. {
  850. Storage::fake('public');
  851. $order = Order::factory()->create();
  852. $files = File::factory()->count(3)->create();
  853. $order->photos()->attach($files->pluck('id'));
  854. // This route requires admin role
  855. $response = $this->actingAs($this->adminUser)
  856. ->delete(route('order.delete-all-photos', $order));
  857. $response->assertRedirect(route('order.show', $order));
  858. $this->assertCount(0, $order->fresh()->photos);
  859. }
  860. // ==================== Document Management ====================
  861. public function test_can_upload_document_to_order(): void
  862. {
  863. Storage::fake('public');
  864. $order = Order::factory()->create();
  865. $document = UploadedFile::fake()->create('document.pdf', 100);
  866. $response = $this->actingAs($this->managerUser)
  867. ->post(route('order.upload-document', $order), [
  868. 'document' => [$document],
  869. ]);
  870. $response->assertRedirect(route('order.show', $order));
  871. $this->assertCount(1, $order->fresh()->documents);
  872. }
  873. public function test_upload_document_preserves_unicode_and_quotes_filename(): void
  874. {
  875. Storage::fake('public');
  876. $order = Order::factory()->create();
  877. $filename = "Документ «смета» 'финал' \"v2\".pdf";
  878. $document = UploadedFile::fake()->create($filename, 100, 'application/pdf');
  879. $response = $this->actingAs($this->managerUser)
  880. ->post(route('order.upload-document', $order), [
  881. 'document' => [$document],
  882. ]);
  883. $response->assertRedirect(route('order.show', $order));
  884. $saved = $order->fresh()->documents->first();
  885. $this->assertNotNull($saved);
  886. $this->assertEquals($filename, $saved->original_name);
  887. $this->assertEquals('orders/' . $order->id . '/document/' . $filename, $saved->path);
  888. Storage::disk('public')->assertExists($saved->path);
  889. }
  890. public function test_upload_statement_preserves_unicode_and_quotes_filename(): void
  891. {
  892. Storage::fake('public');
  893. $order = Order::factory()->create();
  894. $filename = "Акт «приемки» 'этап 1' \"финал\".pdf";
  895. $statement = UploadedFile::fake()->create($filename, 100, 'application/pdf');
  896. $response = $this->actingAs($this->managerUser)
  897. ->post(route('order.upload-statement', $order), [
  898. 'statement' => [$statement],
  899. ]);
  900. $response->assertRedirect(route('order.show', $order));
  901. $saved = $order->fresh()->statements->first();
  902. $this->assertNotNull($saved);
  903. $this->assertEquals($filename, $saved->original_name);
  904. $this->assertEquals('orders/' . $order->id . '/statement/' . $filename, $saved->path);
  905. Storage::disk('public')->assertExists($saved->path);
  906. }
  907. public function test_upload_document_limits_to_5_files(): void
  908. {
  909. Storage::fake('public');
  910. $order = Order::factory()->create();
  911. $documents = [];
  912. for ($i = 0; $i < 7; $i++) {
  913. $documents[] = UploadedFile::fake()->create("document{$i}.pdf", 100);
  914. }
  915. $this->actingAs($this->managerUser)
  916. ->post(route('order.upload-document', $order), [
  917. 'document' => $documents,
  918. ]);
  919. $this->assertCount(5, $order->fresh()->documents);
  920. }
  921. public function test_can_upload_webp_photo(): void
  922. {
  923. Storage::fake('public');
  924. $order = Order::factory()->create();
  925. $photo = UploadedFile::fake()->create('photo.webp', 100, 'image/webp');
  926. $response = $this->actingAs($this->managerUser)
  927. ->post(route('order.upload-photo', $order), [
  928. 'photo' => [$photo],
  929. ]);
  930. $response->assertRedirect();
  931. $saved = $order->fresh()->photos->first();
  932. $this->assertNotNull($saved);
  933. $this->assertSame('photo.webp', $saved->original_name);
  934. }
  935. // ==================== Generation ====================
  936. public function test_generate_installation_pack_is_allowed_for_any_status_when_data_is_valid(): void
  937. {
  938. Bus::fake();
  939. $product = Product::factory()->create();
  940. $mafOrder = MafOrder::factory()->create(['product_id' => $product->id]);
  941. $order = Order::factory()->create([
  942. 'order_status_id' => Order::STATUS_NEW,
  943. ]);
  944. ProductSKU::factory()->create([
  945. 'order_id' => $order->id,
  946. 'product_id' => $product->id,
  947. 'maf_order_id' => $mafOrder->id,
  948. ]);
  949. $response = $this->actingAs($this->managerUser)
  950. ->get(route('order.generate-installation-pack', $order));
  951. $response->assertRedirect(route('order.show', $order));
  952. $response->assertSessionHas('success');
  953. Bus::assertDispatched(GenerateInstallationPack::class);
  954. }
  955. public function test_generate_installation_pack_still_requires_connected_maf(): void
  956. {
  957. Bus::fake();
  958. $product = Product::factory()->create();
  959. $order = Order::factory()->create([
  960. 'order_status_id' => Order::STATUS_IN_MOUNT,
  961. ]);
  962. ProductSKU::factory()->create([
  963. 'order_id' => $order->id,
  964. 'product_id' => $product->id,
  965. 'maf_order_id' => null,
  966. ]);
  967. $response = $this->actingAs($this->managerUser)
  968. ->get(route('order.generate-installation-pack', $order));
  969. $response->assertRedirect(route('order.show', $order));
  970. $response->assertSessionHas('danger');
  971. Bus::assertNotDispatched(GenerateInstallationPack::class);
  972. }
  973. public function test_admin_can_create_ttn_with_departure_date_and_increment_counter(): void
  974. {
  975. Bus::fake();
  976. Setting::set(Setting::KEY_TTN_NEXT_NUMBER, 10);
  977. $product = Product::factory()->create();
  978. $order = Order::factory()->create([
  979. 'installation_date' => '2026-04-15',
  980. ]);
  981. $sku = ProductSKU::factory()->create([
  982. 'order_id' => $order->id,
  983. 'product_id' => $product->id,
  984. ]);
  985. $response = $this->actingAs($this->adminUser)
  986. ->post(route('order.create-ttn'), [
  987. 'order_number' => 'З-100',
  988. 'order_date' => '2026-04-07',
  989. 'departure_date' => '2026-04-08',
  990. 'order_sum' => '150000',
  991. 'skus' => [$sku->id],
  992. ]);
  993. $response->assertRedirect();
  994. $response->assertSessionHas('success');
  995. $this->assertDatabaseHas('ttns', [
  996. 'ttn_number' => 10,
  997. 'order_number' => 'З-100',
  998. 'order_date' => '2026-04-07',
  999. 'departure_date' => '2026-04-08',
  1000. 'order_sum' => '150000',
  1001. ]);
  1002. $this->assertSame(11, Setting::getInt(Setting::KEY_TTN_NEXT_NUMBER));
  1003. $ttn = Ttn::query()->where('ttn_number', 10)->firstOrFail();
  1004. $this->assertSame([(string) $sku->id], array_map('strval', json_decode($ttn->skus, true)));
  1005. Bus::assertDispatched(GenerateTtnPack::class);
  1006. }
  1007. // ==================== Export ====================
  1008. public function test_can_export_orders(): void
  1009. {
  1010. Order::factory()->count(3)->create();
  1011. // This route requires admin role
  1012. $response = $this->actingAs($this->adminUser)
  1013. ->post(route('order.export'));
  1014. $response->assertRedirect(route('order.index'));
  1015. $response->assertSessionHas('success');
  1016. }
  1017. public function test_can_export_single_order(): void
  1018. {
  1019. $order = Order::factory()->create();
  1020. // This route requires admin role
  1021. $response = $this->actingAs($this->adminUser)
  1022. ->post(route('order.export-one', $order));
  1023. $response->assertRedirect(route('order.show', $order));
  1024. $response->assertSessionHas('success');
  1025. }
  1026. }