OrderControllerTest.php 36 KB

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