|
|
@@ -0,0 +1,467 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace Tests\Feature;
|
|
|
+
|
|
|
+use App\Models\Reclamation;
|
|
|
+use App\Models\Reservation;
|
|
|
+use App\Models\Role;
|
|
|
+use App\Models\Shortage;
|
|
|
+use App\Models\SparePart;
|
|
|
+use App\Models\SparePartOrder;
|
|
|
+use App\Models\User;
|
|
|
+use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
|
+use Tests\TestCase;
|
|
|
+
|
|
|
+class SparePartReservationControllerTest extends TestCase
|
|
|
+{
|
|
|
+ use RefreshDatabase;
|
|
|
+
|
|
|
+ protected $seed = true;
|
|
|
+
|
|
|
+ private User $adminUser;
|
|
|
+ private User $managerUser;
|
|
|
+
|
|
|
+ protected function setUp(): void
|
|
|
+ {
|
|
|
+ parent::setUp();
|
|
|
+
|
|
|
+ $this->adminUser = User::factory()->create(['role' => Role::ADMIN]);
|
|
|
+ $this->managerUser = User::factory()->create(['role' => Role::MANAGER]);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== forReclamation (JSON API) ====================
|
|
|
+
|
|
|
+ public function test_guest_cannot_get_reservations_for_reclamation(): void
|
|
|
+ {
|
|
|
+ $reclamation = Reclamation::factory()->create();
|
|
|
+
|
|
|
+ $response = $this->get(route('spare_part_reservations.for_reclamation', $reclamation->id));
|
|
|
+
|
|
|
+ $response->assertRedirect(route('login'));
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_admin_can_get_reservations_for_reclamation(): void
|
|
|
+ {
|
|
|
+ $sparePart = SparePart::factory()->create(['article' => 'SP-RESRV-001']);
|
|
|
+ $order = SparePartOrder::factory()->inStock()->withQuantity(10)->forSparePart($sparePart)->create();
|
|
|
+ $reclamation = Reclamation::factory()->create();
|
|
|
+
|
|
|
+ Reservation::factory()->create([
|
|
|
+ 'spare_part_id' => $sparePart->id,
|
|
|
+ 'spare_part_order_id' => $order->id,
|
|
|
+ 'reclamation_id' => $reclamation->id,
|
|
|
+ 'reserved_qty' => 3,
|
|
|
+ 'status' => Reservation::STATUS_ACTIVE,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $response = $this->actingAs($this->adminUser)
|
|
|
+ ->getJson(route('spare_part_reservations.for_reclamation', $reclamation->id));
|
|
|
+
|
|
|
+ $response->assertStatus(200);
|
|
|
+ $response->assertJsonStructure([
|
|
|
+ 'reservations' => [['id', 'spare_part_id', 'article', 'reserved_qty', 'status']],
|
|
|
+ ]);
|
|
|
+ $response->assertJsonFragment(['spare_part_id' => $sparePart->id]);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_manager_can_get_reservations_for_reclamation(): void
|
|
|
+ {
|
|
|
+ $reclamation = Reclamation::factory()->create();
|
|
|
+
|
|
|
+ $response = $this->actingAs($this->managerUser)
|
|
|
+ ->getJson(route('spare_part_reservations.for_reclamation', $reclamation->id));
|
|
|
+
|
|
|
+ $response->assertStatus(200);
|
|
|
+ $response->assertJsonStructure(['reservations']);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_returns_only_active_reservations_for_reclamation(): void
|
|
|
+ {
|
|
|
+ $sparePart = SparePart::factory()->create();
|
|
|
+ $order = SparePartOrder::factory()->inStock()->withQuantity(20)->forSparePart($sparePart)->create();
|
|
|
+ $reclamation = Reclamation::factory()->create();
|
|
|
+
|
|
|
+ // Активный резерв
|
|
|
+ Reservation::factory()->create([
|
|
|
+ 'spare_part_id' => $sparePart->id,
|
|
|
+ 'spare_part_order_id' => $order->id,
|
|
|
+ 'reclamation_id' => $reclamation->id,
|
|
|
+ 'reserved_qty' => 5,
|
|
|
+ 'status' => Reservation::STATUS_ACTIVE,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ // Отменённый резерв — не должен попасть
|
|
|
+ Reservation::factory()->create([
|
|
|
+ 'spare_part_id' => $sparePart->id,
|
|
|
+ 'spare_part_order_id' => $order->id,
|
|
|
+ 'reclamation_id' => $reclamation->id,
|
|
|
+ 'reserved_qty' => 2,
|
|
|
+ 'status' => Reservation::STATUS_CANCELLED,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $response = $this->actingAs($this->adminUser)
|
|
|
+ ->getJson(route('spare_part_reservations.for_reclamation', $reclamation->id));
|
|
|
+
|
|
|
+ $response->assertStatus(200);
|
|
|
+ $this->assertCount(1, $response->json('reservations'));
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== shortagesForReclamation (JSON API) ====================
|
|
|
+
|
|
|
+ public function test_guest_cannot_get_shortages_for_reclamation(): void
|
|
|
+ {
|
|
|
+ $reclamation = Reclamation::factory()->create();
|
|
|
+
|
|
|
+ $response = $this->get(route('spare_part_reservations.shortages_for_reclamation', $reclamation->id));
|
|
|
+
|
|
|
+ $response->assertRedirect(route('login'));
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_admin_can_get_shortages_for_reclamation(): void
|
|
|
+ {
|
|
|
+ $sparePart = SparePart::factory()->create(['article' => 'SP-SHORTAGE-001']);
|
|
|
+ $reclamation = Reclamation::factory()->create();
|
|
|
+
|
|
|
+ Shortage::factory()->create([
|
|
|
+ 'spare_part_id' => $sparePart->id,
|
|
|
+ 'reclamation_id' => $reclamation->id,
|
|
|
+ 'required_qty' => 10,
|
|
|
+ 'reserved_qty' => 3,
|
|
|
+ 'missing_qty' => 7,
|
|
|
+ 'status' => Shortage::STATUS_OPEN,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $response = $this->actingAs($this->adminUser)
|
|
|
+ ->getJson(route('spare_part_reservations.shortages_for_reclamation', $reclamation->id));
|
|
|
+
|
|
|
+ $response->assertStatus(200);
|
|
|
+ $response->assertJsonStructure([
|
|
|
+ 'shortages' => [['id', 'spare_part_id', 'article', 'required_qty', 'missing_qty', 'status']],
|
|
|
+ ]);
|
|
|
+ $response->assertJsonFragment(['spare_part_id' => $sparePart->id]);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_manager_can_get_shortages_for_reclamation(): void
|
|
|
+ {
|
|
|
+ $reclamation = Reclamation::factory()->create();
|
|
|
+
|
|
|
+ $response = $this->actingAs($this->managerUser)
|
|
|
+ ->getJson(route('spare_part_reservations.shortages_for_reclamation', $reclamation->id));
|
|
|
+
|
|
|
+ $response->assertStatus(200);
|
|
|
+ $response->assertJsonStructure(['shortages']);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== cancel ====================
|
|
|
+
|
|
|
+ public function test_guest_cannot_cancel_reservation(): void
|
|
|
+ {
|
|
|
+ $reservation = Reservation::factory()->active()->create();
|
|
|
+
|
|
|
+ $response = $this->post(route('spare_part_reservations.cancel', $reservation));
|
|
|
+
|
|
|
+ $response->assertRedirect(route('login'));
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_admin_can_cancel_active_reservation(): void
|
|
|
+ {
|
|
|
+ $sparePart = SparePart::factory()->create();
|
|
|
+ $order = SparePartOrder::factory()->inStock()->withQuantity(10)->forSparePart($sparePart)->create();
|
|
|
+ $reclamation = Reclamation::factory()->create();
|
|
|
+
|
|
|
+ $reservation = Reservation::factory()->create([
|
|
|
+ 'spare_part_id' => $sparePart->id,
|
|
|
+ 'spare_part_order_id' => $order->id,
|
|
|
+ 'reclamation_id' => $reclamation->id,
|
|
|
+ 'reserved_qty' => 5,
|
|
|
+ 'status' => Reservation::STATUS_ACTIVE,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $response = $this->actingAs($this->adminUser)
|
|
|
+ ->post(route('spare_part_reservations.cancel', $reservation), [
|
|
|
+ 'reason' => 'Тестовая отмена',
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $response->assertRedirect();
|
|
|
+ $response->assertSessionHas('success');
|
|
|
+ $this->assertDatabaseHas('reservations', [
|
|
|
+ 'id' => $reservation->id,
|
|
|
+ 'status' => Reservation::STATUS_CANCELLED,
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_manager_can_cancel_active_reservation(): void
|
|
|
+ {
|
|
|
+ $sparePart = SparePart::factory()->create();
|
|
|
+ $order = SparePartOrder::factory()->inStock()->withQuantity(10)->forSparePart($sparePart)->create();
|
|
|
+ $reclamation = Reclamation::factory()->create();
|
|
|
+
|
|
|
+ $reservation = Reservation::factory()->create([
|
|
|
+ 'spare_part_id' => $sparePart->id,
|
|
|
+ 'spare_part_order_id' => $order->id,
|
|
|
+ 'reclamation_id' => $reclamation->id,
|
|
|
+ 'reserved_qty' => 3,
|
|
|
+ 'status' => Reservation::STATUS_ACTIVE,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $response = $this->actingAs($this->managerUser)
|
|
|
+ ->post(route('spare_part_reservations.cancel', $reservation));
|
|
|
+
|
|
|
+ $response->assertRedirect();
|
|
|
+ $response->assertSessionHas('success');
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_cannot_cancel_inactive_reservation(): void
|
|
|
+ {
|
|
|
+ $reservation = Reservation::factory()->cancelled()->create();
|
|
|
+
|
|
|
+ $response = $this->actingAs($this->adminUser)
|
|
|
+ ->post(route('spare_part_reservations.cancel', $reservation));
|
|
|
+
|
|
|
+ $response->assertRedirect();
|
|
|
+ $response->assertSessionHas('error');
|
|
|
+ // Статус не изменился
|
|
|
+ $this->assertDatabaseHas('reservations', [
|
|
|
+ 'id' => $reservation->id,
|
|
|
+ 'status' => Reservation::STATUS_CANCELLED,
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_cannot_cancel_issued_reservation(): void
|
|
|
+ {
|
|
|
+ $reservation = Reservation::factory()->issued()->create();
|
|
|
+
|
|
|
+ $response = $this->actingAs($this->adminUser)
|
|
|
+ ->post(route('spare_part_reservations.cancel', $reservation));
|
|
|
+
|
|
|
+ $response->assertRedirect();
|
|
|
+ $response->assertSessionHas('error');
|
|
|
+ $this->assertDatabaseHas('reservations', [
|
|
|
+ 'id' => $reservation->id,
|
|
|
+ 'status' => Reservation::STATUS_ISSUED,
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== issue ====================
|
|
|
+
|
|
|
+ public function test_guest_cannot_issue_reservation(): void
|
|
|
+ {
|
|
|
+ $reservation = Reservation::factory()->active()->create();
|
|
|
+
|
|
|
+ $response = $this->post(route('spare_part_reservations.issue', $reservation));
|
|
|
+
|
|
|
+ $response->assertRedirect(route('login'));
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_admin_can_issue_active_reservation(): void
|
|
|
+ {
|
|
|
+ $sparePart = SparePart::factory()->create();
|
|
|
+ $order = SparePartOrder::factory()->inStock()->forSparePart($sparePart)->create([
|
|
|
+ 'ordered_quantity' => 20,
|
|
|
+ 'available_qty' => 20,
|
|
|
+ ]);
|
|
|
+ $reclamation = Reclamation::factory()->create();
|
|
|
+
|
|
|
+ $reservation = Reservation::factory()->create([
|
|
|
+ 'spare_part_id' => $sparePart->id,
|
|
|
+ 'spare_part_order_id' => $order->id,
|
|
|
+ 'reclamation_id' => $reclamation->id,
|
|
|
+ 'reserved_qty' => 5,
|
|
|
+ 'status' => Reservation::STATUS_ACTIVE,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $response = $this->actingAs($this->adminUser)
|
|
|
+ ->post(route('spare_part_reservations.issue', $reservation), [
|
|
|
+ 'note' => 'Тестовое списание',
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $response->assertRedirect();
|
|
|
+ $response->assertSessionHas('success');
|
|
|
+
|
|
|
+ $this->assertDatabaseHas('reservations', [
|
|
|
+ 'id' => $reservation->id,
|
|
|
+ 'status' => Reservation::STATUS_ISSUED,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $this->assertDatabaseHas('spare_part_orders', [
|
|
|
+ 'id' => $order->id,
|
|
|
+ 'available_qty' => 15,
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_manager_can_issue_active_reservation(): void
|
|
|
+ {
|
|
|
+ $sparePart = SparePart::factory()->create();
|
|
|
+ $order = SparePartOrder::factory()->inStock()->forSparePart($sparePart)->create([
|
|
|
+ 'ordered_quantity' => 10,
|
|
|
+ 'available_qty' => 10,
|
|
|
+ ]);
|
|
|
+ $reclamation = Reclamation::factory()->create();
|
|
|
+
|
|
|
+ $reservation = Reservation::factory()->create([
|
|
|
+ 'spare_part_id' => $sparePart->id,
|
|
|
+ 'spare_part_order_id' => $order->id,
|
|
|
+ 'reclamation_id' => $reclamation->id,
|
|
|
+ 'reserved_qty' => 3,
|
|
|
+ 'status' => Reservation::STATUS_ACTIVE,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $response = $this->actingAs($this->managerUser)
|
|
|
+ ->post(route('spare_part_reservations.issue', $reservation));
|
|
|
+
|
|
|
+ $response->assertRedirect();
|
|
|
+ $response->assertSessionHas('success');
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_cannot_issue_inactive_reservation(): void
|
|
|
+ {
|
|
|
+ $reservation = Reservation::factory()->cancelled()->create();
|
|
|
+
|
|
|
+ $response = $this->actingAs($this->adminUser)
|
|
|
+ ->post(route('spare_part_reservations.issue', $reservation));
|
|
|
+
|
|
|
+ $response->assertRedirect();
|
|
|
+ $response->assertSessionHas('error');
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== issueAllForReclamation ====================
|
|
|
+
|
|
|
+ public function test_guest_cannot_issue_all_for_reclamation(): void
|
|
|
+ {
|
|
|
+ $reclamation = Reclamation::factory()->create();
|
|
|
+
|
|
|
+ $response = $this->post(route('spare_part_reservations.issue_all', $reclamation->id));
|
|
|
+
|
|
|
+ $response->assertRedirect(route('login'));
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_admin_can_issue_all_for_reclamation(): void
|
|
|
+ {
|
|
|
+ $sparePart = SparePart::factory()->create();
|
|
|
+ $order = SparePartOrder::factory()->inStock()->forSparePart($sparePart)->create([
|
|
|
+ 'ordered_quantity' => 50,
|
|
|
+ 'available_qty' => 50,
|
|
|
+ ]);
|
|
|
+ $reclamation = Reclamation::factory()->create();
|
|
|
+
|
|
|
+ // Два активных резерва для рекламации
|
|
|
+ Reservation::factory()->create([
|
|
|
+ 'spare_part_id' => $sparePart->id,
|
|
|
+ 'spare_part_order_id' => $order->id,
|
|
|
+ 'reclamation_id' => $reclamation->id,
|
|
|
+ 'reserved_qty' => 5,
|
|
|
+ 'status' => Reservation::STATUS_ACTIVE,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ Reservation::factory()->create([
|
|
|
+ 'spare_part_id' => $sparePart->id,
|
|
|
+ 'spare_part_order_id' => $order->id,
|
|
|
+ 'reclamation_id' => $reclamation->id,
|
|
|
+ 'reserved_qty' => 3,
|
|
|
+ 'status' => Reservation::STATUS_ACTIVE,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $response = $this->actingAs($this->adminUser)
|
|
|
+ ->post(route('spare_part_reservations.issue_all', $reclamation->id));
|
|
|
+
|
|
|
+ $response->assertRedirect();
|
|
|
+ $response->assertSessionHas('success');
|
|
|
+
|
|
|
+ // Все резервы рекламации должны быть списаны
|
|
|
+ $this->assertDatabaseMissing('reservations', [
|
|
|
+ 'reclamation_id' => $reclamation->id,
|
|
|
+ 'status' => Reservation::STATUS_ACTIVE,
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_manager_can_issue_all_for_reclamation(): void
|
|
|
+ {
|
|
|
+ $reclamation = Reclamation::factory()->create();
|
|
|
+
|
|
|
+ $response = $this->actingAs($this->managerUser)
|
|
|
+ ->post(route('spare_part_reservations.issue_all', $reclamation->id));
|
|
|
+
|
|
|
+ $response->assertRedirect();
|
|
|
+ $response->assertSessionHas('success');
|
|
|
+ }
|
|
|
+
|
|
|
+ // ==================== cancelAllForReclamation ====================
|
|
|
+
|
|
|
+ public function test_guest_cannot_cancel_all_for_reclamation(): void
|
|
|
+ {
|
|
|
+ $reclamation = Reclamation::factory()->create();
|
|
|
+
|
|
|
+ $response = $this->post(route('spare_part_reservations.cancel_all', $reclamation->id));
|
|
|
+
|
|
|
+ $response->assertRedirect(route('login'));
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_admin_can_cancel_all_for_reclamation(): void
|
|
|
+ {
|
|
|
+ $sparePart = SparePart::factory()->create();
|
|
|
+ $order = SparePartOrder::factory()->inStock()->withQuantity(30)->forSparePart($sparePart)->create();
|
|
|
+ $reclamation = Reclamation::factory()->create();
|
|
|
+
|
|
|
+ Reservation::factory()->create([
|
|
|
+ 'spare_part_id' => $sparePart->id,
|
|
|
+ 'spare_part_order_id' => $order->id,
|
|
|
+ 'reclamation_id' => $reclamation->id,
|
|
|
+ 'reserved_qty' => 4,
|
|
|
+ 'status' => Reservation::STATUS_ACTIVE,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ Reservation::factory()->create([
|
|
|
+ 'spare_part_id' => $sparePart->id,
|
|
|
+ 'spare_part_order_id' => $order->id,
|
|
|
+ 'reclamation_id' => $reclamation->id,
|
|
|
+ 'reserved_qty' => 6,
|
|
|
+ 'status' => Reservation::STATUS_ACTIVE,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $response = $this->actingAs($this->adminUser)
|
|
|
+ ->post(route('spare_part_reservations.cancel_all', $reclamation->id), [
|
|
|
+ 'reason' => 'Тестовая массовая отмена',
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $response->assertRedirect();
|
|
|
+ $response->assertSessionHas('success');
|
|
|
+
|
|
|
+ // Все активные резервы рекламации отменены
|
|
|
+ $this->assertDatabaseMissing('reservations', [
|
|
|
+ 'reclamation_id' => $reclamation->id,
|
|
|
+ 'status' => Reservation::STATUS_ACTIVE,
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_manager_can_cancel_all_for_reclamation(): void
|
|
|
+ {
|
|
|
+ $reclamation = Reclamation::factory()->create();
|
|
|
+
|
|
|
+ $response = $this->actingAs($this->managerUser)
|
|
|
+ ->post(route('spare_part_reservations.cancel_all', $reclamation->id));
|
|
|
+
|
|
|
+ $response->assertRedirect();
|
|
|
+ $response->assertSessionHas('success');
|
|
|
+ }
|
|
|
+
|
|
|
+ public function test_cancel_all_includes_success_message_with_quantity(): void
|
|
|
+ {
|
|
|
+ $sparePart = SparePart::factory()->create();
|
|
|
+ $order = SparePartOrder::factory()->inStock()->withQuantity(20)->forSparePart($sparePart)->create();
|
|
|
+ $reclamation = Reclamation::factory()->create();
|
|
|
+
|
|
|
+ Reservation::factory()->create([
|
|
|
+ 'spare_part_id' => $sparePart->id,
|
|
|
+ 'spare_part_order_id' => $order->id,
|
|
|
+ 'reclamation_id' => $reclamation->id,
|
|
|
+ 'reserved_qty' => 7,
|
|
|
+ 'status' => Reservation::STATUS_ACTIVE,
|
|
|
+ ]);
|
|
|
+
|
|
|
+ $response = $this->actingAs($this->adminUser)
|
|
|
+ ->post(route('spare_part_reservations.cancel_all', $reclamation->id));
|
|
|
+
|
|
|
+ $response->assertSessionHas('success');
|
|
|
+ $this->assertStringContainsString('7', session('success'));
|
|
|
+ }
|
|
|
+}
|