Files
opensourcepos/tests/Models/ItemSearchTest.php
T
objecttothis 9138127a67 fix(items): optimize quantity sum handling and add test to prevent over-counting
- Refactor quantity sum calculation by introducing a subquery to handle SUM without over-counting due to joins.
- Update search logic and related query clauses for consistent sorting by total quantities across locations.
- Add test to verify correct quantity aggregation without duplication.

Signed-off-by: objecttothis <17935339+objecttothis@users.noreply.github.com>
2026-08-21 12:49:51 +04:00

221 lines
7.4 KiB
PHP

<?php
namespace Tests\Models;
use CodeIgniter\Test\CIUnitTestCase;
use CodeIgniter\Test\DatabaseTestTrait;
use App\Models\Item;
use Config\Database;
use Tests\Support\ItemSearchFixtureTrait;
class ItemSearchTest extends CIUnitTestCase
{
use DatabaseTestTrait;
use ItemSearchFixtureTrait;
protected $migrate = true;
protected $migrateOnce = true;
protected $seed = '';
protected $seedOnce = true;
protected $refresh = true;
protected $namespace = null;
protected $item;
public static function setUpBeforeClass(): void
{
$seeder = Database::seeder('tests');
$seeder->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);
$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(10, (float) $match[0]->quantity);
}
}