| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477 |
- <?php
- namespace Tests\Unit\Services;
- use App\Models\Reclamation;
- use App\Models\Reservation;
- use App\Models\Shortage;
- use App\Models\SparePart;
- use App\Models\SparePartOrder;
- use App\Services\ShortageService;
- use App\Services\SparePartInventoryService;
- use App\Services\SparePartReservationService;
- use Illuminate\Foundation\Testing\RefreshDatabase;
- use Tests\TestCase;
- class SparePartInventoryServiceTest extends TestCase
- {
- use RefreshDatabase;
- protected $seed = true;
- private SparePartInventoryService $service;
- protected function setUp(): void
- {
- parent::setUp();
- $shortageService = new ShortageService();
- $reservationService = new SparePartReservationService($shortageService);
- $this->service = new SparePartInventoryService($reservationService, $shortageService);
- }
- // ==================== getCriticalShortages ====================
- public function test_get_critical_shortages_returns_spare_parts_with_open_shortages(): void
- {
- // Arrange
- $sparePart1 = SparePart::factory()->create();
- $sparePart2 = SparePart::factory()->create();
- $reclamation = Reclamation::factory()->create();
- Shortage::factory()
- ->open()
- ->withQuantities(5, 0)
- ->forSparePart($sparePart1)
- ->forReclamation($reclamation)
- ->create();
- Shortage::factory()
- ->closed()
- ->forSparePart($sparePart2)
- ->forReclamation($reclamation)
- ->create();
- // Act
- $criticalShortages = $this->service->getCriticalShortages();
- // Assert - returns SparePart objects, not Shortages
- $this->assertCount(1, $criticalShortages);
- $this->assertEquals($sparePart1->id, $criticalShortages->first()->id);
- }
- public function test_get_critical_shortages_returns_empty_when_no_shortages(): void
- {
- // Act
- $criticalShortages = $this->service->getCriticalShortages();
- // Assert
- $this->assertTrue($criticalShortages->isEmpty());
- }
- // ==================== getBelowMinStock ====================
- public function test_get_below_min_stock_returns_parts_below_minimum(): void
- {
- // Arrange
- $sparePart1 = SparePart::factory()->create(['min_stock' => 10]);
- $sparePart2 = SparePart::factory()->create(['min_stock' => 5]);
- // Create stock for sparePart1 - 3 items (below min 10)
- SparePartOrder::factory()
- ->inStock()
- ->withQuantity(3)
- ->forSparePart($sparePart1)
- ->create();
- // Create stock for sparePart2 - 10 items (above min 5)
- SparePartOrder::factory()
- ->inStock()
- ->withQuantity(10)
- ->forSparePart($sparePart2)
- ->create();
- // Act
- $belowMin = $this->service->getBelowMinStock();
- // Assert
- $this->assertTrue($belowMin->contains('id', $sparePart1->id));
- $this->assertFalse($belowMin->contains('id', $sparePart2->id));
- }
- public function test_get_below_min_stock_considers_reservations(): void
- {
- // Arrange
- $sparePart = SparePart::factory()->create(['min_stock' => 5]);
- $reclamation = Reclamation::factory()->create();
- $order = SparePartOrder::factory()
- ->inStock()
- ->withQuantity(10)
- ->forSparePart($sparePart)
- ->create();
- // Reserve 7 items - leaves only 3 free (below min 5)
- Reservation::factory()
- ->active()
- ->withQuantity(7)
- ->fromOrder($order)
- ->forReclamation($reclamation)
- ->create();
- // Act
- $belowMin = $this->service->getBelowMinStock();
- // Assert
- $this->assertTrue($belowMin->contains('id', $sparePart->id));
- }
- // ==================== calculateOrderQuantity ====================
- public function test_calculate_order_quantity_returns_missing_from_shortages(): void
- {
- // Arrange
- $sparePart = SparePart::factory()->create();
- $reclamation1 = Reclamation::factory()->create();
- $reclamation2 = Reclamation::factory()->create();
- // Create two shortages without documents
- Shortage::factory()
- ->open()
- ->withQuantities(5, 2) // missing 3
- ->forSparePart($sparePart)
- ->forReclamation($reclamation1)
- ->withDocuments(false)
- ->create();
- Shortage::factory()
- ->open()
- ->withQuantities(4, 1) // missing 3
- ->forSparePart($sparePart)
- ->forReclamation($reclamation2)
- ->withDocuments(false)
- ->create();
- // Act
- $orderQty = $this->service->calculateOrderQuantity($sparePart, false);
- // Assert
- $this->assertEquals(6, $orderQty); // 3 + 3
- }
- public function test_calculate_order_quantity_respects_with_documents_flag(): void
- {
- // Arrange
- $sparePart = SparePart::factory()->create();
- $reclamation = Reclamation::factory()->create();
- Shortage::factory()
- ->open()
- ->withQuantities(5, 0) // missing 5 WITHOUT docs
- ->forSparePart($sparePart)
- ->forReclamation($reclamation)
- ->withDocuments(false)
- ->create();
- Shortage::factory()
- ->open()
- ->withQuantities(3, 0) // missing 3 WITH docs
- ->forSparePart($sparePart)
- ->forReclamation($reclamation)
- ->withDocuments(true)
- ->create();
- // Act
- $orderQtyWithDocs = $this->service->calculateOrderQuantity($sparePart, true);
- $orderQtyWithoutDocs = $this->service->calculateOrderQuantity($sparePart, false);
- // Assert
- $this->assertEquals(3, $orderQtyWithDocs);
- $this->assertEquals(5, $orderQtyWithoutDocs);
- }
- // ==================== calculateMinStockOrderQuantity ====================
- public function test_calculate_min_stock_order_quantity_returns_difference_to_min(): void
- {
- // Arrange
- $sparePart = SparePart::factory()->create(['min_stock' => 15]);
- SparePartOrder::factory()
- ->inStock()
- ->withQuantity(10)
- ->forSparePart($sparePart)
- ->create();
- // Act
- $orderQty = $this->service->calculateMinStockOrderQuantity($sparePart);
- // Assert
- $this->assertEquals(5, $orderQty); // 15 - 10 = 5
- }
- public function test_calculate_min_stock_order_quantity_returns_zero_when_above_min(): void
- {
- // Arrange
- $sparePart = SparePart::factory()->create(['min_stock' => 5]);
- SparePartOrder::factory()
- ->inStock()
- ->withQuantity(10)
- ->forSparePart($sparePart)
- ->create();
- // Act
- $orderQty = $this->service->calculateMinStockOrderQuantity($sparePart);
- // Assert
- $this->assertEquals(0, $orderQty);
- }
- public function test_calculate_min_stock_order_quantity_considers_reservations(): void
- {
- // Arrange
- $sparePart = SparePart::factory()->create(['min_stock' => 10]);
- $reclamation = Reclamation::factory()->create();
- $order = SparePartOrder::factory()
- ->inStock()
- ->withQuantity(15)
- ->forSparePart($sparePart)
- ->create();
- // Reserve 8 items - leaves 7 free
- Reservation::factory()
- ->active()
- ->withQuantity(8)
- ->fromOrder($order)
- ->forReclamation($reclamation)
- ->create();
- // Act
- $orderQty = $this->service->calculateMinStockOrderQuantity($sparePart);
- // Assert
- $this->assertEquals(3, $orderQty); // 10 - 7 = 3
- }
- // ==================== getStockInfo ====================
- public function test_get_stock_info_returns_complete_information(): void
- {
- // Arrange
- $sparePart = SparePart::factory()->create(['min_stock' => 10]);
- $reclamation = Reclamation::factory()->create();
- $orderWithDocs = SparePartOrder::factory()
- ->inStock()
- ->withDocuments(true)
- ->withQuantity(20)
- ->forSparePart($sparePart)
- ->create();
- $orderWithoutDocs = SparePartOrder::factory()
- ->inStock()
- ->withDocuments(false)
- ->withQuantity(15)
- ->forSparePart($sparePart)
- ->create();
- // Reserve some items
- Reservation::factory()
- ->active()
- ->withQuantity(5)
- ->withDocuments(true)
- ->fromOrder($orderWithDocs)
- ->forReclamation($reclamation)
- ->create();
- // Act
- $info = $this->service->getStockInfo($sparePart->fresh());
- // Assert
- $this->assertEquals($sparePart->id, $info['spare_part_id']);
- $this->assertEquals($sparePart->article, $info['article']);
- // Physical: 20 with docs + 15 without = 35 total
- $this->assertEquals(15, $info['physical_without_docs']);
- $this->assertEquals(20, $info['physical_with_docs']);
- $this->assertEquals(35, $info['physical_total']);
- // Reserved: 5 with docs
- $this->assertEquals(0, $info['reserved_without_docs']);
- $this->assertEquals(5, $info['reserved_with_docs']);
- $this->assertEquals(5, $info['reserved_total']);
- // Free: 35 - 5 = 30
- $this->assertEquals(15, $info['free_without_docs']);
- $this->assertEquals(15, $info['free_with_docs']); // 20 - 5
- $this->assertEquals(30, $info['free_total']);
- $this->assertEquals(10, $info['min_stock']);
- $this->assertFalse($info['below_min_stock']); // 30 > 10
- }
- // ==================== getInventorySummary ====================
- public function test_get_inventory_summary_returns_aggregated_data(): void
- {
- // Arrange
- $sparePart1 = SparePart::factory()->create(['min_stock' => 100]); // will be below min
- $sparePart2 = SparePart::factory()->create(['min_stock' => 5]);
- $reclamation = Reclamation::factory()->create();
- SparePartOrder::factory()
- ->inStock()
- ->withQuantity(10)
- ->forSparePart($sparePart1)
- ->create();
- $order2 = SparePartOrder::factory()
- ->inStock()
- ->withQuantity(20)
- ->forSparePart($sparePart2)
- ->create();
- // Create shortage for sparePart1
- Shortage::factory()
- ->open()
- ->withQuantities(5, 0)
- ->forSparePart($sparePart1)
- ->forReclamation($reclamation)
- ->create();
- // Create reservation for sparePart2
- Reservation::factory()
- ->active()
- ->withQuantity(3)
- ->fromOrder($order2)
- ->forReclamation($reclamation)
- ->create();
- // Act
- $summary = $this->service->getInventorySummary();
- // Assert
- $this->assertEquals(2, $summary['total_spare_parts']);
- $this->assertEquals(1, $summary['critical_shortages_count']);
- $this->assertEquals(1, $summary['below_min_stock_count']);
- $this->assertEquals(30, $summary['total_physical_stock']); // 10 + 20
- $this->assertEquals(3, $summary['total_reserved']);
- $this->assertEquals(27, $summary['total_free']); // 30 - 3
- }
- // ==================== getReservationsForReclamation ====================
- public function test_get_reservations_for_reclamation_returns_active_reservations(): void
- {
- // Arrange
- $sparePart = SparePart::factory()->create();
- $reclamation = Reclamation::factory()->create();
- $order = SparePartOrder::factory()
- ->inStock()
- ->forSparePart($sparePart)
- ->create();
- Reservation::factory()
- ->active()
- ->withQuantity(5)
- ->fromOrder($order)
- ->forReclamation($reclamation)
- ->create();
- Reservation::factory()
- ->cancelled()
- ->fromOrder($order)
- ->forReclamation($reclamation)
- ->create();
- // Act
- $reservations = $this->service->getReservationsForReclamation($reclamation->id);
- // Assert
- $this->assertCount(1, $reservations);
- $this->assertEquals(5, $reservations->first()->reserved_qty);
- }
- // ==================== getShortagesForReclamation ====================
- public function test_get_shortages_for_reclamation_returns_all_shortages(): void
- {
- // Arrange
- $sparePart1 = SparePart::factory()->create();
- $sparePart2 = SparePart::factory()->create();
- $reclamation = Reclamation::factory()->create();
- $otherReclamation = Reclamation::factory()->create();
- Shortage::factory()
- ->open()
- ->forSparePart($sparePart1)
- ->forReclamation($reclamation)
- ->create();
- Shortage::factory()
- ->closed()
- ->forSparePart($sparePart2)
- ->forReclamation($reclamation)
- ->create();
- Shortage::factory()
- ->open()
- ->forSparePart($sparePart1)
- ->forReclamation($otherReclamation)
- ->create();
- // Act
- $shortages = $this->service->getShortagesForReclamation($reclamation->id);
- // Assert
- $this->assertCount(2, $shortages);
- }
- // ==================== Deprecated methods ====================
- public function test_reserve_for_reclamation_delegates_to_reservation_service(): void
- {
- // Arrange
- $sparePart = SparePart::factory()->create(['article' => 'TEST-001']);
- $reclamation = Reclamation::factory()->create();
- SparePartOrder::factory()
- ->inStock()
- ->withDocuments(false)
- ->withQuantity(10)
- ->forSparePart($sparePart)
- ->create();
- // Act
- $result = $this->service->reserveForReclamation(
- 'TEST-001',
- 5,
- false,
- $reclamation->id
- );
- // Assert
- $this->assertEquals(5, $result->reserved);
- $this->assertEquals(0, $result->missing);
- }
- public function test_reserve_for_reclamation_returns_zero_for_unknown_article(): void
- {
- // Arrange
- $reclamation = Reclamation::factory()->create();
- // Act
- $result = $this->service->reserveForReclamation(
- 'UNKNOWN-ARTICLE',
- 5,
- false,
- $reclamation->id
- );
- // Assert
- $this->assertEquals(0, $result->reserved);
- $this->assertEquals(5, $result->missing);
- }
- }
|