OrderControllerTest.php 45 KB

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