From 36c1c185ee118c516248cd2d33493a7ec5cba335 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Wed, 12 Aug 2026 11:09:11 -0300 Subject: [PATCH] feat(layouts): add 'Single' layout option to GroupLayout and update related functionality --- app/Domains/Catalog/Enums/GroupLayout.php | 1 + app/Domains/Catalog/Enums/ProductLayout.php | 1 + .../Resources/CatalogFeaturedItemResource.php | 25 ++++++--- .../Catalog/Services/FeaturedGroupService.php | 8 ++- ...et_selector_and_single_catalog_layouts.php | 53 +++++++++++++++++++ .../Feature/Catalog/CatalogControllerTest.php | 26 +++++++++ tests/Unit/Catalog/CatalogModelsTest.php | 11 ++++ 7 files changed, 117 insertions(+), 8 deletions(-) create mode 100644 database/migrations/2026_08_12_010000_add_ticket_selector_and_single_catalog_layouts.php diff --git a/app/Domains/Catalog/Enums/GroupLayout.php b/app/Domains/Catalog/Enums/GroupLayout.php index f78f6c9..3f766ad 100644 --- a/app/Domains/Catalog/Enums/GroupLayout.php +++ b/app/Domains/Catalog/Enums/GroupLayout.php @@ -8,6 +8,7 @@ enum GroupLayout: string case Simple = 'simple'; case SimpleVertical = 'simple_vertical'; case Carousel = 'carousel'; + case Single = 'single'; /** * @return list diff --git a/app/Domains/Catalog/Enums/ProductLayout.php b/app/Domains/Catalog/Enums/ProductLayout.php index 4a34e49..61fae9d 100644 --- a/app/Domains/Catalog/Enums/ProductLayout.php +++ b/app/Domains/Catalog/Enums/ProductLayout.php @@ -7,6 +7,7 @@ enum ProductLayout: string case Row = 'row'; case ColumnWithImage = 'column_with_image'; case ColumnWithCart = 'column_with_cart'; + case TicketSelector = 'ticket_selector'; /** * @return list diff --git a/app/Domains/Catalog/Resources/CatalogFeaturedItemResource.php b/app/Domains/Catalog/Resources/CatalogFeaturedItemResource.php index c303174..f528bf2 100644 --- a/app/Domains/Catalog/Resources/CatalogFeaturedItemResource.php +++ b/app/Domains/Catalog/Resources/CatalogFeaturedItemResource.php @@ -25,7 +25,7 @@ class CatalogFeaturedItemResource extends JsonResource return $this->columnWithImageData($catalogItem); } - return [ + $data = [ 'id' => $catalogItem->id, 'type' => $catalogItem->type->value, 'nombre' => $catalogItem->nombre, @@ -51,16 +51,17 @@ class CatalogFeaturedItemResource extends JsonResource ]) ->values(), ]; + + if ($featuredGroup->product_layout === ProductLayout::TicketSelector) { + $data['image'] = $this->firstImageUrl($catalogItem); + } + + return $data; } /** @return array */ private function columnWithImageData(CatalogItem $catalogItem): array { - $attachment = $catalogItem->attachments->first() - ?? $catalogItem->variants - ->flatMap(fn (Variant $variant) => $variant->attachments) - ->first(); - return [ 'id' => $catalogItem->id, 'type' => $catalogItem->type->value, @@ -69,7 +70,17 @@ class CatalogFeaturedItemResource extends JsonResource 'ticket_generation_policy' => $catalogItem->ticket_generation_policy->value, 'validity_time_id' => $catalogItem->validity_time_id, 'validity_time' => ValidityTimeResource::make($catalogItem->validityTime), - 'image' => $attachment?->getTemporaryUrl(1440), + 'image' => $this->firstImageUrl($catalogItem), ]; } + + private function firstImageUrl(CatalogItem $catalogItem): ?string + { + $attachment = $catalogItem->attachments->first() + ?? $catalogItem->variants + ->flatMap(fn (Variant $variant) => $variant->attachments) + ->first(); + + return $attachment?->getTemporaryUrl(1440); + } } diff --git a/app/Domains/Catalog/Services/FeaturedGroupService.php b/app/Domains/Catalog/Services/FeaturedGroupService.php index a7fe410..00b21e4 100644 --- a/app/Domains/Catalog/Services/FeaturedGroupService.php +++ b/app/Domains/Catalog/Services/FeaturedGroupService.php @@ -18,7 +18,13 @@ class FeaturedGroupService public function itemsResponse(FeaturedGroup $featuredGroup, int $page): array { if ($featuredGroup->group_layout !== GroupLayout::Paginated) { - $items = $this->itemsQuery($featuredGroup)->get(); + $query = $this->itemsQuery($featuredGroup); + + if ($featuredGroup->group_layout === GroupLayout::Single) { + $query->limit(1); + } + + $items = $query->get(); $this->attachGroup($items, $featuredGroup); return CatalogFeaturedItemResource::collection($items)->resolve(); diff --git a/database/migrations/2026_08_12_010000_add_ticket_selector_and_single_catalog_layouts.php b/database/migrations/2026_08_12_010000_add_ticket_selector_and_single_catalog_layouts.php new file mode 100644 index 0000000..3c659d9 --- /dev/null +++ b/database/migrations/2026_08_12_010000_add_ticket_selector_and_single_catalog_layouts.php @@ -0,0 +1,53 @@ +alterEnums(ProductLayout::values(), GroupLayout::values()); + } + + public function down(): void + { + if (! in_array(DB::getDriverName(), ['mysql', 'mariadb'], true)) { + return; + } + + $this->alterEnums( + ['row', 'column_with_image', 'column_with_cart'], + ['paginated', 'simple', 'simple_vertical', 'carousel'], + ); + } + + /** + * @param list $productLayouts + * @param list $groupLayouts + */ + private function alterEnums(array $productLayouts, array $groupLayouts): void + { + $products = $this->enumValues($productLayouts); + $groups = $this->enumValues($groupLayouts); + + DB::statement("ALTER TABLE featured_groups MODIFY product_layout ENUM({$products}) NOT NULL"); + DB::statement("ALTER TABLE featured_groups MODIFY group_layout ENUM({$groups}) NOT NULL DEFAULT 'paginated'"); + DB::statement("ALTER TABLE tenants MODIFY search_product_layout ENUM({$products}) NOT NULL DEFAULT 'column_with_image'"); + DB::statement("ALTER TABLE tenants MODIFY search_group_layout ENUM({$groups}) NOT NULL DEFAULT 'paginated'"); + } + + /** @param list $values */ + private function enumValues(array $values): string + { + return collect($values) + ->map(fn (string $value): string => DB::getPdo()->quote($value)) + ->implode(','); + } +}; diff --git a/tests/Feature/Catalog/CatalogControllerTest.php b/tests/Feature/Catalog/CatalogControllerTest.php index 9c12fed..f74c196 100644 --- a/tests/Feature/Catalog/CatalogControllerTest.php +++ b/tests/Feature/Catalog/CatalogControllerTest.php @@ -241,6 +241,32 @@ class CatalogControllerTest extends TestCase ->assertJsonPath('0.nombre', 'carousel Item 1'); } + public function test_single_group_layout_returns_only_its_first_available_item(): void + { + $tenant = $this->createTenant('catalog-single-layout'); + $group = $this->createGroup( + $tenant, + ProductLayout::TicketSelector, + 'Entradas', + groupLayout: GroupLayout::Single, + ); + + foreach (['Primera entrada', 'Segunda entrada'] as $order => $name) { + $item = $this->createItem($tenant, $name); + $group->featuredItems()->create([ + 'catalog_item_id' => $item->id, + 'order' => $order, + ]); + } + + $this->getJson("/api/tenants/{$tenant->codigo}/catalog") + ->assertOk() + ->assertJsonPath('0.layout', ProductLayout::TicketSelector->value) + ->assertJsonPath('0.group_layout', GroupLayout::Single->value) + ->assertJsonCount(1, '0.items') + ->assertJsonPath('0.items.0.nombre', 'Primera entrada'); + } + public function test_groups_can_source_items_from_a_category_or_the_entire_catalog(): void { $tenant = $this->createTenant('catalog-sources'); diff --git a/tests/Unit/Catalog/CatalogModelsTest.php b/tests/Unit/Catalog/CatalogModelsTest.php index 48e5a17..cb69cfb 100644 --- a/tests/Unit/Catalog/CatalogModelsTest.php +++ b/tests/Unit/Catalog/CatalogModelsTest.php @@ -36,9 +36,20 @@ class CatalogModelsTest extends TestCase 'simple', 'simple_vertical', 'carousel', + 'single', ], GroupLayout::values()); } + public function test_product_layout_has_all_supported_values(): void + { + $this->assertSame([ + 'row', + 'column_with_image', + 'column_with_cart', + 'ticket_selector', + ], ProductLayout::values()); + } + public function test_attribute_maps_its_values_and_relations(): void { $attribute = new Attribute;