mirror of
https://github.com/opensourcepos/opensourcepos.git
synced 2026-09-14 06:19:44 -04:00
- 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>
221 lines
7.4 KiB
PHP
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);
|
|
}
|
|
}
|