OrderControllerTest.php 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302
  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\Permission;
  13. use App\Models\Product;
  14. use App\Models\ProductSKU;
  15. use App\Models\Role;
  16. use App\Models\Setting;
  17. use App\Models\Ttn;
  18. use App\Models\User;
  19. use Illuminate\Foundation\Testing\RefreshDatabase;
  20. use Illuminate\Http\UploadedFile;
  21. use Illuminate\Support\Facades\Bus;
  22. use Illuminate\Support\Facades\Storage;
  23. use Tests\TestCase;
  24. class OrderControllerTest extends TestCase
  25. {
  26. use RefreshDatabase;
  27. protected $seed = true;
  28. private User $adminUser;
  29. private User $managerUser;
  30. private User $brigadierUser;
  31. protected function setUp(): void
  32. {
  33. parent::setUp();
  34. $this->adminUser = User::factory()->create(['role' => Role::ADMIN]);
  35. $this->managerUser = User::factory()->create(['role' => Role::MANAGER]);
  36. $this->brigadierUser = User::factory()->create(['role' => Role::BRIGADIER]);
  37. }
  38. // ==================== Authentication ====================
  39. public function test_guest_cannot_access_orders_index(): void
  40. {
  41. $response = $this->get(route('order.index'));
  42. $response->assertRedirect(route('login'));
  43. }
  44. public function test_authenticated_user_can_access_orders_index(): void
  45. {
  46. $response = $this->actingAs($this->managerUser)
  47. ->get(route('order.index'));
  48. $response->assertStatus(200);
  49. $response->assertViewIs('orders.index');
  50. }
  51. // ==================== Index ====================
  52. public function test_orders_index_displays_orders(): void
  53. {
  54. $order = Order::factory()->create([
  55. 'object_address' => 'ул. Проверочная, д. 101',
  56. ]);
  57. $response = $this->actingAs($this->managerUser)
  58. ->get(route('order.index'));
  59. $response->assertStatus(200);
  60. $response->assertSee($order->object_address);
  61. }
  62. public function test_orders_index_displays_products_total_column_value(): void
  63. {
  64. $order = Order::factory()->create([
  65. 'object_address' => 'ул. Колоночная, д. 10',
  66. ]);
  67. $product = Product::factory()->create();
  68. ProductSKU::factory()->count(3)->create([
  69. 'order_id' => $order->id,
  70. 'product_id' => $product->id,
  71. ]);
  72. $response = $this->actingAs($this->managerUser)
  73. ->get(route('order.index'));
  74. $response->assertOk();
  75. $response->assertSee('Всего МАФ');
  76. $response->assertSee('ул. Колоночная, д. 10');
  77. $response->assertViewHas('orders', static function ($orders) use ($order) {
  78. return (int) $orders->firstWhere('id', $order->id)?->products_total === 3;
  79. });
  80. }
  81. public function test_orders_index_displays_filtered_footer_stats(): void
  82. {
  83. $district = District::factory()->create(['shortname' => 'ЦАО']);
  84. $area = Area::factory()->create(['district_id' => $district->id, 'name' => 'Тверской']);
  85. $matchingOrder = Order::factory()->create([
  86. 'district_id' => $district->id,
  87. 'area_id' => $area->id,
  88. 'object_address' => 'ул. Итоговая, д. 1',
  89. ]);
  90. $otherOrder = Order::factory()->create([
  91. 'object_address' => 'ул. Неучтенная, д. 2',
  92. ]);
  93. $product = Product::factory()->create();
  94. ProductSKU::factory()->count(2)->create([
  95. 'order_id' => $matchingOrder->id,
  96. 'product_id' => $product->id,
  97. ]);
  98. ProductSKU::factory()->count(5)->create([
  99. 'order_id' => $otherOrder->id,
  100. 'product_id' => $product->id,
  101. ]);
  102. $response = $this->actingAs($this->managerUser)
  103. ->get(route('order.index', ['s' => 'Итоговая']));
  104. $response->assertOk();
  105. $response->assertSee('Адресов: 1', false);
  106. $response->assertSee('МАФ: 2', false);
  107. $response->assertDontSee('МАФ: 7', false);
  108. }
  109. public function test_brigadier_sees_only_assigned_orders(): void
  110. {
  111. $assignedOrder = Order::factory()->create([
  112. 'brigadier_id' => $this->brigadierUser->id,
  113. 'object_address' => 'ул. Бригадирская, д. 10',
  114. ]);
  115. $otherOrder = Order::factory()->create([
  116. 'brigadier_id' => User::factory()->create(['role' => Role::BRIGADIER])->id,
  117. 'object_address' => 'ул. Чужая, д. 20',
  118. ]);
  119. $response = $this->actingAs($this->brigadierUser)
  120. ->get(route('order.index'));
  121. $response->assertStatus(200);
  122. $response->assertSee($assignedOrder->object_address);
  123. $response->assertDontSee($otherOrder->object_address);
  124. }
  125. public function test_brigadier_does_not_see_handed_over_orders_in_index(): void
  126. {
  127. $visibleOrder = Order::factory()->create([
  128. 'brigadier_id' => $this->brigadierUser->id,
  129. 'order_status_id' => Order::STATUS_IN_MOUNT,
  130. 'object_address' => 'ул. Видимая, д. 11',
  131. ]);
  132. $hiddenOrder = Order::factory()->create([
  133. 'brigadier_id' => $this->brigadierUser->id,
  134. 'order_status_id' => Order::STATUS_HANDED_OVER,
  135. 'object_address' => 'ул. Сданная, д. 12',
  136. ]);
  137. $response = $this->actingAs($this->brigadierUser)
  138. ->get(route('order.index'));
  139. $response->assertStatus(200);
  140. $response->assertSee($visibleOrder->object_address);
  141. $response->assertDontSee($hiddenOrder->object_address);
  142. }
  143. // ==================== Create ====================
  144. public function test_can_view_create_order_form(): void
  145. {
  146. $response = $this->actingAs($this->adminUser)
  147. ->get(route('order.create'));
  148. $response->assertStatus(200);
  149. $response->assertViewIs('orders.edit');
  150. }
  151. // ==================== Store ====================
  152. public function test_can_create_new_order(): void
  153. {
  154. $district = District::factory()->create();
  155. $area = Area::factory()->create();
  156. $objectType = ObjectType::factory()->create();
  157. $response = $this->actingAs($this->managerUser)
  158. ->post(route('order.store'), [
  159. 'name' => 'Тестовый заказ',
  160. 'user_id' => $this->managerUser->id,
  161. 'district_id' => $district->id,
  162. 'area_id' => $area->id,
  163. 'object_address' => 'ул. Тестовая, д. 1',
  164. 'object_type_id' => $objectType->id,
  165. 'comment' => 'Тестовый комментарий',
  166. ]);
  167. $response->assertRedirect();
  168. $this->assertDatabaseHas('orders', [
  169. 'object_address' => 'ул. Тестовая, д. 1',
  170. 'order_status_id' => Order::STATUS_NEW,
  171. ]);
  172. }
  173. public function test_order_show_uses_nav_context_for_back_url(): void
  174. {
  175. $order = Order::factory()->create();
  176. $indexResponse = $this->actingAs($this->managerUser)
  177. ->get(route('order.index', [
  178. 'filters' => ['object_address' => 'Test address'],
  179. ]));
  180. $nav = $indexResponse->viewData('nav');
  181. $response = $this->actingAs($this->managerUser)
  182. ->get(route('order.show', [
  183. 'order' => $order,
  184. 'nav' => $nav,
  185. ]));
  186. $response->assertOk();
  187. $response->assertViewHas('nav', $nav);
  188. $response->assertViewHas('back_url', function (string $backUrl): bool {
  189. if (!str_starts_with($backUrl, route('order.index'))) {
  190. return false;
  191. }
  192. $query = parse_url($backUrl, PHP_URL_QUERY);
  193. parse_str((string) $query, $params);
  194. return ($params['filters']['object_address'] ?? null) === 'Test address';
  195. });
  196. }
  197. public function test_order_index_starts_new_navigation_context(): void
  198. {
  199. $response = $this->actingAs($this->managerUser)
  200. ->withSession([
  201. 'navigation' => [
  202. 'existing-card-nav' => [
  203. 'updated_at' => time(),
  204. 'stack' => [
  205. route('order.show', Order::factory()->create()),
  206. route('reclamations.index'),
  207. ],
  208. ],
  209. ],
  210. ])
  211. ->get(route('order.index', ['nav' => 'existing-card-nav']));
  212. $response->assertOk();
  213. $response->assertViewHas('nav', function (string $nav): bool {
  214. return $nav !== 'existing-card-nav';
  215. });
  216. }
  217. public function test_order_store_redirects_to_show_with_nav_token(): void
  218. {
  219. $district = District::factory()->create();
  220. $area = Area::factory()->create();
  221. $objectType = ObjectType::factory()->create();
  222. $response = $this->actingAs($this->managerUser)
  223. ->withSession([
  224. 'navigation' => [
  225. 'order-nav-token' => [
  226. 'updated_at' => time(),
  227. 'stack' => [
  228. route('order.index'),
  229. ],
  230. ],
  231. ],
  232. ])
  233. ->post(route('order.store'), [
  234. 'nav' => 'order-nav-token',
  235. 'name' => 'Тестовый заказ',
  236. 'user_id' => $this->managerUser->id,
  237. 'district_id' => $district->id,
  238. 'area_id' => $area->id,
  239. 'object_address' => 'ул. Навигационная, д. 5',
  240. 'object_type_id' => $objectType->id,
  241. 'comment' => 'Тестовый комментарий',
  242. ]);
  243. $createdOrder = Order::query()->where('object_address', 'ул. Навигационная, д. 5')->firstOrFail();
  244. $response->assertRedirect(route('order.show', [
  245. 'order' => $createdOrder,
  246. 'nav' => 'order-nav-token',
  247. ]));
  248. }
  249. public function test_creating_order_sets_tg_group_name(): void
  250. {
  251. $district = District::factory()->create(['shortname' => 'ЦАО']);
  252. $area = Area::factory()->create(['name' => 'Тверской']);
  253. $objectType = ObjectType::factory()->create();
  254. $this->actingAs($this->managerUser)
  255. ->post(route('order.store'), [
  256. 'name' => 'Тестовый заказ',
  257. 'user_id' => $this->managerUser->id,
  258. 'district_id' => $district->id,
  259. 'area_id' => $area->id,
  260. 'object_address' => 'ул. Пушкина',
  261. 'object_type_id' => $objectType->id,
  262. ]);
  263. $order = Order::where('object_address', 'ул. Пушкина')->first();
  264. $this->assertStringContainsString('ЦАО', $order->tg_group_name);
  265. $this->assertStringContainsString('Тверской', $order->tg_group_name);
  266. }
  267. public function test_can_update_existing_order(): void
  268. {
  269. $order = Order::factory()->create([
  270. 'object_address' => 'Старый адрес',
  271. ]);
  272. $response = $this->actingAs($this->managerUser)
  273. ->post(route('order.store'), [
  274. 'id' => $order->id,
  275. 'name' => $order->name,
  276. 'user_id' => $order->user_id,
  277. 'district_id' => $order->district_id,
  278. 'area_id' => $order->area_id,
  279. 'object_address' => 'Новый адрес',
  280. 'object_type_id' => $order->object_type_id,
  281. ]);
  282. $response->assertRedirect();
  283. $this->assertDatabaseHas('orders', [
  284. 'id' => $order->id,
  285. 'object_address' => 'Новый адрес',
  286. ]);
  287. }
  288. public function test_cannot_set_in_mount_status_when_order_has_unlinked_maf(): void
  289. {
  290. $product = Product::factory()->create();
  291. $order = Order::factory()->readyToMount()->withBrigadier($this->brigadierUser)->create([
  292. 'installation_date' => '2026-04-25',
  293. ]);
  294. ProductSKU::factory()->create([
  295. 'order_id' => $order->id,
  296. 'product_id' => $product->id,
  297. 'maf_order_id' => null,
  298. ]);
  299. $response = $this->actingAs($this->adminUser)
  300. ->post(route('order.update'), [
  301. 'id' => $order->id,
  302. 'order_status_id' => Order::STATUS_IN_MOUNT,
  303. ]);
  304. $response->assertRedirect();
  305. $response->assertSessionHas('danger', function ($messages) {
  306. return in_array('МАФ не привязан к заказу', (array) $messages, true);
  307. });
  308. $this->assertDatabaseHas('orders', [
  309. 'id' => $order->id,
  310. 'order_status_id' => Order::STATUS_READY_TO_MOUNT,
  311. ]);
  312. }
  313. public function test_ajax_cannot_set_in_mount_status_when_maf_order_number_is_empty(): void
  314. {
  315. $product = Product::factory()->create();
  316. $order = Order::factory()->readyToMount()->withBrigadier($this->brigadierUser)->create([
  317. 'installation_date' => '2026-04-25',
  318. ]);
  319. $mafOrder = MafOrder::factory()->create([
  320. 'product_id' => $product->id,
  321. 'order_number' => '',
  322. ]);
  323. ProductSKU::factory()->create([
  324. 'order_id' => $order->id,
  325. 'product_id' => $product->id,
  326. 'maf_order_id' => $mafOrder->id,
  327. ]);
  328. $response = $this->actingAs($this->adminUser)
  329. ->withHeader('X-Requested-With', 'XMLHttpRequest')
  330. ->post(route('order.update'), [
  331. 'id' => $order->id,
  332. 'order_status_id' => Order::STATUS_IN_MOUNT,
  333. ]);
  334. $response->assertStatus(422);
  335. $response->assertJson([
  336. 'message' => 'МАФ не привязан к заказу',
  337. ]);
  338. $this->assertDatabaseHas('orders', [
  339. 'id' => $order->id,
  340. 'order_status_id' => Order::STATUS_READY_TO_MOUNT,
  341. ]);
  342. }
  343. public function test_can_set_in_mount_status_when_all_mafs_are_linked_to_order_numbers(): void
  344. {
  345. $product = Product::factory()->create();
  346. $order = Order::factory()->readyToMount()->withBrigadier($this->brigadierUser)->create([
  347. 'installation_date' => '2026-04-25',
  348. ]);
  349. $mafOrder = MafOrder::factory()->create([
  350. 'product_id' => $product->id,
  351. 'order_number' => 'MO-7788',
  352. ]);
  353. ProductSKU::factory()->create([
  354. 'order_id' => $order->id,
  355. 'product_id' => $product->id,
  356. 'maf_order_id' => $mafOrder->id,
  357. ]);
  358. $response = $this->actingAs($this->adminUser)
  359. ->post(route('order.update'), [
  360. 'id' => $order->id,
  361. 'order_status_id' => Order::STATUS_IN_MOUNT,
  362. ]);
  363. $response->assertRedirect();
  364. $this->assertDatabaseHas('orders', [
  365. 'id' => $order->id,
  366. 'order_status_id' => Order::STATUS_IN_MOUNT,
  367. ]);
  368. }
  369. public function test_manager_can_update_ready_date(): void
  370. {
  371. $order = Order::factory()->create([
  372. 'ready_date' => '2026-04-01',
  373. ]);
  374. $response = $this->actingAs($this->managerUser)
  375. ->post(route('order.store'), [
  376. 'id' => $order->id,
  377. 'ready_date' => '2026-04-25',
  378. ]);
  379. $response->assertRedirect();
  380. $this->assertDatabaseHas('orders', [
  381. 'id' => $order->id,
  382. 'ready_date' => '2026-04-25',
  383. ]);
  384. }
  385. // ==================== Show ====================
  386. public function test_can_view_order_details(): void
  387. {
  388. $order = Order::factory()->create([
  389. 'object_address' => 'ул. Детальная, д. 5',
  390. ]);
  391. $response = $this->actingAs($this->managerUser)
  392. ->get(route('order.show', $order));
  393. $response->assertStatus(200);
  394. $response->assertViewIs('orders.show');
  395. $response->assertSee($order->object_address);
  396. }
  397. public function test_order_details_show_area_and_district(): void
  398. {
  399. $district = District::factory()->create(['name' => 'Центральный округ']);
  400. $area = Area::factory()->create([
  401. 'district_id' => $district->id,
  402. 'name' => 'Тверской район',
  403. ]);
  404. $order = Order::factory()->create([
  405. 'district_id' => $district->id,
  406. 'area_id' => $area->id,
  407. ]);
  408. $response = $this->actingAs($this->managerUser)
  409. ->get(route('order.show', $order));
  410. $response->assertOk();
  411. $response->assertSee('Центральный округ');
  412. $response->assertSee('Тверской район');
  413. }
  414. public function test_order_details_render_inline_maf_fields_and_passport_upload_by_permissions(): void
  415. {
  416. $order = Order::factory()->create();
  417. $product = Product::factory()->create(['article' => 'MAF-INLINE-001']);
  418. $sku = ProductSKU::factory()->create([
  419. 'order_id' => $order->id,
  420. 'product_id' => $product->id,
  421. 'passport_id' => null,
  422. 'rfid' => 'RFID-ORDER-OLD',
  423. 'factory_number' => 'FN-ORDER-OLD',
  424. 'manufacture_date' => '2026-05-19',
  425. 'statement_number' => 'STAT-ORDER-OLD',
  426. 'statement_date' => '2026-05-18',
  427. ]);
  428. $response = $this->actingAs($this->adminUser)
  429. ->get(route('order.show', $order));
  430. $response->assertOk();
  431. $response->assertSee('inline-product-sku-field', false);
  432. $response->assertSee('data-field="rfid"', false);
  433. $response->assertSee('data-field="factory_number"', false);
  434. $response->assertSee('data-field="manufacture_date"', false);
  435. $response->assertSee('data-field="statement_number"', false);
  436. $response->assertSee('data-field="statement_date"', false);
  437. $response->assertSee('Добавить Ведомость');
  438. $response->assertSee(route('order.add-statement-to-mafs', $order), false);
  439. $response->assertSee(route('product_sku.inline-update', $sku->id), false);
  440. $response->assertSee('upload-maf-passport', false);
  441. $response->assertSee(route('product-sku.upload-passport', ['product_sku' => $sku]), false);
  442. }
  443. public function test_order_details_hide_inline_maf_controls_without_permissions(): void
  444. {
  445. $role = Role::query()->create([
  446. 'slug' => 'orders_viewer_without_maf_permissions',
  447. 'name' => 'Orders viewer without MAF permissions',
  448. 'is_system' => false,
  449. 'is_active' => true,
  450. ]);
  451. $permissions = Permission::query()
  452. ->whereIn('slug', ['orders.view', 'orders.scope.manager'])
  453. ->pluck('id')
  454. ->mapWithKeys(fn (int $id): array => [$id => ['effect' => 'allow']]);
  455. $role->permissions()->sync($permissions);
  456. $user = User::factory()->create([
  457. 'role' => $role->slug,
  458. 'role_id' => $role->id,
  459. ]);
  460. $order = Order::factory()->create();
  461. $product = Product::factory()->create(['article' => 'MAF-NO-INLINE-001']);
  462. $sku = ProductSKU::factory()->create([
  463. 'order_id' => $order->id,
  464. 'product_id' => $product->id,
  465. 'passport_id' => null,
  466. 'rfid' => 'RFID-READONLY',
  467. ]);
  468. $response = $this->actingAs($user)
  469. ->get(route('order.show', $order));
  470. $response->assertOk();
  471. $response->assertDontSee(route('product_sku.inline-update', $sku->id), false);
  472. $response->assertDontSee(route('product-sku.upload-passport', ['product_sku' => $sku]), false);
  473. }
  474. public function test_can_add_statement_to_selected_mafs_on_order(): void
  475. {
  476. $order = Order::factory()->create();
  477. $otherOrder = Order::factory()->create();
  478. $product = Product::factory()->create();
  479. $selectedSku = ProductSKU::factory()->create([
  480. 'order_id' => $order->id,
  481. 'product_id' => $product->id,
  482. 'statement_number' => null,
  483. 'statement_date' => null,
  484. ]);
  485. $notSelectedSku = ProductSKU::factory()->create([
  486. 'order_id' => $order->id,
  487. 'product_id' => $product->id,
  488. 'statement_number' => null,
  489. 'statement_date' => null,
  490. ]);
  491. $otherOrderSku = ProductSKU::factory()->create([
  492. 'order_id' => $otherOrder->id,
  493. 'product_id' => $product->id,
  494. 'statement_number' => null,
  495. 'statement_date' => null,
  496. ]);
  497. $response = $this->actingAs($this->adminUser)
  498. ->post(route('order.add-statement-to-mafs', $order), [
  499. 'statement_number' => 'STAT-2026-05',
  500. 'statement_date' => '2026-05-19',
  501. 'skus' => [$selectedSku->id, $otherOrderSku->id],
  502. ]);
  503. $response->assertRedirect();
  504. $response->assertSessionHas('success', 'Ведомость добавлена к МАФ: 1.');
  505. $this->assertDatabaseHas('products_sku', [
  506. 'id' => $selectedSku->id,
  507. 'statement_number' => 'STAT-2026-05',
  508. 'statement_date' => '2026-05-19',
  509. ]);
  510. $this->assertDatabaseHas('products_sku', [
  511. 'id' => $notSelectedSku->id,
  512. 'statement_number' => null,
  513. 'statement_date' => null,
  514. ]);
  515. $this->assertDatabaseHas('products_sku', [
  516. 'id' => $otherOrderSku->id,
  517. 'statement_number' => null,
  518. 'statement_date' => null,
  519. ]);
  520. }
  521. public function test_add_statement_to_all_mafs_marks_order_paid_when_upd_exists(): void
  522. {
  523. $order = Order::factory()->create([
  524. 'order_status_id' => Order::STATUS_HANDED_OVER,
  525. ]);
  526. $firstSku = ProductSKU::factory()->create([
  527. 'order_id' => $order->id,
  528. 'statement_number' => null,
  529. 'statement_date' => null,
  530. 'upd_number' => 'UPD-001',
  531. ]);
  532. $secondSku = ProductSKU::factory()->create([
  533. 'order_id' => $order->id,
  534. 'statement_number' => null,
  535. 'statement_date' => null,
  536. 'upd_number' => 'UPD-002',
  537. ]);
  538. $response = $this->actingAs($this->adminUser)
  539. ->post(route('order.add-statement-to-mafs', $order), [
  540. 'statement_number' => 'STAT-2026-05',
  541. 'statement_date' => '2026-05-19',
  542. 'skus' => [$firstSku->id, $secondSku->id],
  543. ]);
  544. $response->assertRedirect();
  545. $this->assertDatabaseHas('orders', [
  546. 'id' => $order->id,
  547. 'order_status_id' => Order::STATUS_PAID,
  548. ]);
  549. }
  550. public function test_add_statement_to_mafs_requires_field_permissions(): void
  551. {
  552. $order = Order::factory()->create();
  553. $sku = ProductSKU::factory()->create(['order_id' => $order->id]);
  554. $response = $this->actingAs($this->managerUser)
  555. ->post(route('order.add-statement-to-mafs', $order), [
  556. 'statement_number' => 'STAT-DENIED',
  557. 'statement_date' => '2026-05-19',
  558. 'skus' => [$sku->id],
  559. ]);
  560. $response->assertForbidden();
  561. }
  562. public function test_brigadier_cannot_view_handed_over_order_details(): void
  563. {
  564. $order = Order::factory()->create([
  565. 'brigadier_id' => $this->brigadierUser->id,
  566. 'order_status_id' => Order::STATUS_HANDED_OVER,
  567. ]);
  568. $response = $this->actingAs($this->brigadierUser)
  569. ->get(route('order.show', $order));
  570. $response->assertStatus(403);
  571. }
  572. // ==================== Edit ====================
  573. public function test_can_view_edit_order_form(): void
  574. {
  575. $order = Order::factory()->create();
  576. $response = $this->actingAs($this->managerUser)
  577. ->get(route('order.edit', $order));
  578. $response->assertStatus(200);
  579. $response->assertViewIs('orders.edit');
  580. }
  581. public function test_manager_can_edit_ready_date_in_order_edit_form(): void
  582. {
  583. $order = Order::factory()->create();
  584. $response = $this->actingAs($this->managerUser)
  585. ->get(route('order.edit', $order));
  586. $response->assertOk();
  587. $this->assertMatchesRegularExpression(
  588. '/<input[^>]*name="ready_date"(?:(?!disabled).)*>/s',
  589. $response->getContent()
  590. );
  591. }
  592. // ==================== Destroy ====================
  593. public function test_can_delete_order(): void
  594. {
  595. $order = Order::factory()->create();
  596. $orderId = $order->id;
  597. $response = $this->actingAs($this->adminUser)
  598. ->delete(route('order.destroy', $order));
  599. $response->assertRedirect(route('order.index'));
  600. // Order uses SoftDeletes, so check deleted_at is set
  601. $this->assertSoftDeleted('orders', ['id' => $orderId]);
  602. }
  603. public function test_deleting_order_removes_related_product_skus(): void
  604. {
  605. $order = Order::factory()->create();
  606. $product = Product::factory()->create();
  607. $sku = ProductSKU::factory()->create([
  608. 'order_id' => $order->id,
  609. 'product_id' => $product->id,
  610. ]);
  611. $this->actingAs($this->adminUser)
  612. ->delete(route('order.destroy', $order));
  613. // ProductSKU uses SoftDeletes, so check deleted_at is set
  614. $this->assertSoftDeleted('products_sku', ['id' => $sku->id]);
  615. }
  616. // ==================== Search ====================
  617. public function test_search_returns_matching_orders(): void
  618. {
  619. $order = Order::factory()->create([
  620. 'object_address' => 'ул. Уникальная Тестовая, д. 999',
  621. ]);
  622. $otherOrder = Order::factory()->create([
  623. 'object_address' => 'ул. Другая, д. 1',
  624. ]);
  625. $response = $this->actingAs($this->managerUser)
  626. ->get(route('order.index', ['s' => 'Уникальная Тестовая']));
  627. $response->assertStatus(200);
  628. $response->assertSee($order->object_address);
  629. $response->assertDontSee($otherOrder->object_address);
  630. }
  631. public function test_order_search_route_can_search_by_manager_name(): void
  632. {
  633. $manager = User::factory()->create([
  634. 'role' => Role::MANAGER,
  635. 'name' => 'Менеджер Поиска',
  636. ]);
  637. $matchedOrder = Order::factory()->create([
  638. 'user_id' => $manager->id,
  639. 'object_address' => 'ул. Найденная, д. 7',
  640. ]);
  641. $otherOrder = Order::factory()->create([
  642. 'object_address' => 'ул. Не должна попасть, д. 8',
  643. ]);
  644. $response = $this->actingAs($this->adminUser)
  645. ->getJson(route('order.search', ['s' => 'Менеджер Поиска']));
  646. $response->assertOk();
  647. $response->assertJsonPath((string) $matchedOrder->id, $matchedOrder->move_maf_name);
  648. $response->assertJsonMissing([$otherOrder->id => $otherOrder->move_maf_name]);
  649. }
  650. public function test_order_search_route_includes_site_name_and_excludes_current_order(): void
  651. {
  652. $district = District::factory()->create(['name' => 'Северный округ']);
  653. $area = Area::factory()->create([
  654. 'district_id' => $district->id,
  655. 'name' => 'Левобережный',
  656. ]);
  657. $currentOrder = Order::factory()->create([
  658. 'district_id' => $district->id,
  659. 'area_id' => $area->id,
  660. 'name' => 'Текущая площадка',
  661. 'object_address' => 'ул. Проверочная, д. 1',
  662. ]);
  663. $targetOrder = Order::factory()->create([
  664. 'district_id' => $district->id,
  665. 'area_id' => $area->id,
  666. 'name' => 'Площадка назначения',
  667. 'object_address' => 'ул. Проверочная, д. 2',
  668. ]);
  669. $response = $this->actingAs($this->adminUser)
  670. ->getJson(route('order.search', [
  671. 's' => 'Проверочная',
  672. 'current_order_id' => $currentOrder->id,
  673. ]));
  674. $response->assertOk();
  675. $response->assertJsonPath((string) $targetOrder->id, $targetOrder->move_maf_name);
  676. $response->assertJsonMissing([(string) $currentOrder->id => $currentOrder->move_maf_name]);
  677. }
  678. // ==================== MAF Operations ====================
  679. public function test_get_maf_to_order_assigns_available_maf(): void
  680. {
  681. $product = Product::factory()->create();
  682. $order = Order::factory()->create();
  683. $productSku = ProductSKU::factory()->create([
  684. 'order_id' => $order->id,
  685. 'product_id' => $product->id,
  686. 'maf_order_id' => null,
  687. 'status' => 'требуется',
  688. ]);
  689. $mafOrder = MafOrder::factory()->create([
  690. 'product_id' => $product->id,
  691. 'in_stock' => 5,
  692. ]);
  693. // This route requires admin role
  694. $response = $this->actingAs($this->adminUser)
  695. ->get(route('order.get-maf', $order));
  696. $response->assertRedirect(route('order.show', $order));
  697. $response->assertSessionHas('success', function ($messages) {
  698. return str_contains(implode(' ', (array) $messages), 'Привязано МАФ: 1.');
  699. });
  700. $productSku->refresh();
  701. $this->assertEquals($mafOrder->id, $productSku->maf_order_id);
  702. $this->assertEquals('отгружен', $productSku->status);
  703. $mafOrder->refresh();
  704. $this->assertEquals(4, $mafOrder->in_stock);
  705. }
  706. public function test_get_maf_to_order_shows_error_when_not_enough_stock(): void
  707. {
  708. $product = Product::factory()->create();
  709. $order = Order::factory()->create();
  710. $productSku = ProductSKU::factory()->create([
  711. 'order_id' => $order->id,
  712. 'product_id' => $product->id,
  713. 'maf_order_id' => null,
  714. 'status' => 'требуется',
  715. ]);
  716. $response = $this->actingAs($this->adminUser)
  717. ->get(route('order.get-maf', $order));
  718. $response->assertRedirect(route('order.show', $order));
  719. $response->assertSessionHas('danger', function ($messages) {
  720. return str_contains(implode(' ', (array) $messages), 'Не удалось привязать 1 шт. МАФ');
  721. });
  722. $productSku->refresh();
  723. $this->assertNull($productSku->maf_order_id);
  724. $this->assertEquals('требуется', $productSku->status);
  725. }
  726. public function test_revert_maf_returns_maf_to_stock(): void
  727. {
  728. $product = Product::factory()->create();
  729. $order = Order::factory()->create();
  730. $mafOrder = MafOrder::factory()->create([
  731. 'product_id' => $product->id,
  732. 'in_stock' => 3,
  733. ]);
  734. $productSku = ProductSKU::factory()->create([
  735. 'order_id' => $order->id,
  736. 'product_id' => $product->id,
  737. 'maf_order_id' => $mafOrder->id,
  738. 'status' => 'отгружен',
  739. ]);
  740. // This route requires admin role
  741. $response = $this->actingAs($this->adminUser)
  742. ->get(route('order.revert-maf', $order));
  743. $response->assertRedirect(route('order.show', $order));
  744. $response->assertSessionHas('success', function ($messages) {
  745. return str_contains(implode(' ', (array) $messages), 'Отвязано МАФ: 1.');
  746. });
  747. $productSku->refresh();
  748. $this->assertNull($productSku->maf_order_id);
  749. $this->assertEquals('требуется', $productSku->status);
  750. $mafOrder->refresh();
  751. $this->assertEquals(4, $mafOrder->in_stock);
  752. }
  753. public function test_revert_maf_shows_error_when_nothing_is_attached(): void
  754. {
  755. $order = Order::factory()->create();
  756. ProductSKU::factory()->create([
  757. 'order_id' => $order->id,
  758. 'maf_order_id' => null,
  759. 'status' => 'требуется',
  760. ]);
  761. $response = $this->actingAs($this->adminUser)
  762. ->get(route('order.revert-maf', $order));
  763. $response->assertRedirect(route('order.show', $order));
  764. $response->assertSessionHas('danger', function ($messages) {
  765. return str_contains(implode(' ', (array) $messages), 'Нет МАФ для отвязки');
  766. });
  767. }
  768. public function test_store_order_cannot_remove_attached_maf_from_order_list(): void
  769. {
  770. $attachedProduct = Product::factory()->create();
  771. $otherProduct = Product::factory()->create();
  772. $order = Order::factory()->create([
  773. 'order_status_id' => Order::STATUS_NEW,
  774. ]);
  775. $mafOrder = MafOrder::factory()->create([
  776. 'product_id' => $attachedProduct->id,
  777. 'in_stock' => 1,
  778. ]);
  779. $attachedSku = ProductSKU::factory()->create([
  780. 'order_id' => $order->id,
  781. 'product_id' => $attachedProduct->id,
  782. 'maf_order_id' => $mafOrder->id,
  783. 'status' => 'отгружен',
  784. ]);
  785. ProductSKU::factory()->create([
  786. 'order_id' => $order->id,
  787. 'product_id' => $otherProduct->id,
  788. 'maf_order_id' => null,
  789. 'status' => 'требуется',
  790. ]);
  791. $response = $this->actingAs($this->adminUser)
  792. ->post(route('order.store'), [
  793. 'id' => $order->id,
  794. 'products' => [$otherProduct->id],
  795. 'quantity' => [1],
  796. ]);
  797. $response->assertRedirect();
  798. $response->assertSessionHas('danger', function ($messages) {
  799. return str_contains(implode(' ', (array) $messages), 'Нельзя удалить привязанные МАФ');
  800. });
  801. $this->assertDatabaseHas('products_sku', [
  802. 'id' => $attachedSku->id,
  803. 'maf_order_id' => $mafOrder->id,
  804. 'deleted_at' => null,
  805. ]);
  806. }
  807. public function test_store_order_cannot_add_new_positions_when_has_attached_maf(): void
  808. {
  809. $attachedProduct = Product::factory()->create();
  810. $existingProduct = Product::factory()->create();
  811. $newProduct = Product::factory()->create();
  812. $order = Order::factory()->create([
  813. 'order_status_id' => Order::STATUS_NEW,
  814. ]);
  815. $mafOrder = MafOrder::factory()->create([
  816. 'product_id' => $attachedProduct->id,
  817. 'in_stock' => 1,
  818. ]);
  819. ProductSKU::factory()->create([
  820. 'order_id' => $order->id,
  821. 'product_id' => $attachedProduct->id,
  822. 'maf_order_id' => $mafOrder->id,
  823. 'status' => 'отгружен',
  824. ]);
  825. ProductSKU::factory()->create([
  826. 'order_id' => $order->id,
  827. 'product_id' => $existingProduct->id,
  828. 'maf_order_id' => null,
  829. 'status' => 'требуется',
  830. ]);
  831. $response = $this->actingAs($this->adminUser)
  832. ->post(route('order.store'), [
  833. 'id' => $order->id,
  834. 'products' => [$attachedProduct->id, $existingProduct->id, $newProduct->id],
  835. 'quantity' => [1, 1, 1],
  836. ]);
  837. $response->assertRedirect();
  838. $response->assertSessionHas('danger', function ($messages) {
  839. return str_contains(implode(' ', (array) $messages), 'Нельзя добавлять новые позиции МАФ');
  840. });
  841. $this->assertDatabaseMissing('products_sku', [
  842. 'order_id' => $order->id,
  843. 'product_id' => $newProduct->id,
  844. 'deleted_at' => null,
  845. ]);
  846. }
  847. public function test_move_maf_transfers_sku_to_another_order(): void
  848. {
  849. $product = Product::factory()->create();
  850. $order1 = Order::factory()->create();
  851. $order2 = Order::factory()->create();
  852. $productSku = ProductSKU::factory()->create([
  853. 'order_id' => $order1->id,
  854. 'product_id' => $product->id,
  855. ]);
  856. // This route requires admin role
  857. $response = $this->actingAs($this->adminUser)
  858. ->post(route('order.move-maf'), [
  859. 'new_order_id' => $order2->id,
  860. 'ids' => json_encode([$productSku->id]),
  861. ]);
  862. $response->assertStatus(200);
  863. $response->assertJson(['success' => true]);
  864. $productSku->refresh();
  865. $this->assertEquals($order2->id, $productSku->order_id);
  866. }
  867. // ==================== Photo Management ====================
  868. public function test_can_upload_photo_to_order(): void
  869. {
  870. Storage::fake('public');
  871. $order = Order::factory()->create();
  872. // Use create() instead of image() to avoid GD extension requirement
  873. $photo = UploadedFile::fake()->create('photo.jpg', 100, 'image/jpeg');
  874. $response = $this->actingAs($this->managerUser)
  875. ->post(route('order.upload-photo', $order), [
  876. 'photo' => [$photo],
  877. ]);
  878. $response->assertRedirect(route('order.show', $order));
  879. $this->assertCount(1, $order->fresh()->photos);
  880. }
  881. public function test_can_delete_photo_from_order(): void
  882. {
  883. Storage::fake('public');
  884. $order = Order::factory()->create();
  885. $file = File::factory()->create();
  886. $order->photos()->attach($file);
  887. // This route requires admin role
  888. $response = $this->actingAs($this->adminUser)
  889. ->delete(route('order.delete-photo', [$order, $file]));
  890. $response->assertRedirect(route('order.show', $order));
  891. $this->assertCount(0, $order->fresh()->photos);
  892. }
  893. public function test_can_delete_all_photos_from_order(): void
  894. {
  895. Storage::fake('public');
  896. $order = Order::factory()->create();
  897. $files = File::factory()->count(3)->create();
  898. $order->photos()->attach($files->pluck('id'));
  899. // This route requires admin role
  900. $response = $this->actingAs($this->adminUser)
  901. ->delete(route('order.delete-all-photos', $order));
  902. $response->assertRedirect(route('order.show', $order));
  903. $this->assertCount(0, $order->fresh()->photos);
  904. }
  905. // ==================== Document Management ====================
  906. public function test_can_upload_document_to_order(): void
  907. {
  908. Storage::fake('public');
  909. $order = Order::factory()->create();
  910. $document = UploadedFile::fake()->create('document.pdf', 100);
  911. $response = $this->actingAs($this->managerUser)
  912. ->post(route('order.upload-document', $order), [
  913. 'document' => [$document],
  914. ]);
  915. $response->assertRedirect(route('order.show', $order));
  916. $this->assertCount(1, $order->fresh()->documents);
  917. }
  918. public function test_upload_document_preserves_unicode_and_quotes_filename(): void
  919. {
  920. Storage::fake('public');
  921. $order = Order::factory()->create();
  922. $filename = "Документ «смета» 'финал' \"v2\".pdf";
  923. $document = UploadedFile::fake()->create($filename, 100, 'application/pdf');
  924. $response = $this->actingAs($this->managerUser)
  925. ->post(route('order.upload-document', $order), [
  926. 'document' => [$document],
  927. ]);
  928. $response->assertRedirect(route('order.show', $order));
  929. $saved = $order->fresh()->documents->first();
  930. $this->assertNotNull($saved);
  931. $this->assertEquals($filename, $saved->original_name);
  932. $this->assertEquals('orders/' . $order->id . '/document/' . $filename, $saved->path);
  933. Storage::disk('public')->assertExists($saved->path);
  934. }
  935. public function test_upload_statement_preserves_unicode_and_quotes_filename(): void
  936. {
  937. Storage::fake('public');
  938. $order = Order::factory()->create();
  939. $filename = "Акт «приемки» 'этап 1' \"финал\".pdf";
  940. $statement = UploadedFile::fake()->create($filename, 100, 'application/pdf');
  941. $response = $this->actingAs($this->managerUser)
  942. ->post(route('order.upload-statement', $order), [
  943. 'statement' => [$statement],
  944. ]);
  945. $response->assertRedirect(route('order.show', $order));
  946. $saved = $order->fresh()->statements->first();
  947. $this->assertNotNull($saved);
  948. $this->assertEquals($filename, $saved->original_name);
  949. $this->assertEquals('orders/' . $order->id . '/statement/' . $filename, $saved->path);
  950. Storage::disk('public')->assertExists($saved->path);
  951. }
  952. public function test_upload_document_limits_to_5_files(): void
  953. {
  954. Storage::fake('public');
  955. $order = Order::factory()->create();
  956. $documents = [];
  957. for ($i = 0; $i < 7; $i++) {
  958. $documents[] = UploadedFile::fake()->create("document{$i}.pdf", 100);
  959. }
  960. $this->actingAs($this->managerUser)
  961. ->post(route('order.upload-document', $order), [
  962. 'document' => $documents,
  963. ]);
  964. $this->assertCount(5, $order->fresh()->documents);
  965. }
  966. public function test_can_upload_webp_photo(): void
  967. {
  968. Storage::fake('public');
  969. $order = Order::factory()->create();
  970. $photo = UploadedFile::fake()->create('photo.webp', 100, 'image/webp');
  971. $response = $this->actingAs($this->managerUser)
  972. ->post(route('order.upload-photo', $order), [
  973. 'photo' => [$photo],
  974. ]);
  975. $response->assertRedirect();
  976. $saved = $order->fresh()->photos->first();
  977. $this->assertNotNull($saved);
  978. $this->assertSame('photo.webp', $saved->original_name);
  979. }
  980. // ==================== Generation ====================
  981. public function test_generate_installation_pack_is_allowed_for_any_status_when_data_is_valid(): void
  982. {
  983. Bus::fake();
  984. $product = Product::factory()->create();
  985. $mafOrder = MafOrder::factory()->create(['product_id' => $product->id]);
  986. $order = Order::factory()->create([
  987. 'order_status_id' => Order::STATUS_NEW,
  988. ]);
  989. ProductSKU::factory()->create([
  990. 'order_id' => $order->id,
  991. 'product_id' => $product->id,
  992. 'maf_order_id' => $mafOrder->id,
  993. ]);
  994. $response = $this->actingAs($this->managerUser)
  995. ->get(route('order.generate-installation-pack', $order));
  996. $response->assertRedirect(route('order.show', $order));
  997. $response->assertSessionHas('success');
  998. Bus::assertDispatched(GenerateInstallationPack::class);
  999. }
  1000. public function test_generate_installation_pack_still_requires_connected_maf(): void
  1001. {
  1002. Bus::fake();
  1003. $product = Product::factory()->create();
  1004. $order = Order::factory()->create([
  1005. 'order_status_id' => Order::STATUS_IN_MOUNT,
  1006. ]);
  1007. ProductSKU::factory()->create([
  1008. 'order_id' => $order->id,
  1009. 'product_id' => $product->id,
  1010. 'maf_order_id' => null,
  1011. ]);
  1012. $response = $this->actingAs($this->managerUser)
  1013. ->get(route('order.generate-installation-pack', $order));
  1014. $response->assertRedirect(route('order.show', $order));
  1015. $response->assertSessionHas('danger');
  1016. Bus::assertNotDispatched(GenerateInstallationPack::class);
  1017. }
  1018. public function test_admin_can_create_ttn_with_departure_date_and_increment_counter(): void
  1019. {
  1020. Bus::fake();
  1021. Setting::set(Setting::KEY_TTN_NEXT_NUMBER, 10);
  1022. $product = Product::factory()->create();
  1023. $order = Order::factory()->create([
  1024. 'installation_date' => '2026-04-15',
  1025. ]);
  1026. $sku = ProductSKU::factory()->create([
  1027. 'order_id' => $order->id,
  1028. 'product_id' => $product->id,
  1029. ]);
  1030. $response = $this->actingAs($this->adminUser)
  1031. ->post(route('order.create-ttn'), [
  1032. 'order_number' => 'З-100',
  1033. 'order_date' => '2026-04-07',
  1034. 'departure_date' => '2026-04-08',
  1035. 'order_sum' => '150000',
  1036. 'skus' => [$sku->id],
  1037. ]);
  1038. $response->assertRedirect();
  1039. $response->assertSessionHas('success');
  1040. $this->assertDatabaseHas('ttns', [
  1041. 'ttn_number' => 10,
  1042. 'order_number' => 'З-100',
  1043. 'order_date' => '2026-04-07',
  1044. 'departure_date' => '2026-04-08',
  1045. 'order_sum' => '150000',
  1046. ]);
  1047. $this->assertSame(11, Setting::getInt(Setting::KEY_TTN_NEXT_NUMBER));
  1048. $ttn = Ttn::query()->where('ttn_number', 10)->firstOrFail();
  1049. $this->assertSame([(string) $sku->id], array_map('strval', json_decode($ttn->skus, true)));
  1050. Bus::assertDispatched(GenerateTtnPack::class);
  1051. }
  1052. // ==================== Export ====================
  1053. public function test_can_export_orders(): void
  1054. {
  1055. Order::factory()->count(3)->create();
  1056. // This route requires admin role
  1057. $response = $this->actingAs($this->adminUser)
  1058. ->post(route('order.export'));
  1059. $response->assertRedirect(route('order.index'));
  1060. $response->assertSessionHas('success');
  1061. }
  1062. public function test_can_export_single_order(): void
  1063. {
  1064. $order = Order::factory()->create();
  1065. // This route requires admin role
  1066. $response = $this->actingAs($this->adminUser)
  1067. ->post(route('order.export-one', $order));
  1068. $response->assertRedirect(route('order.show', $order));
  1069. $response->assertSessionHas('success');
  1070. }
  1071. }