call('TestDatabaseBootstrapSeeder'); } protected function setUp(): void { parent::setUp(); $this->item = model(Item::class); } public function testSearchReturnsMatchingItemByName(): void { $uniqueName = 'Findable Widget ' . uniqid(); $this->createSearchableItem([ 'name' => $uniqueName, 'cost_price' => 12.50, 'unit_price' => 24.99, ]); $results = $this->item->search($uniqueName, $this->defaultSearchFilters())->getResult(); $this->assertCount(1, $results); $this->assertEquals($uniqueName, $results[0]->name); $this->assertEquals(12.50, (float) $results[0]->cost_price); $this->assertEquals(24.99, (float) $results[0]->unit_price); } public function testSearchExcludesNonMatchingItems(): void { $matchingName = 'Matching Gadget ' . uniqid(); $this->createSearchableItem(['name' => $matchingName]); $this->createSearchableItem(['name' => 'Unrelated Thing ' . uniqid()]); $results = $this->item->search($matchingName, $this->defaultSearchFilters())->getResult(); $this->assertCount(1, $results); $this->assertEquals($matchingName, $results[0]->name); } public function testGetFoundRowsMatchesActualCount(): void { $sharedCategory = 'Shared Category ' . uniqid(); $this->createSearchableItem(['category' => $sharedCategory]); $this->createSearchableItem(['category' => $sharedCategory]); $this->createSearchableItem(['category' => $sharedCategory]); $filters = $this->defaultSearchFilters(); $foundRows = $this->item->get_found_rows($sharedCategory, $filters); $actualRows = $this->item->search($sharedCategory, $filters)->getResultArray(); $this->assertEquals(3, $foundRows); $this->assertCount($foundRows, $actualRows); } public function testSearchPaginationRowsAndLimitFrom(): void { $sharedCategory = 'Paginated Category ' . uniqid(); $this->createSearchableItem(['name' => 'A Item ' . uniqid(), 'category' => $sharedCategory]); $this->createSearchableItem(['name' => 'B Item ' . uniqid(), 'category' => $sharedCategory]); $this->createSearchableItem(['name' => 'C Item ' . uniqid(), 'category' => $sharedCategory]); $results = $this->item->search( $sharedCategory, $this->defaultSearchFilters(), 1, 1, 'items.name', 'asc' )->getResult(); $this->assertCount(1, $results); $this->assertStringStartsWith('B Item', $results[0]->name); } public function testSearchOrderPreservedAfterPhaseBJoin(): void { $sharedCategory = 'Ordered Category ' . uniqid(); $this->createSearchableItem(['name' => 'Alpha Item ' . uniqid(), 'category' => $sharedCategory]); $this->createSearchableItem(['name' => 'Beta Item ' . uniqid(), 'category' => $sharedCategory]); $this->createSearchableItem(['name' => 'Gamma Item ' . uniqid(), 'category' => $sharedCategory]); $results = $this->item->search( $sharedCategory, $this->defaultSearchFilters(), 0, 0, 'items.name', 'desc' )->getResult(); $names = array_map(static fn ($item) => $item->name, $results); $sorted = $names; rsort($sorted); $this->assertSame($sorted, $names); } public function testSearchNoMatchesReturnsEmptyResultNotError(): void { $results = $this->item->search('no-such-item-' . uniqid(), $this->defaultSearchFilters())->getResult(); $this->assertIsArray($results); $this->assertCount(0, $results); } public function testSearchExcludesDeletedItemsByDefault(): void { $deletedName = 'Deleted Item ' . uniqid(); $this->createSearchableItem(['name' => $deletedName, 'deleted' => 1]); $results = $this->item->search($deletedName, $this->defaultSearchFilters(['is_deleted' => false]))->getResult(); $this->assertCount(0, $results); } public function testSearchIncludesDeletedWhenFilterSet(): void { $deletedName = 'Deleted Item ' . uniqid(); $this->createSearchableItem(['name' => $deletedName, 'deleted' => 1]); $results = $this->item->search($deletedName, $this->defaultSearchFilters(['is_deleted' => true]))->getResult(); $this->assertCount(1, $results); $this->assertEquals($deletedName, $results[0]->name); } public function testSearchStockLocationFilter(): void { $locationAName = 'Location A Item ' . uniqid(); $itemId = $this->createSearchableItem(['name' => $locationAName]); $this->addItemQuantity($itemId, 1, 10); $resultsAtLocation1 = $this->item->search( $locationAName, $this->defaultSearchFilters(['stock_location_id' => 1]) )->getResult(); $resultsAtLocation2 = $this->item->search( $locationAName, $this->defaultSearchFilters(['stock_location_id' => 2]) )->getResult(); $this->assertCount(1, $resultsAtLocation1); $this->assertCount(0, $resultsAtLocation2); } public function testSearchTemporaryFilter(): void { $tempName = 'Temp Item ' . uniqid(); $this->createSearchableItem(['name' => $tempName, 'item_type' => ITEM_TEMP]); $resultsWhenTemporary = $this->item->search( $tempName, $this->defaultSearchFilters(['temporary' => true]) )->getResult(); $resultsWhenNotTemporary = $this->item->search( $tempName, $this->defaultSearchFilters(['temporary' => false]) )->getResult(); $this->assertCount(1, $resultsWhenTemporary); $this->assertCount(0, $resultsWhenNotTemporary); } public function testSearchSortByQuantitySumsAcrossLocationsWithoutMultiplication(): void { $itemId = $this->createSearchableItem(['name' => 'Multi Location Item ' . uniqid()]); // createSearchableItem already inserts one inventory row; add two more // inventory transactions so a naive SUM-after-join (which multiplies // item_quantities rows by the inventory join) would triple-count. $this->addInventoryRecord($itemId, 1); $this->addInventoryRecord($itemId, 1); $this->addItemQuantity($itemId, 1, 10); $this->addItemQuantity($itemId, 2, 5); $results = $this->item->search( '', $this->defaultSearchFilters(), 0, 0, 'quantity', 'desc' )->getResult(); $match = array_values(array_filter($results, static fn ($r) => (int) $r->item_id === $itemId)); $this->assertCount(1, $match); $this->assertEquals(15, (float) $match[0]->quantity); } }