SparePartReservationControllerTest.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467
  1. <?php
  2. namespace Tests\Feature;
  3. use App\Models\Reclamation;
  4. use App\Models\Reservation;
  5. use App\Models\Role;
  6. use App\Models\Shortage;
  7. use App\Models\SparePart;
  8. use App\Models\SparePartOrder;
  9. use App\Models\User;
  10. use Illuminate\Foundation\Testing\RefreshDatabase;
  11. use Tests\TestCase;
  12. class SparePartReservationControllerTest extends TestCase
  13. {
  14. use RefreshDatabase;
  15. protected $seed = true;
  16. private User $adminUser;
  17. private User $managerUser;
  18. protected function setUp(): void
  19. {
  20. parent::setUp();
  21. $this->adminUser = User::factory()->create(['role' => Role::ADMIN]);
  22. $this->managerUser = User::factory()->create(['role' => Role::MANAGER]);
  23. }
  24. // ==================== forReclamation (JSON API) ====================
  25. public function test_guest_cannot_get_reservations_for_reclamation(): void
  26. {
  27. $reclamation = Reclamation::factory()->create();
  28. $response = $this->get(route('spare_part_reservations.for_reclamation', $reclamation->id));
  29. $response->assertRedirect(route('login'));
  30. }
  31. public function test_admin_can_get_reservations_for_reclamation(): void
  32. {
  33. $sparePart = SparePart::factory()->create(['article' => 'SP-RESRV-001']);
  34. $order = SparePartOrder::factory()->inStock()->withQuantity(10)->forSparePart($sparePart)->create();
  35. $reclamation = Reclamation::factory()->create();
  36. Reservation::factory()->create([
  37. 'spare_part_id' => $sparePart->id,
  38. 'spare_part_order_id' => $order->id,
  39. 'reclamation_id' => $reclamation->id,
  40. 'reserved_qty' => 3,
  41. 'status' => Reservation::STATUS_ACTIVE,
  42. ]);
  43. $response = $this->actingAs($this->adminUser)
  44. ->getJson(route('spare_part_reservations.for_reclamation', $reclamation->id));
  45. $response->assertStatus(200);
  46. $response->assertJsonStructure([
  47. 'reservations' => [['id', 'spare_part_id', 'article', 'reserved_qty', 'status']],
  48. ]);
  49. $response->assertJsonFragment(['spare_part_id' => $sparePart->id]);
  50. }
  51. public function test_manager_can_get_reservations_for_reclamation(): void
  52. {
  53. $reclamation = Reclamation::factory()->create();
  54. $response = $this->actingAs($this->managerUser)
  55. ->getJson(route('spare_part_reservations.for_reclamation', $reclamation->id));
  56. $response->assertStatus(200);
  57. $response->assertJsonStructure(['reservations']);
  58. }
  59. public function test_returns_only_active_reservations_for_reclamation(): void
  60. {
  61. $sparePart = SparePart::factory()->create();
  62. $order = SparePartOrder::factory()->inStock()->withQuantity(20)->forSparePart($sparePart)->create();
  63. $reclamation = Reclamation::factory()->create();
  64. // Активный резерв
  65. Reservation::factory()->create([
  66. 'spare_part_id' => $sparePart->id,
  67. 'spare_part_order_id' => $order->id,
  68. 'reclamation_id' => $reclamation->id,
  69. 'reserved_qty' => 5,
  70. 'status' => Reservation::STATUS_ACTIVE,
  71. ]);
  72. // Отменённый резерв — не должен попасть
  73. Reservation::factory()->create([
  74. 'spare_part_id' => $sparePart->id,
  75. 'spare_part_order_id' => $order->id,
  76. 'reclamation_id' => $reclamation->id,
  77. 'reserved_qty' => 2,
  78. 'status' => Reservation::STATUS_CANCELLED,
  79. ]);
  80. $response = $this->actingAs($this->adminUser)
  81. ->getJson(route('spare_part_reservations.for_reclamation', $reclamation->id));
  82. $response->assertStatus(200);
  83. $this->assertCount(1, $response->json('reservations'));
  84. }
  85. // ==================== shortagesForReclamation (JSON API) ====================
  86. public function test_guest_cannot_get_shortages_for_reclamation(): void
  87. {
  88. $reclamation = Reclamation::factory()->create();
  89. $response = $this->get(route('spare_part_reservations.shortages_for_reclamation', $reclamation->id));
  90. $response->assertRedirect(route('login'));
  91. }
  92. public function test_admin_can_get_shortages_for_reclamation(): void
  93. {
  94. $sparePart = SparePart::factory()->create(['article' => 'SP-SHORTAGE-001']);
  95. $reclamation = Reclamation::factory()->create();
  96. Shortage::factory()->create([
  97. 'spare_part_id' => $sparePart->id,
  98. 'reclamation_id' => $reclamation->id,
  99. 'required_qty' => 10,
  100. 'reserved_qty' => 3,
  101. 'missing_qty' => 7,
  102. 'status' => Shortage::STATUS_OPEN,
  103. ]);
  104. $response = $this->actingAs($this->adminUser)
  105. ->getJson(route('spare_part_reservations.shortages_for_reclamation', $reclamation->id));
  106. $response->assertStatus(200);
  107. $response->assertJsonStructure([
  108. 'shortages' => [['id', 'spare_part_id', 'article', 'required_qty', 'missing_qty', 'status']],
  109. ]);
  110. $response->assertJsonFragment(['spare_part_id' => $sparePart->id]);
  111. }
  112. public function test_manager_can_get_shortages_for_reclamation(): void
  113. {
  114. $reclamation = Reclamation::factory()->create();
  115. $response = $this->actingAs($this->managerUser)
  116. ->getJson(route('spare_part_reservations.shortages_for_reclamation', $reclamation->id));
  117. $response->assertStatus(200);
  118. $response->assertJsonStructure(['shortages']);
  119. }
  120. // ==================== cancel ====================
  121. public function test_guest_cannot_cancel_reservation(): void
  122. {
  123. $reservation = Reservation::factory()->active()->create();
  124. $response = $this->post(route('spare_part_reservations.cancel', $reservation));
  125. $response->assertRedirect(route('login'));
  126. }
  127. public function test_admin_can_cancel_active_reservation(): void
  128. {
  129. $sparePart = SparePart::factory()->create();
  130. $order = SparePartOrder::factory()->inStock()->withQuantity(10)->forSparePart($sparePart)->create();
  131. $reclamation = Reclamation::factory()->create();
  132. $reservation = Reservation::factory()->create([
  133. 'spare_part_id' => $sparePart->id,
  134. 'spare_part_order_id' => $order->id,
  135. 'reclamation_id' => $reclamation->id,
  136. 'reserved_qty' => 5,
  137. 'status' => Reservation::STATUS_ACTIVE,
  138. ]);
  139. $response = $this->actingAs($this->adminUser)
  140. ->post(route('spare_part_reservations.cancel', $reservation), [
  141. 'reason' => 'Тестовая отмена',
  142. ]);
  143. $response->assertRedirect();
  144. $response->assertSessionHas('success');
  145. $this->assertDatabaseHas('reservations', [
  146. 'id' => $reservation->id,
  147. 'status' => Reservation::STATUS_CANCELLED,
  148. ]);
  149. }
  150. public function test_manager_can_cancel_active_reservation(): void
  151. {
  152. $sparePart = SparePart::factory()->create();
  153. $order = SparePartOrder::factory()->inStock()->withQuantity(10)->forSparePart($sparePart)->create();
  154. $reclamation = Reclamation::factory()->create();
  155. $reservation = Reservation::factory()->create([
  156. 'spare_part_id' => $sparePart->id,
  157. 'spare_part_order_id' => $order->id,
  158. 'reclamation_id' => $reclamation->id,
  159. 'reserved_qty' => 3,
  160. 'status' => Reservation::STATUS_ACTIVE,
  161. ]);
  162. $response = $this->actingAs($this->managerUser)
  163. ->post(route('spare_part_reservations.cancel', $reservation));
  164. $response->assertRedirect();
  165. $response->assertSessionHas('success');
  166. }
  167. public function test_cannot_cancel_inactive_reservation(): void
  168. {
  169. $reservation = Reservation::factory()->cancelled()->create();
  170. $response = $this->actingAs($this->adminUser)
  171. ->post(route('spare_part_reservations.cancel', $reservation));
  172. $response->assertRedirect();
  173. $response->assertSessionHas('error');
  174. // Статус не изменился
  175. $this->assertDatabaseHas('reservations', [
  176. 'id' => $reservation->id,
  177. 'status' => Reservation::STATUS_CANCELLED,
  178. ]);
  179. }
  180. public function test_cannot_cancel_issued_reservation(): void
  181. {
  182. $reservation = Reservation::factory()->issued()->create();
  183. $response = $this->actingAs($this->adminUser)
  184. ->post(route('spare_part_reservations.cancel', $reservation));
  185. $response->assertRedirect();
  186. $response->assertSessionHas('error');
  187. $this->assertDatabaseHas('reservations', [
  188. 'id' => $reservation->id,
  189. 'status' => Reservation::STATUS_ISSUED,
  190. ]);
  191. }
  192. // ==================== issue ====================
  193. public function test_guest_cannot_issue_reservation(): void
  194. {
  195. $reservation = Reservation::factory()->active()->create();
  196. $response = $this->post(route('spare_part_reservations.issue', $reservation));
  197. $response->assertRedirect(route('login'));
  198. }
  199. public function test_admin_can_issue_active_reservation(): void
  200. {
  201. $sparePart = SparePart::factory()->create();
  202. $order = SparePartOrder::factory()->inStock()->forSparePart($sparePart)->create([
  203. 'ordered_quantity' => 20,
  204. 'available_qty' => 20,
  205. ]);
  206. $reclamation = Reclamation::factory()->create();
  207. $reservation = Reservation::factory()->create([
  208. 'spare_part_id' => $sparePart->id,
  209. 'spare_part_order_id' => $order->id,
  210. 'reclamation_id' => $reclamation->id,
  211. 'reserved_qty' => 5,
  212. 'status' => Reservation::STATUS_ACTIVE,
  213. ]);
  214. $response = $this->actingAs($this->adminUser)
  215. ->post(route('spare_part_reservations.issue', $reservation), [
  216. 'note' => 'Тестовое списание',
  217. ]);
  218. $response->assertRedirect();
  219. $response->assertSessionHas('success');
  220. $this->assertDatabaseHas('reservations', [
  221. 'id' => $reservation->id,
  222. 'status' => Reservation::STATUS_ISSUED,
  223. ]);
  224. $this->assertDatabaseHas('spare_part_orders', [
  225. 'id' => $order->id,
  226. 'available_qty' => 15,
  227. ]);
  228. }
  229. public function test_manager_can_issue_active_reservation(): void
  230. {
  231. $sparePart = SparePart::factory()->create();
  232. $order = SparePartOrder::factory()->inStock()->forSparePart($sparePart)->create([
  233. 'ordered_quantity' => 10,
  234. 'available_qty' => 10,
  235. ]);
  236. $reclamation = Reclamation::factory()->create();
  237. $reservation = Reservation::factory()->create([
  238. 'spare_part_id' => $sparePart->id,
  239. 'spare_part_order_id' => $order->id,
  240. 'reclamation_id' => $reclamation->id,
  241. 'reserved_qty' => 3,
  242. 'status' => Reservation::STATUS_ACTIVE,
  243. ]);
  244. $response = $this->actingAs($this->managerUser)
  245. ->post(route('spare_part_reservations.issue', $reservation));
  246. $response->assertRedirect();
  247. $response->assertSessionHas('success');
  248. }
  249. public function test_cannot_issue_inactive_reservation(): void
  250. {
  251. $reservation = Reservation::factory()->cancelled()->create();
  252. $response = $this->actingAs($this->adminUser)
  253. ->post(route('spare_part_reservations.issue', $reservation));
  254. $response->assertRedirect();
  255. $response->assertSessionHas('error');
  256. }
  257. // ==================== issueAllForReclamation ====================
  258. public function test_guest_cannot_issue_all_for_reclamation(): void
  259. {
  260. $reclamation = Reclamation::factory()->create();
  261. $response = $this->post(route('spare_part_reservations.issue_all', $reclamation->id));
  262. $response->assertRedirect(route('login'));
  263. }
  264. public function test_admin_can_issue_all_for_reclamation(): void
  265. {
  266. $sparePart = SparePart::factory()->create();
  267. $order = SparePartOrder::factory()->inStock()->forSparePart($sparePart)->create([
  268. 'ordered_quantity' => 50,
  269. 'available_qty' => 50,
  270. ]);
  271. $reclamation = Reclamation::factory()->create();
  272. // Два активных резерва для рекламации
  273. Reservation::factory()->create([
  274. 'spare_part_id' => $sparePart->id,
  275. 'spare_part_order_id' => $order->id,
  276. 'reclamation_id' => $reclamation->id,
  277. 'reserved_qty' => 5,
  278. 'status' => Reservation::STATUS_ACTIVE,
  279. ]);
  280. Reservation::factory()->create([
  281. 'spare_part_id' => $sparePart->id,
  282. 'spare_part_order_id' => $order->id,
  283. 'reclamation_id' => $reclamation->id,
  284. 'reserved_qty' => 3,
  285. 'status' => Reservation::STATUS_ACTIVE,
  286. ]);
  287. $response = $this->actingAs($this->adminUser)
  288. ->post(route('spare_part_reservations.issue_all', $reclamation->id));
  289. $response->assertRedirect();
  290. $response->assertSessionHas('success');
  291. // Все резервы рекламации должны быть списаны
  292. $this->assertDatabaseMissing('reservations', [
  293. 'reclamation_id' => $reclamation->id,
  294. 'status' => Reservation::STATUS_ACTIVE,
  295. ]);
  296. }
  297. public function test_manager_can_issue_all_for_reclamation(): void
  298. {
  299. $reclamation = Reclamation::factory()->create();
  300. $response = $this->actingAs($this->managerUser)
  301. ->post(route('spare_part_reservations.issue_all', $reclamation->id));
  302. $response->assertRedirect();
  303. $response->assertSessionHas('success');
  304. }
  305. // ==================== cancelAllForReclamation ====================
  306. public function test_guest_cannot_cancel_all_for_reclamation(): void
  307. {
  308. $reclamation = Reclamation::factory()->create();
  309. $response = $this->post(route('spare_part_reservations.cancel_all', $reclamation->id));
  310. $response->assertRedirect(route('login'));
  311. }
  312. public function test_admin_can_cancel_all_for_reclamation(): void
  313. {
  314. $sparePart = SparePart::factory()->create();
  315. $order = SparePartOrder::factory()->inStock()->withQuantity(30)->forSparePart($sparePart)->create();
  316. $reclamation = Reclamation::factory()->create();
  317. Reservation::factory()->create([
  318. 'spare_part_id' => $sparePart->id,
  319. 'spare_part_order_id' => $order->id,
  320. 'reclamation_id' => $reclamation->id,
  321. 'reserved_qty' => 4,
  322. 'status' => Reservation::STATUS_ACTIVE,
  323. ]);
  324. Reservation::factory()->create([
  325. 'spare_part_id' => $sparePart->id,
  326. 'spare_part_order_id' => $order->id,
  327. 'reclamation_id' => $reclamation->id,
  328. 'reserved_qty' => 6,
  329. 'status' => Reservation::STATUS_ACTIVE,
  330. ]);
  331. $response = $this->actingAs($this->adminUser)
  332. ->post(route('spare_part_reservations.cancel_all', $reclamation->id), [
  333. 'reason' => 'Тестовая массовая отмена',
  334. ]);
  335. $response->assertRedirect();
  336. $response->assertSessionHas('success');
  337. // Все активные резервы рекламации отменены
  338. $this->assertDatabaseMissing('reservations', [
  339. 'reclamation_id' => $reclamation->id,
  340. 'status' => Reservation::STATUS_ACTIVE,
  341. ]);
  342. }
  343. public function test_manager_can_cancel_all_for_reclamation(): void
  344. {
  345. $reclamation = Reclamation::factory()->create();
  346. $response = $this->actingAs($this->managerUser)
  347. ->post(route('spare_part_reservations.cancel_all', $reclamation->id));
  348. $response->assertRedirect();
  349. $response->assertSessionHas('success');
  350. }
  351. public function test_cancel_all_includes_success_message_with_quantity(): void
  352. {
  353. $sparePart = SparePart::factory()->create();
  354. $order = SparePartOrder::factory()->inStock()->withQuantity(20)->forSparePart($sparePart)->create();
  355. $reclamation = Reclamation::factory()->create();
  356. Reservation::factory()->create([
  357. 'spare_part_id' => $sparePart->id,
  358. 'spare_part_order_id' => $order->id,
  359. 'reclamation_id' => $reclamation->id,
  360. 'reserved_qty' => 7,
  361. 'status' => Reservation::STATUS_ACTIVE,
  362. ]);
  363. $response = $this->actingAs($this->adminUser)
  364. ->post(route('spare_part_reservations.cancel_all', $reclamation->id));
  365. $response->assertSessionHas('success');
  366. $this->assertStringContainsString('7', session('success'));
  367. }
  368. }