ProductSKUControllerTest.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. <?php
  2. namespace Tests\Feature;
  3. use App\Jobs\ExportMafJob;
  4. use App\Models\Order;
  5. use App\Models\Permission;
  6. use App\Models\Product;
  7. use App\Models\ProductSKU;
  8. use App\Models\Role;
  9. use App\Models\User;
  10. use Database\Seeders\RbacSeeder;
  11. use Illuminate\Foundation\Testing\RefreshDatabase;
  12. use Illuminate\Http\UploadedFile;
  13. use Illuminate\Support\Facades\Bus;
  14. use Illuminate\Support\Facades\Storage;
  15. use Tests\TestCase;
  16. class ProductSKUControllerTest extends TestCase
  17. {
  18. use RefreshDatabase;
  19. protected $seed = true;
  20. private User $adminUser;
  21. private User $managerUser;
  22. private User $brigadierUser;
  23. private User $assistantHeadUser;
  24. protected function setUp(): void
  25. {
  26. parent::setUp();
  27. $this->seed(RbacSeeder::class);
  28. $this->adminUser = $this->createUserWithRole(Role::ADMIN);
  29. $this->managerUser = $this->createUserWithRole(Role::MANAGER);
  30. $this->brigadierUser = $this->createUserWithRole(Role::BRIGADIER);
  31. $this->assistantHeadUser = $this->createUserWithRole(Role::ASSISTANT_HEAD);
  32. }
  33. private function createUserWithRole(string $roleSlug): User
  34. {
  35. return User::factory()->create([
  36. 'role' => $roleSlug,
  37. 'role_id' => Role::query()->where('slug', $roleSlug)->value('id'),
  38. ]);
  39. }
  40. // ==================== Guest redirects ====================
  41. public function test_guest_cannot_access_product_sku_index(): void
  42. {
  43. $response = $this->get(route('product_sku.index'));
  44. $response->assertRedirect(route('login'));
  45. }
  46. public function test_guest_cannot_access_product_sku_show(): void
  47. {
  48. $sku = ProductSKU::factory()->create();
  49. $response = $this->get(route('product_sku.show', $sku));
  50. $response->assertRedirect(route('login'));
  51. }
  52. public function test_guest_cannot_update_product_sku(): void
  53. {
  54. $sku = ProductSKU::factory()->create();
  55. $response = $this->post(route('product_sku.update', $sku), []);
  56. $response->assertRedirect(route('login'));
  57. }
  58. // ==================== Authorization ====================
  59. public function test_brigadier_cannot_access_product_sku_index(): void
  60. {
  61. $response = $this->actingAs($this->brigadierUser)
  62. ->get(route('product_sku.index'));
  63. $response->assertStatus(403);
  64. }
  65. public function test_manager_cannot_export_mafs(): void
  66. {
  67. $response = $this->actingAs($this->managerUser)
  68. ->post(route('mafs.export'));
  69. $response->assertStatus(403);
  70. }
  71. // ==================== Index ====================
  72. public function test_admin_can_access_product_sku_index(): void
  73. {
  74. $response = $this->actingAs($this->adminUser)
  75. ->get(route('product_sku.index'));
  76. $response->assertStatus(200);
  77. $response->assertViewIs('products_sku.index');
  78. }
  79. public function test_manager_can_access_product_sku_index(): void
  80. {
  81. $response = $this->actingAs($this->managerUser)
  82. ->get(route('product_sku.index'));
  83. $response->assertStatus(200);
  84. $response->assertViewIs('products_sku.index');
  85. }
  86. public function test_maf_filter_options_include_empty_marker_for_factory_number(): void
  87. {
  88. ProductSKU::factory()->create(['factory_number' => null]);
  89. ProductSKU::factory()->create(['factory_number' => ' ']);
  90. ProductSKU::factory()->create(['factory_number' => 'FN-123456']);
  91. $response = $this->actingAs($this->adminUser)
  92. ->getJson(route('getFilters', [
  93. 'table' => 'product_sku',
  94. 'column' => 'factory_number',
  95. ]));
  96. $response->assertOk();
  97. $response->assertJsonFragment(['-пусто-']);
  98. $response->assertJsonFragment(['FN-123456']);
  99. }
  100. public function test_maf_empty_filter_matches_null_blank_and_null_date_values(): void
  101. {
  102. $orderWithNullFactory = Order::factory()->create(['object_address' => 'ул. Пустая фабрика']);
  103. $orderWithBlankFactory = Order::factory()->create(['object_address' => 'ул. Пробелы фабрика']);
  104. $orderWithNullDate = Order::factory()->create(['object_address' => 'ул. Пустая дата']);
  105. $orderWithBlankStatement = Order::factory()->create(['object_address' => 'ул. Пустая ведомость']);
  106. $orderWithFilledValue = Order::factory()->create(['object_address' => 'ул. Заполненная']);
  107. $product = Product::factory()->create();
  108. ProductSKU::factory()->create([
  109. 'order_id' => $orderWithNullFactory->id,
  110. 'product_id' => $product->id,
  111. 'factory_number' => null,
  112. 'manufacture_date' => '2026-04-01',
  113. 'statement_number' => 'STAT-1',
  114. ]);
  115. ProductSKU::factory()->create([
  116. 'order_id' => $orderWithBlankFactory->id,
  117. 'product_id' => $product->id,
  118. 'factory_number' => ' ',
  119. 'manufacture_date' => '2026-04-02',
  120. 'statement_number' => 'STAT-2',
  121. ]);
  122. ProductSKU::factory()->create([
  123. 'order_id' => $orderWithNullDate->id,
  124. 'product_id' => $product->id,
  125. 'factory_number' => 'FN-000001',
  126. 'manufacture_date' => null,
  127. 'statement_number' => 'STAT-3',
  128. ]);
  129. ProductSKU::factory()->create([
  130. 'order_id' => $orderWithBlankStatement->id,
  131. 'product_id' => $product->id,
  132. 'factory_number' => 'FN-000002',
  133. 'manufacture_date' => '2026-04-03',
  134. 'statement_number' => ' ',
  135. ]);
  136. ProductSKU::factory()->create([
  137. 'order_id' => $orderWithFilledValue->id,
  138. 'product_id' => $product->id,
  139. 'factory_number' => 'FN-999999',
  140. 'manufacture_date' => '2026-04-04',
  141. 'statement_number' => 'STAT-9',
  142. ]);
  143. $factoryResponse = $this->actingAs($this->adminUser)
  144. ->get(route('product_sku.index', [
  145. 'filters' => ['factory_number' => '-пусто-'],
  146. ]));
  147. $factoryResponse->assertOk();
  148. $factoryAddresses = $factoryResponse->viewData('products_sku')->pluck('object_address')->all();
  149. $this->assertContains('ул. Пустая фабрика', $factoryAddresses);
  150. $this->assertContains('ул. Пробелы фабрика', $factoryAddresses);
  151. $this->assertNotContains('ул. Заполненная', $factoryAddresses);
  152. $dateResponse = $this->actingAs($this->adminUser)
  153. ->get(route('product_sku.index', [
  154. 'filters' => ['manufacture_date' => '-пусто-'],
  155. ]));
  156. $dateResponse->assertOk();
  157. $dateAddresses = $dateResponse->viewData('products_sku')->pluck('object_address')->all();
  158. $this->assertContains('ул. Пустая дата', $dateAddresses);
  159. $this->assertNotContains('ул. Заполненная', $dateAddresses);
  160. $statementResponse = $this->actingAs($this->adminUser)
  161. ->get(route('product_sku.index', [
  162. 'filters' => ['statement_number' => '-пусто-'],
  163. ]));
  164. $statementResponse->assertOk();
  165. $statementAddresses = $statementResponse->viewData('products_sku')->pluck('object_address')->all();
  166. $this->assertContains('ул. Пустая ведомость', $statementAddresses);
  167. $this->assertNotContains('ул. Заполненная', $statementAddresses);
  168. }
  169. // ==================== Show ====================
  170. public function test_admin_can_view_product_sku(): void
  171. {
  172. $sku = ProductSKU::factory()->create();
  173. $response = $this->actingAs($this->adminUser)
  174. ->get(route('product_sku.show', $sku));
  175. $response->assertStatus(200);
  176. $response->assertViewIs('products_sku.edit');
  177. }
  178. public function test_manager_can_view_product_sku(): void
  179. {
  180. $sku = ProductSKU::factory()->create();
  181. $response = $this->actingAs($this->managerUser)
  182. ->get(route('product_sku.show', $sku));
  183. $response->assertStatus(200);
  184. $response->assertViewIs('products_sku.edit');
  185. }
  186. public function test_product_sku_show_uses_nav_context_back_url(): void
  187. {
  188. $sku = ProductSKU::factory()->create();
  189. $reclamation = \App\Models\Reclamation::factory()->create();
  190. $indexResponse = $this->actingAs($this->adminUser)
  191. ->get(route('reclamations.index'));
  192. $nav = $indexResponse->viewData('nav');
  193. $this->actingAs($this->adminUser)
  194. ->get(route('reclamations.show', [
  195. 'reclamation' => $reclamation,
  196. 'nav' => $nav,
  197. ]))
  198. ->assertOk();
  199. $response = $this->actingAs($this->adminUser)
  200. ->get(route('product_sku.show', [
  201. 'product_sku' => $sku,
  202. 'nav' => $nav,
  203. ]));
  204. $response->assertOk();
  205. $response->assertViewHas('nav', $nav);
  206. $response->assertViewHas('back_url', route('reclamations.show', [
  207. 'reclamation' => $reclamation,
  208. 'nav' => $nav,
  209. ]));
  210. }
  211. // ==================== Update ====================
  212. public function test_admin_can_update_product_sku(): void
  213. {
  214. $product = Product::factory()->create();
  215. $order = Order::factory()->create();
  216. $sku = ProductSKU::factory()->create([
  217. 'product_id' => $product->id,
  218. 'order_id' => $order->id,
  219. ]);
  220. $response = $this->actingAs($this->adminUser)
  221. ->post(route('product_sku.update', $sku), [
  222. 'product_id' => $product->id,
  223. 'order_id' => $order->id,
  224. 'status' => 'shipped',
  225. 'factory_number' => 'FN-999999',
  226. 'comment' => 'Updated by admin',
  227. ]);
  228. $response->assertRedirect();
  229. $this->assertDatabaseHas('products_sku', [
  230. 'id' => $sku->id,
  231. 'factory_number' => 'FN-999999',
  232. 'comment' => 'Updated by admin',
  233. ]);
  234. }
  235. public function test_manager_can_update_product_sku(): void
  236. {
  237. $product = Product::factory()->create();
  238. $order = Order::factory()->create();
  239. $sku = ProductSKU::factory()->create([
  240. 'product_id' => $product->id,
  241. 'order_id' => $order->id,
  242. ]);
  243. $response = $this->actingAs($this->managerUser)
  244. ->post(route('product_sku.update', $sku), [
  245. 'product_id' => $product->id,
  246. 'order_id' => $order->id,
  247. 'status' => 'needs',
  248. 'factory_number' => 'FN-777777',
  249. ]);
  250. $response->assertRedirect();
  251. }
  252. public function test_update_product_sku_redirects_to_parent_url_from_nav_context(): void
  253. {
  254. $product = Product::factory()->create();
  255. $order = Order::factory()->create();
  256. $sku = ProductSKU::factory()->create([
  257. 'product_id' => $product->id,
  258. 'order_id' => $order->id,
  259. ]);
  260. $reclamation = \App\Models\Reclamation::factory()->create();
  261. $nav = 'product-sku-nav-token';
  262. $response = $this->actingAs($this->adminUser)
  263. ->withSession([
  264. 'navigation' => [
  265. $nav => [
  266. 'updated_at' => time(),
  267. 'stack' => [
  268. route('reclamations.index'),
  269. route('reclamations.show', $reclamation),
  270. route('product_sku.show', $sku),
  271. ],
  272. ],
  273. ],
  274. ])
  275. ->post(route('product_sku.update', $sku), [
  276. 'nav' => $nav,
  277. 'product_id' => $product->id,
  278. 'order_id' => $order->id,
  279. 'status' => 'shipped',
  280. 'factory_number' => 'FN-NAV-123',
  281. 'comment' => 'Updated through nav',
  282. ]);
  283. $response->assertRedirect(route('reclamations.show', [
  284. 'reclamation' => $reclamation,
  285. 'nav' => 'product-sku-nav-token',
  286. ]));
  287. }
  288. public function test_admin_can_inline_update_product_sku_field(): void
  289. {
  290. $sku = ProductSKU::factory()->create(['rfid' => 'OLD-RFID']);
  291. $response = $this->actingAs($this->adminUser)
  292. ->postJson(route('product_sku.inline-update', $sku), [
  293. 'field' => 'rfid',
  294. 'value' => 'NEW-RFID',
  295. ]);
  296. $response->assertOk()
  297. ->assertJson([
  298. 'field' => 'rfid',
  299. 'value' => 'NEW-RFID',
  300. ]);
  301. $this->assertDatabaseHas('products_sku', [
  302. 'id' => $sku->id,
  303. 'rfid' => 'NEW-RFID',
  304. ]);
  305. }
  306. public function test_assistant_head_can_inline_update_product_sku_date_field(): void
  307. {
  308. $sku = ProductSKU::factory()->create(['manufacture_date' => null]);
  309. $response = $this->actingAs($this->assistantHeadUser)
  310. ->postJson(route('product_sku.inline-update', $sku), [
  311. 'field' => 'manufacture_date',
  312. 'value' => '2026-05-19',
  313. ]);
  314. $response->assertOk()
  315. ->assertJson([
  316. 'field' => 'manufacture_date',
  317. 'value' => '2026-05-19',
  318. ]);
  319. $this->assertDatabaseHas('products_sku', [
  320. 'id' => $sku->id,
  321. 'manufacture_date' => '2026-05-19',
  322. ]);
  323. }
  324. public function test_user_with_maf_field_update_permission_can_inline_update_product_sku_field(): void
  325. {
  326. $role = Role::query()->create([
  327. 'slug' => 'maf_inline_editor',
  328. 'name' => 'Редактор МАФ в таблице',
  329. 'is_system' => false,
  330. 'is_active' => true,
  331. 'sort' => 100,
  332. ]);
  333. $permission = Permission::query()->where('slug', 'maf.fields.statement_number.update')->firstOrFail();
  334. $role->permissions()->sync([
  335. $permission->id => ['effect' => 'allow'],
  336. ]);
  337. $user = User::factory()->create([
  338. 'role' => $role->slug,
  339. 'role_id' => $role->id,
  340. ]);
  341. $sku = ProductSKU::factory()->create(['statement_number' => 'OLD-STAT']);
  342. $response = $this->actingAs($user)
  343. ->postJson(route('product_sku.inline-update', $sku), [
  344. 'field' => 'statement_number',
  345. 'value' => 'NEW-STAT',
  346. ]);
  347. $response->assertOk()
  348. ->assertJson([
  349. 'field' => 'statement_number',
  350. 'value' => 'NEW-STAT',
  351. ]);
  352. $this->assertDatabaseHas('products_sku', [
  353. 'id' => $sku->id,
  354. 'statement_number' => 'NEW-STAT',
  355. ]);
  356. }
  357. public function test_manager_cannot_inline_update_product_sku_field(): void
  358. {
  359. $sku = ProductSKU::factory()->create(['upd_number' => 'UPD-OLD']);
  360. $response = $this->actingAs($this->managerUser)
  361. ->postJson(route('product_sku.inline-update', $sku), [
  362. 'field' => 'upd_number',
  363. 'value' => 'UPD-NEW',
  364. ]);
  365. $response->assertForbidden();
  366. $this->assertDatabaseHas('products_sku', [
  367. 'id' => $sku->id,
  368. 'upd_number' => 'UPD-OLD',
  369. ]);
  370. }
  371. public function test_inline_update_rejects_unapproved_product_sku_field(): void
  372. {
  373. $sku = ProductSKU::factory()->create(['comment' => 'Old comment']);
  374. $response = $this->actingAs($this->adminUser)
  375. ->postJson(route('product_sku.inline-update', $sku), [
  376. 'field' => 'comment',
  377. 'value' => 'New comment',
  378. ]);
  379. $response->assertUnprocessable();
  380. $this->assertDatabaseHas('products_sku', [
  381. 'id' => $sku->id,
  382. 'comment' => 'Old comment',
  383. ]);
  384. }
  385. public function test_user_with_passport_upload_permission_can_upload_product_sku_passport(): void
  386. {
  387. Storage::fake('public');
  388. $role = Role::query()->create([
  389. 'slug' => 'maf_passport_loader',
  390. 'name' => 'Загрузка паспортов МАФ',
  391. 'is_system' => false,
  392. 'is_active' => true,
  393. 'sort' => 100,
  394. ]);
  395. $permission = Permission::query()->where('slug', 'maf.passports.upload')->firstOrFail();
  396. $role->permissions()->sync([
  397. $permission->id => ['effect' => 'allow'],
  398. ]);
  399. $user = User::factory()->create([
  400. 'role' => $role->slug,
  401. 'role_id' => $role->id,
  402. ]);
  403. $sku = ProductSKU::factory()->create(['passport_id' => null]);
  404. $response = $this->actingAs($user)
  405. ->from(route('order.show', $sku->order))
  406. ->post(route('product-sku.upload-passport', $sku), [
  407. 'passport' => UploadedFile::fake()->create('passport.pdf', 10, 'application/pdf'),
  408. ]);
  409. $response->assertRedirect(route('order.show', $sku->order));
  410. $this->assertNotNull($sku->fresh()->passport_id);
  411. }
  412. public function test_user_without_passport_upload_permission_cannot_upload_product_sku_passport(): void
  413. {
  414. Storage::fake('public');
  415. $sku = ProductSKU::factory()->create(['passport_id' => null]);
  416. $response = $this->actingAs($this->brigadierUser)
  417. ->post(route('product-sku.upload-passport', $sku), [
  418. 'passport' => UploadedFile::fake()->create('passport.pdf', 10, 'application/pdf'),
  419. ]);
  420. $response->assertForbidden();
  421. $this->assertNull($sku->fresh()->passport_id);
  422. }
  423. // ==================== Export ====================
  424. public function test_admin_can_export_mafs(): void
  425. {
  426. Bus::fake();
  427. $response = $this->actingAs($this->adminUser)
  428. ->post(route('mafs.export'));
  429. $response->assertRedirect(route('product_sku.index'));
  430. $response->assertSessionHas('success');
  431. Bus::assertDispatched(ExportMafJob::class);
  432. }
  433. }