OrderControllerTest.php 41 KB

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