feat(category): include products from all descendant categories in paginated response

This commit is contained in:
2026-09-24 16:28:15 -03:00
parent 41c0902b83
commit cf3f0ceb1b
2 changed files with 63 additions and 6 deletions

View File

@@ -2,8 +2,6 @@
namespace App\Domains\Commerce\Catalog\Services; namespace App\Domains\Commerce\Catalog\Services;
use App\Shared\Attachable\Models\Attachment;
use App\Shared\Attachable\Services\AttachmentService;
use App\Domains\Commerce\Catalog\Enums\CatalogItemType; use App\Domains\Commerce\Catalog\Enums\CatalogItemType;
use App\Domains\Commerce\Catalog\Models\Attribute; use App\Domains\Commerce\Catalog\Models\Attribute;
use App\Domains\Commerce\Catalog\Models\CatalogItem; use App\Domains\Commerce\Catalog\Models\CatalogItem;
@@ -11,8 +9,10 @@ use App\Domains\Commerce\Catalog\Models\Category;
use App\Domains\Commerce\Catalog\Models\Inventory; use App\Domains\Commerce\Catalog\Models\Inventory;
use App\Domains\Commerce\Catalog\Models\ItemAttribute; use App\Domains\Commerce\Catalog\Models\ItemAttribute;
use App\Domains\Commerce\Catalog\Models\Variant; use App\Domains\Commerce\Catalog\Models\Variant;
use App\Domains\Ticketing\Event\Models\EventDate;
use App\Domains\Core\Tenant\Models\Tenant; use App\Domains\Core\Tenant\Models\Tenant;
use App\Domains\Ticketing\Event\Models\EventDate;
use App\Shared\Attachable\Models\Attachment;
use App\Shared\Attachable\Services\AttachmentService;
use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Builder;
use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\LengthAwarePaginator;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
@@ -286,9 +286,11 @@ class CatalogService
int $perPage, int $perPage,
int $page, int $page,
): LengthAwarePaginator { ): LengthAwarePaginator {
$categoryIds = $this->categoryAndDescendantIds($tenant, $category);
return CatalogItem::query() return CatalogItem::query()
->forTenantCatalog($tenant) ->forTenantCatalog($tenant)
->where('category_id', $category->id) ->whereIn('category_id', $categoryIds)
->whereAvailable() ->whereAvailable()
->with([ ->with([
'attachments', 'attachments',
@@ -306,6 +308,34 @@ class CatalogService
->paginate(perPage: $perPage, pageName: 'page', page: $page); ->paginate(perPage: $perPage, pageName: 'page', page: $page);
} }
/** @return array<int, int> */
private function categoryAndDescendantIds(Tenant $tenant, Category $category): array
{
$childrenByParent = Category::query()
->where('tenant_code', $tenant->codigo)
->get(['id', 'categoria_id'])
->groupBy('categoria_id');
$categoryIds = [];
$pendingIds = [$category->id];
while ($pendingIds !== []) {
$categoryId = array_shift($pendingIds);
if (isset($categoryIds[$categoryId])) {
continue;
}
$categoryIds[$categoryId] = $categoryId;
foreach ($childrenByParent->get($categoryId, collect()) as $child) {
$pendingIds[] = $child->id;
}
}
return array_values($categoryIds);
}
public function delete(CatalogItem $catalogItem): void public function delete(CatalogItem $catalogItem): void
{ {
DB::transaction(function () use ($catalogItem): void { DB::transaction(function () use ($catalogItem): void {

View File

@@ -72,6 +72,29 @@ class CategoryDetailTest extends TestCase
->assertJsonCount(0, 'data'); ->assertJsonCount(0, 'data');
} }
public function test_it_includes_products_from_all_descendant_categories(): void
{
$tenant = $this->createTenant('category-descendants');
$parent = $this->createCategory($tenant, 'Indumentaria');
$child = $this->createCategory($tenant, 'Remeras', $parent);
$grandchild = $this->createCategory($tenant, 'Manga corta', $child);
$sibling = $this->createCategory($tenant, 'Pantalones');
$parentItem = $this->createCatalogItem($tenant, $parent, 'Producto padre');
$childItem = $this->createCatalogItem($tenant, $child, 'Producto hijo');
$grandchildItem = $this->createCatalogItem($tenant, $grandchild, 'Producto nieto');
$this->createCatalogItem($tenant, $sibling, 'Producto ajeno');
$this->getJson("/api/tenants/{$tenant->codigo}/categories/{$parent->id}")
->assertOk()
->assertJsonPath('meta.total', 3)
->assertJsonCount(3, 'data')
->assertJsonPath('data.0.id', $childItem->id)
->assertJsonPath('data.1.id', $grandchildItem->id)
->assertJsonPath('data.2.id', $parentItem->id)
->assertJsonMissing(['nombre' => 'Producto ajeno']);
}
public function test_it_rejects_categories_from_another_tenant(): void public function test_it_rejects_categories_from_another_tenant(): void
{ {
$tenant = $this->createTenant('category-owner'); $tenant = $this->createTenant('category-owner');
@@ -107,10 +130,14 @@ class CategoryDetailTest extends TestCase
]); ]);
} }
private function createCategory(Tenant $tenant, string $name): Category private function createCategory(
{ Tenant $tenant,
string $name,
?Category $parent = null,
): Category {
return Category::query()->create([ return Category::query()->create([
'tenant_code' => $tenant->codigo, 'tenant_code' => $tenant->codigo,
'categoria_id' => $parent?->id,
'nombre' => $name, 'nombre' => $name,
]); ]);
} }