create(); $reclamation = Reclamation::factory()->create(); // Create open shortage $shortage = Shortage::factory() ->open() ->withQuantities(5, 0) // required=5, reserved=0, missing=5 ->forSparePart($sparePart) ->forReclamation($reclamation) ->withDocuments(false) ->create(); // Act - create order with in_stock status (observer should trigger) $order = SparePartOrder::factory() ->inStock() ->withDocuments(false) ->withQuantity(10) ->forSparePart($sparePart) ->create(); // Assert $shortage->refresh(); // Shortage should be covered (status=closed when fully covered) $this->assertEquals(5, $shortage->reserved_qty); $this->assertEquals(0, $shortage->missing_qty); $this->assertEquals(Shortage::STATUS_CLOSED, $shortage->status); // Reservation should be created $this->assertDatabaseHas('reservations', [ 'spare_part_id' => $sparePart->id, 'spare_part_order_id' => $order->id, 'reclamation_id' => $reclamation->id, 'reserved_qty' => 5, 'status' => Reservation::STATUS_ACTIVE, ]); } public function test_creating_order_with_ordered_status_does_not_trigger_shortage_coverage(): void { // Arrange $sparePart = SparePart::factory()->create(); $reclamation = Reclamation::factory()->create(); Shortage::factory() ->open() ->withQuantities(5, 0) ->forSparePart($sparePart) ->forReclamation($reclamation) ->withDocuments(false) ->create(); // Act - create order with ordered status SparePartOrder::factory() ->ordered() ->withDocuments(false) ->withQuantity(10) ->forSparePart($sparePart) ->create(); // Assert - shortage should remain open $this->assertDatabaseHas('shortages', [ 'spare_part_id' => $sparePart->id, 'reclamation_id' => $reclamation->id, 'status' => Shortage::STATUS_OPEN, 'missing_qty' => 5, ]); } public function test_updating_order_status_to_in_stock_covers_shortages(): void { // Arrange $sparePart = SparePart::factory()->create(); $reclamation = Reclamation::factory()->create(); Shortage::factory() ->open() ->withQuantities(3, 0) ->forSparePart($sparePart) ->forReclamation($reclamation) ->withDocuments(true) ->create(); // Create order with ordered status (not in_stock) $order = SparePartOrder::factory() ->ordered() ->withDocuments(true) ->withQuantity(10) ->forSparePart($sparePart) ->create(); // Act - update status to in_stock $order->update(['status' => SparePartOrder::STATUS_IN_STOCK]); // Assert $this->assertDatabaseHas('shortages', [ 'spare_part_id' => $sparePart->id, 'reclamation_id' => $reclamation->id, 'status' => Shortage::STATUS_CLOSED, 'reserved_qty' => 3, 'missing_qty' => 0, ]); } public function test_updating_other_fields_does_not_trigger_shortage_coverage(): void { // Arrange $sparePart = SparePart::factory()->create(); $reclamation = Reclamation::factory()->create(); Shortage::factory() ->open() ->withQuantities(5, 0) ->forSparePart($sparePart) ->forReclamation($reclamation) ->withDocuments(false) ->create(); $order = SparePartOrder::factory() ->inStock() ->withDocuments(false) ->withQuantity(10) ->forSparePart($sparePart) ->create(); // Clear the shortage that was covered on creation Shortage::query()->update([ 'status' => Shortage::STATUS_OPEN, 'reserved_qty' => 0, 'missing_qty' => 5, ]); Reservation::query()->delete(); // Act - update note (not status) $order->update(['note' => 'Updated note']); // Assert - shortage should remain open (status didn't change) $this->assertDatabaseHas('shortages', [ 'spare_part_id' => $sparePart->id, 'status' => Shortage::STATUS_OPEN, 'missing_qty' => 5, ]); } public function test_observer_respects_with_documents_flag(): void { // Arrange $sparePart = SparePart::factory()->create(); $reclamation = Reclamation::factory()->create(); // Create shortage requiring documents Shortage::factory() ->open() ->withQuantities(5, 0) ->forSparePart($sparePart) ->forReclamation($reclamation) ->withDocuments(true) ->create(); // Act - create order WITHOUT documents SparePartOrder::factory() ->inStock() ->withDocuments(false) // Different from shortage ->withQuantity(10) ->forSparePart($sparePart) ->create(); // Assert - shortage should remain open (documents mismatch) $this->assertDatabaseHas('shortages', [ 'spare_part_id' => $sparePart->id, 'with_documents' => true, 'status' => Shortage::STATUS_OPEN, 'missing_qty' => 5, ]); } public function test_observer_covers_multiple_shortages_fifo(): void { // Arrange $sparePart = SparePart::factory()->create(); $reclamation1 = Reclamation::factory()->create(); $reclamation2 = Reclamation::factory()->create(); // Create older shortage $shortage1 = Shortage::factory() ->open() ->withQuantities(3, 0) ->forSparePart($sparePart) ->forReclamation($reclamation1) ->withDocuments(false) ->create(['created_at' => now()->subDays(2)]); // Create newer shortage $shortage2 = Shortage::factory() ->open() ->withQuantities(4, 0) ->forSparePart($sparePart) ->forReclamation($reclamation2) ->withDocuments(false) ->create(['created_at' => now()->subDay()]); // Act - create order with 5 items (should cover first fully, second partially) SparePartOrder::factory() ->inStock() ->withDocuments(false) ->withQuantity(5) ->forSparePart($sparePart) ->create(); // Assert $shortage1->refresh(); $shortage2->refresh(); // First shortage fully covered (closed) $this->assertEquals(Shortage::STATUS_CLOSED, $shortage1->status); $this->assertEquals(3, $shortage1->reserved_qty); // Second shortage partially covered $this->assertEquals(Shortage::STATUS_OPEN, $shortage2->status); $this->assertEquals(2, $shortage2->reserved_qty); $this->assertEquals(2, $shortage2->missing_qty); } public function test_observer_does_not_cover_already_covered_shortages(): void { // Arrange $sparePart = SparePart::factory()->create(); $reclamation = Reclamation::factory()->create(); // Create already covered shortage Shortage::factory() ->covered() ->withQuantities(5, 5) // fully covered ->forSparePart($sparePart) ->forReclamation($reclamation) ->withDocuments(false) ->create(); // Act $order = SparePartOrder::factory() ->inStock() ->withDocuments(false) ->withQuantity(10) ->forSparePart($sparePart) ->create(); // Assert - no new reservations for already covered shortage $this->assertEquals(0, Reservation::where('spare_part_order_id', $order->id)->count()); } }