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, ]); } }