| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523 |
- <?php
- namespace Tests\Unit\Services;
- use App\Models\InventoryMovement;
- use App\Models\Reclamation;
- use App\Models\Reservation;
- use App\Models\Shortage;
- use App\Models\SparePart;
- use App\Models\SparePartOrder;
- use App\Services\ReservationResult;
- use App\Services\ShortageService;
- use App\Services\SparePartReservationService;
- use Illuminate\Foundation\Testing\RefreshDatabase;
- use Tests\TestCase;
- class SparePartReservationServiceTest extends TestCase
- {
- use RefreshDatabase;
- protected $seed = true;
- private SparePartReservationService $service;
- private ShortageService $shortageService;
- protected function setUp(): void
- {
- parent::setUp();
- $this->shortageService = new ShortageService();
- $this->service = new SparePartReservationService($this->shortageService);
- }
- public function test_reserve_with_sufficient_stock_creates_reservation(): void
- {
- // Arrange
- $sparePart = SparePart::factory()->create();
- $reclamation = Reclamation::factory()->create();
- $order = SparePartOrder::factory()
- ->inStock()
- ->withDocuments(false)
- ->withQuantity(10)
- ->forSparePart($sparePart)
- ->create();
- // Act
- $result = $this->service->reserve(
- sparePartId: $sparePart->id,
- quantity: 5,
- withDocuments: false,
- reclamationId: $reclamation->id
- );
- // Assert
- $this->assertInstanceOf(ReservationResult::class, $result);
- $this->assertEquals(5, $result->reserved);
- $this->assertEquals(0, $result->missing);
- $this->assertTrue($result->isFullyReserved());
- $this->assertFalse($result->hasShortage());
- $this->assertCount(1, $result->reservations);
- // Check reservation was created in DB
- $this->assertDatabaseHas('reservations', [
- 'spare_part_id' => $sparePart->id,
- 'spare_part_order_id' => $order->id,
- 'reclamation_id' => $reclamation->id,
- 'reserved_qty' => 5,
- 'with_documents' => false,
- 'status' => Reservation::STATUS_ACTIVE,
- ]);
- // Check inventory movement was created
- $this->assertDatabaseHas('inventory_movements', [
- 'spare_part_id' => $sparePart->id,
- 'spare_part_order_id' => $order->id,
- 'qty' => 5,
- 'movement_type' => InventoryMovement::TYPE_RESERVE,
- 'source_type' => InventoryMovement::SOURCE_RECLAMATION,
- 'source_id' => $reclamation->id,
- ]);
- }
- public function test_reserve_with_partial_stock_creates_shortage(): void
- {
- // Arrange
- $sparePart = SparePart::factory()->create();
- $reclamation = Reclamation::factory()->create();
- SparePartOrder::factory()
- ->inStock()
- ->withDocuments(false)
- ->withQuantity(3)
- ->forSparePart($sparePart)
- ->create();
- // Act
- $result = $this->service->reserve(
- sparePartId: $sparePart->id,
- quantity: 5,
- withDocuments: false,
- reclamationId: $reclamation->id
- );
- // Assert
- $this->assertEquals(3, $result->reserved);
- $this->assertEquals(2, $result->missing);
- $this->assertFalse($result->isFullyReserved());
- $this->assertTrue($result->hasShortage());
- // Check shortage was created
- $this->assertDatabaseHas('shortages', [
- 'spare_part_id' => $sparePart->id,
- 'reclamation_id' => $reclamation->id,
- 'with_documents' => false,
- 'required_qty' => 5,
- 'reserved_qty' => 3,
- 'missing_qty' => 2,
- 'status' => Shortage::STATUS_OPEN,
- ]);
- }
- public function test_reserve_with_no_stock_creates_full_shortage(): void
- {
- // Arrange
- $sparePart = SparePart::factory()->create();
- $reclamation = Reclamation::factory()->create();
- // No orders created = no stock
- // Act
- $result = $this->service->reserve(
- sparePartId: $sparePart->id,
- quantity: 5,
- withDocuments: false,
- reclamationId: $reclamation->id
- );
- // Assert
- $this->assertEquals(0, $result->reserved);
- $this->assertEquals(5, $result->missing);
- $this->assertFalse($result->isFullyReserved());
- $this->assertTrue($result->hasShortage());
- $this->assertEmpty($result->reservations);
- // Check full shortage was created
- $this->assertDatabaseHas('shortages', [
- 'spare_part_id' => $sparePart->id,
- 'reclamation_id' => $reclamation->id,
- 'required_qty' => 5,
- 'reserved_qty' => 0,
- 'missing_qty' => 5,
- 'status' => Shortage::STATUS_OPEN,
- ]);
- }
- public function test_reserve_respects_fifo_order_for_multiple_batches(): void
- {
- // Arrange
- $sparePart = SparePart::factory()->create();
- $reclamation = Reclamation::factory()->create();
- // Create first batch (older)
- $olderOrder = SparePartOrder::factory()
- ->inStock()
- ->withDocuments(false)
- ->withQuantity(3)
- ->forSparePart($sparePart)
- ->create(['created_at' => now()->subDays(2)]);
- // Create second batch (newer)
- $newerOrder = SparePartOrder::factory()
- ->inStock()
- ->withDocuments(false)
- ->withQuantity(5)
- ->forSparePart($sparePart)
- ->create(['created_at' => now()->subDay()]);
- // Act - reserve 5 items (should use 3 from older + 2 from newer)
- $result = $this->service->reserve(
- sparePartId: $sparePart->id,
- quantity: 5,
- withDocuments: false,
- reclamationId: $reclamation->id
- );
- // Assert
- $this->assertEquals(5, $result->reserved);
- $this->assertCount(2, $result->reservations);
- // Check FIFO: first batch fully used
- $this->assertDatabaseHas('reservations', [
- 'spare_part_order_id' => $olderOrder->id,
- 'reserved_qty' => 3,
- ]);
- // Check FIFO: second batch partially used
- $this->assertDatabaseHas('reservations', [
- 'spare_part_order_id' => $newerOrder->id,
- 'reserved_qty' => 2,
- ]);
- }
- public function test_reserve_respects_with_documents_flag(): void
- {
- // Arrange
- $sparePart = SparePart::factory()->create();
- $reclamation = Reclamation::factory()->create();
- // Create batch WITHOUT documents
- SparePartOrder::factory()
- ->inStock()
- ->withDocuments(false)
- ->withQuantity(10)
- ->forSparePart($sparePart)
- ->create();
- // Create batch WITH documents
- $orderWithDocs = SparePartOrder::factory()
- ->inStock()
- ->withDocuments(true)
- ->withQuantity(10)
- ->forSparePart($sparePart)
- ->create();
- // Act - reserve WITH documents
- $result = $this->service->reserve(
- sparePartId: $sparePart->id,
- quantity: 5,
- withDocuments: true,
- reclamationId: $reclamation->id
- );
- // Assert - should only use batch with documents
- $this->assertEquals(5, $result->reserved);
- $this->assertCount(1, $result->reservations);
- $this->assertDatabaseHas('reservations', [
- 'spare_part_order_id' => $orderWithDocs->id,
- 'with_documents' => true,
- ]);
- }
- public function test_reserve_considers_existing_reservations(): void
- {
- // Arrange
- $sparePart = SparePart::factory()->create();
- $reclamation1 = Reclamation::factory()->create();
- $reclamation2 = Reclamation::factory()->create();
- $order = SparePartOrder::factory()
- ->inStock()
- ->withDocuments(false)
- ->withQuantity(10)
- ->forSparePart($sparePart)
- ->create();
- // Create existing active reservation for 7 items
- Reservation::factory()
- ->active()
- ->withQuantity(7)
- ->fromOrder($order)
- ->forReclamation($reclamation1)
- ->create();
- // Act - try to reserve 5 more (only 3 available)
- $result = $this->service->reserve(
- sparePartId: $sparePart->id,
- quantity: 5,
- withDocuments: false,
- reclamationId: $reclamation2->id
- );
- // Assert
- $this->assertEquals(3, $result->reserved);
- $this->assertEquals(2, $result->missing);
- $this->assertTrue($result->hasShortage());
- }
- public function test_cancel_for_reclamation_cancels_all_reservations(): void
- {
- // Arrange
- $sparePart = SparePart::factory()->create();
- $reclamation = Reclamation::factory()->create();
- $order = SparePartOrder::factory()
- ->inStock()
- ->withDocuments(false)
- ->withQuantity(10)
- ->forSparePart($sparePart)
- ->create();
- // Create reservations
- Reservation::factory()
- ->active()
- ->withQuantity(3)
- ->fromOrder($order)
- ->forReclamation($reclamation)
- ->create();
- Reservation::factory()
- ->active()
- ->withQuantity(2)
- ->fromOrder($order)
- ->forReclamation($reclamation)
- ->create();
- // Create shortage
- Shortage::factory()
- ->open()
- ->withQuantities(10, 5)
- ->forSparePart($sparePart)
- ->forReclamation($reclamation)
- ->withDocuments(false)
- ->create();
- // Act
- $cancelled = $this->service->cancelForReclamation($reclamation->id);
- // Assert
- $this->assertEquals(5, $cancelled);
- // Check all reservations are cancelled
- $this->assertDatabaseMissing('reservations', [
- 'reclamation_id' => $reclamation->id,
- 'status' => Reservation::STATUS_ACTIVE,
- ]);
- $this->assertEquals(2, Reservation::where('reclamation_id', $reclamation->id)
- ->where('status', Reservation::STATUS_CANCELLED)->count());
- // Check shortage is closed
- $this->assertDatabaseHas('shortages', [
- 'reclamation_id' => $reclamation->id,
- 'status' => Shortage::STATUS_CLOSED,
- ]);
- }
- public function test_cancel_for_reclamation_filters_by_spare_part(): void
- {
- // Arrange
- $sparePart1 = SparePart::factory()->create();
- $sparePart2 = SparePart::factory()->create();
- $reclamation = Reclamation::factory()->create();
- $order1 = SparePartOrder::factory()
- ->inStock()
- ->forSparePart($sparePart1)
- ->create();
- $order2 = SparePartOrder::factory()
- ->inStock()
- ->forSparePart($sparePart2)
- ->create();
- $reservation1 = Reservation::factory()
- ->active()
- ->withQuantity(3)
- ->fromOrder($order1)
- ->forReclamation($reclamation)
- ->create();
- $reservation2 = Reservation::factory()
- ->active()
- ->withQuantity(5)
- ->fromOrder($order2)
- ->forReclamation($reclamation)
- ->create();
- // Act - cancel only for sparePart1
- $cancelled = $this->service->cancelForReclamation(
- $reclamation->id,
- sparePartId: $sparePart1->id
- );
- // Assert
- $this->assertEquals(3, $cancelled);
- $this->assertDatabaseHas('reservations', [
- 'id' => $reservation1->id,
- 'status' => Reservation::STATUS_CANCELLED,
- ]);
- $this->assertDatabaseHas('reservations', [
- 'id' => $reservation2->id,
- 'status' => Reservation::STATUS_ACTIVE,
- ]);
- }
- public function test_get_reservations_for_reclamation_returns_active_only(): void
- {
- // Arrange
- $sparePart = SparePart::factory()->create();
- $reclamation = Reclamation::factory()->create();
- $order = SparePartOrder::factory()
- ->inStock()
- ->forSparePart($sparePart)
- ->create();
- Reservation::factory()
- ->active()
- ->fromOrder($order)
- ->forReclamation($reclamation)
- ->create();
- Reservation::factory()
- ->cancelled()
- ->fromOrder($order)
- ->forReclamation($reclamation)
- ->create();
- Reservation::factory()
- ->issued()
- ->fromOrder($order)
- ->forReclamation($reclamation)
- ->create();
- // Act
- $reservations = $this->service->getReservationsForReclamation($reclamation->id);
- // Assert
- $this->assertCount(1, $reservations);
- $this->assertEquals(Reservation::STATUS_ACTIVE, $reservations->first()->status);
- }
- public function test_adjust_reservation_increases_quantity(): void
- {
- // Arrange
- $sparePart = SparePart::factory()->create();
- $reclamation = Reclamation::factory()->create();
- $order = SparePartOrder::factory()
- ->inStock()
- ->withDocuments(false)
- ->withQuantity(20)
- ->forSparePart($sparePart)
- ->create();
- // Initial reservation of 5
- Reservation::factory()
- ->active()
- ->withQuantity(5)
- ->withDocuments(false)
- ->fromOrder($order)
- ->forReclamation($reclamation)
- ->create();
- // Act - increase to 10
- $result = $this->service->adjustReservation(
- reclamationId: $reclamation->id,
- sparePartId: $sparePart->id,
- withDocuments: false,
- newQuantity: 10
- );
- // Assert - should have reserved additional 5
- $this->assertEquals(5, $result->reserved);
- $this->assertEquals(0, $result->missing);
- $totalReserved = Reservation::where('reclamation_id', $reclamation->id)
- ->where('spare_part_id', $sparePart->id)
- ->where('status', Reservation::STATUS_ACTIVE)
- ->sum('reserved_qty');
- $this->assertEquals(10, $totalReserved);
- }
- public function test_adjust_reservation_decreases_quantity(): void
- {
- // Arrange
- $sparePart = SparePart::factory()->create();
- $reclamation = Reclamation::factory()->create();
- $order = SparePartOrder::factory()
- ->inStock()
- ->withDocuments(false)
- ->withQuantity(20)
- ->forSparePart($sparePart)
- ->create();
- // Initial reservation of 10
- Reservation::factory()
- ->active()
- ->withQuantity(10)
- ->withDocuments(false)
- ->fromOrder($order)
- ->forReclamation($reclamation)
- ->create();
- // Act - decrease to 3
- $result = $this->service->adjustReservation(
- reclamationId: $reclamation->id,
- sparePartId: $sparePart->id,
- withDocuments: false,
- newQuantity: 3
- );
- // Assert
- $this->assertEquals(3, $result->reserved);
- $this->assertEquals(0, $result->missing);
- }
- public function test_cancel_reservation_does_not_cancel_non_active(): void
- {
- // Arrange
- $sparePart = SparePart::factory()->create();
- $reclamation = Reclamation::factory()->create();
- $order = SparePartOrder::factory()
- ->inStock()
- ->forSparePart($sparePart)
- ->create();
- $reservation = Reservation::factory()
- ->issued()
- ->fromOrder($order)
- ->forReclamation($reclamation)
- ->create();
- // Act
- $result = $this->service->cancelReservation($reservation);
- // Assert
- $this->assertFalse($result);
- $this->assertDatabaseHas('reservations', [
- 'id' => $reservation->id,
- 'status' => Reservation::STATUS_ISSUED,
- ]);
- }
- }
|