SparePartOrderControllerTest.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555
  1. <?php
  2. namespace Tests\Feature;
  3. use App\Models\Reservation;
  4. use App\Models\Role;
  5. use App\Models\SparePart;
  6. use App\Models\SparePartOrder;
  7. use App\Models\User;
  8. use Illuminate\Foundation\Testing\RefreshDatabase;
  9. use Tests\TestCase;
  10. class SparePartOrderControllerTest extends TestCase
  11. {
  12. use RefreshDatabase;
  13. protected $seed = true;
  14. private User $adminUser;
  15. private User $managerUser;
  16. protected function setUp(): void
  17. {
  18. parent::setUp();
  19. $this->adminUser = User::factory()->create(['role' => Role::ADMIN]);
  20. $this->managerUser = User::factory()->create(['role' => Role::MANAGER]);
  21. }
  22. // ==================== Authentication ====================
  23. public function test_guest_cannot_access_spare_part_orders_index(): void
  24. {
  25. $response = $this->get(route('spare_part_orders.index'));
  26. $response->assertRedirect(route('login'));
  27. }
  28. public function test_guest_cannot_access_spare_part_order_create(): void
  29. {
  30. $response = $this->get(route('spare_part_orders.create'));
  31. $response->assertRedirect(route('login'));
  32. }
  33. public function test_guest_cannot_store_spare_part_order(): void
  34. {
  35. $response = $this->post(route('spare_part_orders.store'), []);
  36. $response->assertRedirect(route('login'));
  37. }
  38. // ==================== Index ====================
  39. public function test_admin_can_access_spare_part_orders_index(): void
  40. {
  41. $response = $this->actingAs($this->adminUser)
  42. ->get(route('spare_part_orders.index'));
  43. $response->assertStatus(200);
  44. $response->assertViewIs('spare_parts.index');
  45. }
  46. public function test_manager_can_access_spare_part_orders_index(): void
  47. {
  48. $response = $this->actingAs($this->managerUser)
  49. ->get(route('spare_part_orders.index'));
  50. $response->assertStatus(200);
  51. $response->assertViewIs('spare_parts.index');
  52. }
  53. public function test_index_passes_only_ordered_order_numbers_to_view(): void
  54. {
  55. SparePartOrder::factory()->ordered()->create([
  56. 'order_number' => 'SP-BULK-001',
  57. ]);
  58. SparePartOrder::factory()->ordered()->create([
  59. 'order_number' => 'SP-BULK-001',
  60. ]);
  61. SparePartOrder::factory()->ordered()->create([
  62. 'order_number' => 'SP-BULK-002',
  63. ]);
  64. SparePartOrder::factory()->inStock()->create([
  65. 'order_number' => 'SP-IN-STOCK-001',
  66. ]);
  67. SparePartOrder::factory()->ordered()->create([
  68. 'order_number' => '',
  69. ]);
  70. SparePartOrder::factory()->ordered()->create([
  71. 'order_number' => null,
  72. ]);
  73. $response = $this->actingAs($this->adminUser)
  74. ->get(route('spare_part_orders.index'));
  75. $response->assertStatus(200);
  76. $response->assertViewHas('order_numbers', function ($orderNumbers) {
  77. return $orderNumbers->values()->all() === ['SP-BULK-001', 'SP-BULK-002'];
  78. });
  79. }
  80. // ==================== Show ====================
  81. public function test_admin_can_view_spare_part_order(): void
  82. {
  83. $sparePartOrder = SparePartOrder::factory()->create();
  84. $response = $this->actingAs($this->adminUser)
  85. ->get(route('spare_part_orders.show', $sparePartOrder));
  86. $response->assertStatus(200);
  87. $response->assertViewIs('spare_part_orders.edit');
  88. }
  89. public function test_manager_can_view_spare_part_order(): void
  90. {
  91. $sparePartOrder = SparePartOrder::factory()->create();
  92. $response = $this->actingAs($this->managerUser)
  93. ->get(route('spare_part_orders.show', $sparePartOrder));
  94. $response->assertStatus(200);
  95. $response->assertViewIs('spare_part_orders.edit');
  96. }
  97. public function test_ordered_spare_part_order_page_renders_set_in_stock_button_with_separate_form(): void
  98. {
  99. $sparePartOrder = SparePartOrder::factory()->ordered()->create();
  100. $response = $this->actingAs($this->managerUser)
  101. ->get(route('spare_part_orders.show', $sparePartOrder));
  102. $response->assertStatus(200);
  103. $response->assertSee('form="set-in-stock-form"', false);
  104. $response->assertSee('id="set-in-stock-form"', false);
  105. $response->assertSee(route('spare_part_orders.set_in_stock', $sparePartOrder), false);
  106. }
  107. public function test_guest_cannot_view_spare_part_order(): void
  108. {
  109. $sparePartOrder = SparePartOrder::factory()->create();
  110. $response = $this->get(route('spare_part_orders.show', $sparePartOrder));
  111. $response->assertRedirect(route('login'));
  112. }
  113. // ==================== Create ====================
  114. public function test_admin_can_access_create_form(): void
  115. {
  116. $response = $this->actingAs($this->adminUser)
  117. ->get(route('spare_part_orders.create'));
  118. $response->assertStatus(200);
  119. $response->assertViewIs('spare_part_orders.edit');
  120. }
  121. public function test_manager_can_access_create_form(): void
  122. {
  123. $response = $this->actingAs($this->managerUser)
  124. ->get(route('spare_part_orders.create'));
  125. $response->assertStatus(200);
  126. $response->assertViewIs('spare_part_orders.edit');
  127. }
  128. // ==================== Store ====================
  129. public function test_admin_can_create_spare_part_order(): void
  130. {
  131. $sparePart = SparePart::factory()->create();
  132. $response = $this->actingAs($this->adminUser)
  133. ->post(route('spare_part_orders.store'), [
  134. 'spare_part_id' => $sparePart->id,
  135. 'status' => SparePartOrder::STATUS_ORDERED,
  136. 'ordered_quantity' => 10,
  137. 'source_text' => 'Test Supplier',
  138. ]);
  139. $response->assertRedirect();
  140. $response->assertSessionHas('success');
  141. $this->assertDatabaseHas('spare_part_orders', [
  142. 'spare_part_id' => $sparePart->id,
  143. 'status' => SparePartOrder::STATUS_ORDERED,
  144. 'ordered_quantity' => 10,
  145. ]);
  146. }
  147. public function test_manager_can_create_spare_part_order(): void
  148. {
  149. $sparePart = SparePart::factory()->create();
  150. $response = $this->actingAs($this->managerUser)
  151. ->post(route('spare_part_orders.store'), [
  152. 'spare_part_id' => $sparePart->id,
  153. 'status' => SparePartOrder::STATUS_ORDERED,
  154. 'ordered_quantity' => 5,
  155. ]);
  156. $response->assertRedirect();
  157. $response->assertSessionHas('success');
  158. }
  159. public function test_store_validates_required_spare_part_id(): void
  160. {
  161. $response = $this->actingAs($this->adminUser)
  162. ->post(route('spare_part_orders.store'), [
  163. 'status' => SparePartOrder::STATUS_ORDERED,
  164. 'ordered_quantity' => 10,
  165. ]);
  166. $response->assertSessionHasErrors('spare_part_id');
  167. }
  168. public function test_store_validates_required_ordered_quantity(): void
  169. {
  170. $sparePart = SparePart::factory()->create();
  171. $response = $this->actingAs($this->adminUser)
  172. ->post(route('spare_part_orders.store'), [
  173. 'spare_part_id' => $sparePart->id,
  174. 'status' => SparePartOrder::STATUS_ORDERED,
  175. ]);
  176. $response->assertSessionHasErrors('ordered_quantity');
  177. }
  178. // ==================== Update ====================
  179. public function test_admin_can_update_spare_part_order(): void
  180. {
  181. $sparePartOrder = SparePartOrder::factory()->ordered()->create();
  182. $response = $this->actingAs($this->adminUser)
  183. ->put(route('spare_part_orders.update', $sparePartOrder), [
  184. 'spare_part_id' => $sparePartOrder->spare_part_id,
  185. 'status' => SparePartOrder::STATUS_ORDERED,
  186. 'ordered_quantity' => 20,
  187. 'order_number' => 'ORD-2024-001',
  188. ]);
  189. $response->assertRedirect();
  190. $response->assertSessionHas('success');
  191. $this->assertDatabaseHas('spare_part_orders', [
  192. 'id' => $sparePartOrder->id,
  193. 'ordered_quantity' => 20,
  194. 'order_number' => 'ORD-2024-001',
  195. ]);
  196. }
  197. public function test_manager_can_update_spare_part_order(): void
  198. {
  199. $sparePartOrder = SparePartOrder::factory()->ordered()->create();
  200. $response = $this->actingAs($this->managerUser)
  201. ->put(route('spare_part_orders.update', $sparePartOrder), [
  202. 'spare_part_id' => $sparePartOrder->spare_part_id,
  203. 'status' => SparePartOrder::STATUS_ORDERED,
  204. 'ordered_quantity' => 15,
  205. ]);
  206. $response->assertRedirect();
  207. $response->assertSessionHas('success');
  208. }
  209. public function test_guest_cannot_update_spare_part_order(): void
  210. {
  211. $sparePartOrder = SparePartOrder::factory()->create();
  212. $response = $this->put(route('spare_part_orders.update', $sparePartOrder), []);
  213. $response->assertRedirect(route('login'));
  214. }
  215. // ==================== Destroy ====================
  216. public function test_admin_can_delete_spare_part_order_without_active_reservations(): void
  217. {
  218. $sparePartOrder = SparePartOrder::factory()->create();
  219. $response = $this->actingAs($this->adminUser)
  220. ->delete(route('spare_part_orders.destroy', $sparePartOrder));
  221. $response->assertRedirect(route('spare_part_orders.index'));
  222. $response->assertSessionHas('success');
  223. $this->assertSoftDeleted('spare_part_orders', ['id' => $sparePartOrder->id]);
  224. }
  225. public function test_admin_cannot_delete_spare_part_order_with_active_reservations(): void
  226. {
  227. $sparePartOrder = SparePartOrder::factory()->inStock()->create();
  228. Reservation::factory()->create([
  229. 'spare_part_order_id' => $sparePartOrder->id,
  230. 'spare_part_id' => $sparePartOrder->spare_part_id,
  231. 'status' => Reservation::STATUS_ACTIVE,
  232. ]);
  233. $response = $this->actingAs($this->adminUser)
  234. ->delete(route('spare_part_orders.destroy', $sparePartOrder));
  235. $response->assertRedirect(route('spare_part_orders.show', $sparePartOrder));
  236. $response->assertSessionHas('error');
  237. $this->assertDatabaseHas('spare_part_orders', ['id' => $sparePartOrder->id, 'deleted_at' => null]);
  238. }
  239. public function test_manager_cannot_delete_spare_part_order(): void
  240. {
  241. $sparePartOrder = SparePartOrder::factory()->create();
  242. $response = $this->actingAs($this->managerUser)
  243. ->delete(route('spare_part_orders.destroy', $sparePartOrder));
  244. $response->assertStatus(403);
  245. }
  246. public function test_guest_cannot_delete_spare_part_order(): void
  247. {
  248. $sparePartOrder = SparePartOrder::factory()->create();
  249. $response = $this->delete(route('spare_part_orders.destroy', $sparePartOrder));
  250. $response->assertRedirect(route('login'));
  251. }
  252. // ==================== SetInStock ====================
  253. public function test_admin_can_set_order_in_stock(): void
  254. {
  255. $sparePartOrder = SparePartOrder::factory()->ordered()->create();
  256. $response = $this->actingAs($this->adminUser)
  257. ->post(route('spare_part_orders.set_in_stock', $sparePartOrder));
  258. $response->assertRedirect(route('spare_part_orders.show', $sparePartOrder));
  259. $response->assertSessionHas('success');
  260. $this->assertDatabaseHas('spare_part_orders', [
  261. 'id' => $sparePartOrder->id,
  262. 'status' => SparePartOrder::STATUS_IN_STOCK,
  263. ]);
  264. }
  265. public function test_manager_can_set_order_in_stock(): void
  266. {
  267. $sparePartOrder = SparePartOrder::factory()->ordered()->create();
  268. $response = $this->actingAs($this->managerUser)
  269. ->post(route('spare_part_orders.set_in_stock', $sparePartOrder));
  270. $response->assertRedirect(route('spare_part_orders.show', $sparePartOrder));
  271. $response->assertSessionHas('success');
  272. }
  273. public function test_guest_cannot_set_order_in_stock(): void
  274. {
  275. $sparePartOrder = SparePartOrder::factory()->create();
  276. $response = $this->post(route('spare_part_orders.set_in_stock', $sparePartOrder));
  277. $response->assertRedirect(route('login'));
  278. }
  279. public function test_admin_can_set_whole_order_in_stock_by_order_number(): void
  280. {
  281. $first = SparePartOrder::factory()->ordered()->create([
  282. 'order_number' => 'SP-BULK-100',
  283. ]);
  284. $second = SparePartOrder::factory()->ordered()->create([
  285. 'order_number' => 'SP-BULK-100',
  286. ]);
  287. $sameNumberInStock = SparePartOrder::factory()->inStock()->create([
  288. 'order_number' => 'SP-BULK-100',
  289. ]);
  290. $other = SparePartOrder::factory()->ordered()->create([
  291. 'order_number' => 'SP-BULK-200',
  292. ]);
  293. $response = $this->actingAs($this->adminUser)
  294. ->post(route('spare_part_orders.set_order_in_stock'), [
  295. 'bulk_order_number' => 'SP-BULK-100',
  296. ]);
  297. $response->assertRedirect(route('spare_part_orders.index'));
  298. $response->assertSessionHas('success');
  299. $this->assertDatabaseHas('spare_part_orders', [
  300. 'id' => $first->id,
  301. 'status' => SparePartOrder::STATUS_IN_STOCK,
  302. ]);
  303. $this->assertDatabaseHas('spare_part_orders', [
  304. 'id' => $second->id,
  305. 'status' => SparePartOrder::STATUS_IN_STOCK,
  306. ]);
  307. $this->assertDatabaseHas('spare_part_orders', [
  308. 'id' => $sameNumberInStock->id,
  309. 'status' => SparePartOrder::STATUS_IN_STOCK,
  310. ]);
  311. $this->assertDatabaseHas('spare_part_orders', [
  312. 'id' => $other->id,
  313. 'status' => SparePartOrder::STATUS_ORDERED,
  314. ]);
  315. }
  316. public function test_bulk_set_order_in_stock_validates_selected_number_has_ordered_rows(): void
  317. {
  318. SparePartOrder::factory()->inStock()->create([
  319. 'order_number' => 'SP-NOT-ORDERED',
  320. ]);
  321. $response = $this->from(route('spare_part_orders.index'))
  322. ->actingAs($this->adminUser)
  323. ->post(route('spare_part_orders.set_order_in_stock'), [
  324. 'bulk_order_number' => 'SP-NOT-ORDERED',
  325. ]);
  326. $response->assertRedirect(route('spare_part_orders.index'));
  327. $response->assertSessionHasErrors('bulk_order_number');
  328. }
  329. // ==================== Ship ====================
  330. public function test_admin_can_ship_spare_part_order(): void
  331. {
  332. $sparePartOrder = SparePartOrder::factory()->inStock()->withQuantity(20)->create();
  333. $response = $this->actingAs($this->adminUser)
  334. ->post(route('spare_part_orders.ship', $sparePartOrder), [
  335. 'quantity' => 5,
  336. 'note' => 'Тестовая отгрузка',
  337. ]);
  338. $response->assertRedirect(route('spare_part_orders.show', $sparePartOrder));
  339. $response->assertSessionHas('success');
  340. $this->assertDatabaseHas('spare_part_orders', [
  341. 'id' => $sparePartOrder->id,
  342. 'available_qty' => 15,
  343. ]);
  344. }
  345. public function test_ship_fails_if_quantity_exceeds_available(): void
  346. {
  347. $sparePartOrder = SparePartOrder::factory()->inStock()->withQuantity(3)->create();
  348. $response = $this->actingAs($this->adminUser)
  349. ->post(route('spare_part_orders.ship', $sparePartOrder), [
  350. 'quantity' => 10,
  351. 'note' => 'Попытка отгрузить больше чем есть',
  352. ]);
  353. $response->assertRedirect(route('spare_part_orders.show', $sparePartOrder));
  354. $response->assertSessionHas('error');
  355. $this->assertDatabaseHas('spare_part_orders', [
  356. 'id' => $sparePartOrder->id,
  357. 'available_qty' => 3,
  358. ]);
  359. }
  360. public function test_ship_validates_required_quantity(): void
  361. {
  362. $sparePartOrder = SparePartOrder::factory()->inStock()->create();
  363. $response = $this->actingAs($this->adminUser)
  364. ->post(route('spare_part_orders.ship', $sparePartOrder), [
  365. 'note' => 'Без количества',
  366. ]);
  367. $response->assertSessionHasErrors('quantity');
  368. }
  369. public function test_manager_can_ship_spare_part_order(): void
  370. {
  371. $sparePartOrder = SparePartOrder::factory()->inStock()->withQuantity(10)->create();
  372. $response = $this->actingAs($this->managerUser)
  373. ->post(route('spare_part_orders.ship', $sparePartOrder), [
  374. 'quantity' => 2,
  375. 'note' => 'Отгрузка менеджером',
  376. ]);
  377. $response->assertRedirect(route('spare_part_orders.show', $sparePartOrder));
  378. $response->assertSessionHas('success');
  379. }
  380. public function test_guest_cannot_ship_spare_part_order(): void
  381. {
  382. $sparePartOrder = SparePartOrder::factory()->create();
  383. $response = $this->post(route('spare_part_orders.ship', $sparePartOrder), [
  384. 'quantity' => 1,
  385. 'note' => 'Test',
  386. ]);
  387. $response->assertRedirect(route('login'));
  388. }
  389. // ==================== Correct (Inventory) ====================
  390. public function test_admin_can_correct_inventory(): void
  391. {
  392. $sparePartOrder = SparePartOrder::factory()->inStock()->withQuantity(10)->create();
  393. $response = $this->actingAs($this->adminUser)
  394. ->post(route('spare_part_orders.correct', $sparePartOrder), [
  395. 'new_quantity' => 8,
  396. 'reason' => 'Инвентаризация — обнаружена недостача',
  397. ]);
  398. $response->assertRedirect(route('spare_part_orders.show', $sparePartOrder));
  399. $response->assertSessionHas('success');
  400. $this->assertDatabaseHas('spare_part_orders', [
  401. 'id' => $sparePartOrder->id,
  402. 'available_qty' => 8,
  403. ]);
  404. }
  405. public function test_correct_fails_if_quantity_unchanged(): void
  406. {
  407. $sparePartOrder = SparePartOrder::factory()->inStock()->withQuantity(10)->create();
  408. $response = $this->actingAs($this->adminUser)
  409. ->post(route('spare_part_orders.correct', $sparePartOrder), [
  410. 'new_quantity' => 10,
  411. 'reason' => 'Без изменений',
  412. ]);
  413. $response->assertRedirect(route('spare_part_orders.show', $sparePartOrder));
  414. $response->assertSessionHas('error');
  415. }
  416. public function test_manager_cannot_correct_inventory(): void
  417. {
  418. $sparePartOrder = SparePartOrder::factory()->inStock()->create();
  419. $response = $this->actingAs($this->managerUser)
  420. ->post(route('spare_part_orders.correct', $sparePartOrder), [
  421. 'new_quantity' => 5,
  422. 'reason' => 'Test',
  423. ]);
  424. $response->assertStatus(403);
  425. }
  426. public function test_guest_cannot_correct_inventory(): void
  427. {
  428. $sparePartOrder = SparePartOrder::factory()->create();
  429. $response = $this->post(route('spare_part_orders.correct', $sparePartOrder), [
  430. 'new_quantity' => 5,
  431. 'reason' => 'Test',
  432. ]);
  433. $response->assertRedirect(route('login'));
  434. }
  435. }