From cf3f0ceb1b7db0648fa0f490c0d5dd66c134fe7f Mon Sep 17 00:00:00 2001 From: ncoronel Date: Thu, 24 Sep 2026 16:28:15 -0300 Subject: [PATCH] feat(category): include products from all descendant categories in paginated response --- .../Catalog/Services/CatalogService.php | 38 +++++++++++++++++-- tests/Feature/Catalog/CategoryDetailTest.php | 31 ++++++++++++++- 2 files changed, 63 insertions(+), 6 deletions(-) diff --git a/app/Domains/Commerce/Catalog/Services/CatalogService.php b/app/Domains/Commerce/Catalog/Services/CatalogService.php index 61cef22..5d2c028 100644 --- a/app/Domains/Commerce/Catalog/Services/CatalogService.php +++ b/app/Domains/Commerce/Catalog/Services/CatalogService.php @@ -2,8 +2,6 @@ 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\Models\Attribute; 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\ItemAttribute; use App\Domains\Commerce\Catalog\Models\Variant; -use App\Domains\Ticketing\Event\Models\EventDate; 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\Pagination\LengthAwarePaginator; use Illuminate\Support\Facades\DB; @@ -286,9 +286,11 @@ class CatalogService int $perPage, int $page, ): LengthAwarePaginator { + $categoryIds = $this->categoryAndDescendantIds($tenant, $category); + return CatalogItem::query() ->forTenantCatalog($tenant) - ->where('category_id', $category->id) + ->whereIn('category_id', $categoryIds) ->whereAvailable() ->with([ 'attachments', @@ -306,6 +308,34 @@ class CatalogService ->paginate(perPage: $perPage, pageName: 'page', page: $page); } + /** @return array */ + 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 { DB::transaction(function () use ($catalogItem): void { diff --git a/tests/Feature/Catalog/CategoryDetailTest.php b/tests/Feature/Catalog/CategoryDetailTest.php index 0eee7db..ba22be2 100644 --- a/tests/Feature/Catalog/CategoryDetailTest.php +++ b/tests/Feature/Catalog/CategoryDetailTest.php @@ -72,6 +72,29 @@ class CategoryDetailTest extends TestCase ->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 { $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([ 'tenant_code' => $tenant->codigo, + 'categoria_id' => $parent?->id, 'nombre' => $name, ]); }