diff --git a/app/Domains/Catalog/Models/ItemAttribute.php b/app/Domains/Catalog/Models/ItemAttribute.php index 72fc815..a2b6c74 100644 --- a/app/Domains/Catalog/Models/ItemAttribute.php +++ b/app/Domains/Catalog/Models/ItemAttribute.php @@ -13,6 +13,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; 'attribute_id', 'allow_multi_select', 'sort_order', + 'show_in_selector', ])] class ItemAttribute extends Model { @@ -20,6 +21,10 @@ class ItemAttribute extends Model protected $table = 'item_attributes'; + protected $attributes = [ + 'show_in_selector' => true, + ]; + protected function casts(): array { return [ @@ -27,6 +32,7 @@ class ItemAttribute extends Model 'attribute_id' => 'integer', 'allow_multi_select' => 'boolean', 'sort_order' => 'integer', + 'show_in_selector' => 'boolean', ]; } diff --git a/app/Domains/Catalog/Requests/StoreCatalogItemRequest.php b/app/Domains/Catalog/Requests/StoreCatalogItemRequest.php index 6f07e98..bb5f8c2 100644 --- a/app/Domains/Catalog/Requests/StoreCatalogItemRequest.php +++ b/app/Domains/Catalog/Requests/StoreCatalogItemRequest.php @@ -82,6 +82,15 @@ class StoreCatalogItemRequest extends FormRequest fn ($query) => $query->where('tenant_codigo', $tenantCode) ), ], + 'hidden_attribute_codes' => [Rule::prohibitedIf($isBundle), 'sometimes', 'array'], + 'hidden_attribute_codes.*' => [ + 'required', + 'string', + 'distinct', + Rule::exists('attribute', 'codigo')->where( + fn ($query) => $query->where('tenant_codigo', $tenantCode) + ), + ], 'images' => ['sometimes', 'array'], 'images.*' => ['required', new ImageOrBase64Rule], 'variants' => [Rule::prohibitedIf($isBundle), 'sometimes', 'array'], diff --git a/app/Domains/Catalog/Resources/CatalogItemDetailResource.php b/app/Domains/Catalog/Resources/CatalogItemDetailResource.php index 016624f..8e7f17a 100644 --- a/app/Domains/Catalog/Resources/CatalogItemDetailResource.php +++ b/app/Domains/Catalog/Resources/CatalogItemDetailResource.php @@ -89,6 +89,7 @@ class CatalogItemDetailResource extends JsonResource 'codigo' => $attribute->codigo, 'nombre' => $attribute->nombre, 'sort_order' => $itemAttribute->sort_order, + 'show_in_selector' => $itemAttribute->show_in_selector, 'is_required' => $attribute->is_required, 'allow_multi_select' => $itemAttribute->allow_multi_select, 'metadata_schema' => $attribute->metadata_schema, diff --git a/app/Domains/Catalog/Services/CatalogService.php b/app/Domains/Catalog/Services/CatalogService.php index f2edfe3..d8afe2f 100644 --- a/app/Domains/Catalog/Services/CatalogService.php +++ b/app/Domains/Catalog/Services/CatalogService.php @@ -37,6 +37,7 @@ class CatalogService $images = $data['images'] ?? []; $attributeCodes = $data['attribute_codes'] ?? []; $multiSelectAttributeCodes = $data['multi_select_attribute_codes'] ?? []; + $hiddenAttributeCodes = $data['hidden_attribute_codes'] ?? []; $components = $data['components'] ?? []; $hasDirectStock = array_key_exists('real_stock', $data); $realStock = (int) ($data['real_stock'] ?? 0); @@ -64,6 +65,14 @@ class CatalogService ]); } + if (array_diff($hiddenAttributeCodes, $attributeCodes) !== []) { + throw ValidationException::withMessages([ + 'hidden_attribute_codes' => [ + __('api.catalog.hidden_attribute_not_on_item'), + ], + ]); + } + $this->validateUniqueVariantCombinations($variants, $attributeCodes); if ($type === CatalogItemType::Bundle) { @@ -88,6 +97,7 @@ class CatalogService $data['images'], $data['attribute_codes'], $data['multi_select_attribute_codes'], + $data['hidden_attribute_codes'], $data['components'], $data['real_stock'], $data['reserved_stock'], @@ -109,7 +119,12 @@ class CatalogService $catalogItem = CatalogItem::query()->create($data); $itemAttributes = $type === CatalogItemType::Standard - ? $this->createItemAttributes($catalogItem, $attributeCodes, $multiSelectAttributeCodes) + ? $this->createItemAttributes( + $catalogItem, + $attributeCodes, + $multiSelectAttributeCodes, + $hiddenAttributeCodes, + ) : []; if ($type === CatalogItemType::Bundle) { @@ -488,12 +503,14 @@ class CatalogService /** * @param array $attributeCodes * @param array $multiSelectAttributeCodes + * @param array $hiddenAttributeCodes * @return array */ private function createItemAttributes( CatalogItem $catalogItem, array $attributeCodes, array $multiSelectAttributeCodes = [], + array $hiddenAttributeCodes = [], ): array { $itemAttributes = []; $attributeCodes = array_values(array_unique($attributeCodes)); @@ -517,6 +534,7 @@ class CatalogService $itemAttribute = $catalogItem->itemAttributes()->create([ 'attribute_id' => $attribute->id, 'allow_multi_select' => in_array($attributeCode, $multiSelectAttributeCodes, true), + 'show_in_selector' => ! in_array($attributeCode, $hiddenAttributeCodes, true), ]); $itemAttributes[$attributeCode] = $itemAttribute; diff --git a/database/migrations/2026_08_12_030000_add_show_in_selector_to_item_attributes.php b/database/migrations/2026_08_12_030000_add_show_in_selector_to_item_attributes.php new file mode 100644 index 0000000..4d8b2ff --- /dev/null +++ b/database/migrations/2026_08_12_030000_add_show_in_selector_to_item_attributes.php @@ -0,0 +1,35 @@ +boolean('show_in_selector')->default(true)->after('sort_order'); + }); + + $abonoDateAttributeIds = DB::table('item_attributes') + ->join('catalog_items', 'catalog_items.id', '=', 'item_attributes.catalog_item_id') + ->join('attribute', 'attribute.id', '=', 'item_attributes.attribute_id') + ->where('catalog_items.tenant_code', 'fiesta_futbol_infantil') + ->where('catalog_items.slug', 'abono') + ->where('attribute.codigo', 'event_date') + ->pluck('item_attributes.id'); + + DB::table('item_attributes') + ->whereIn('id', $abonoDateAttributeIds) + ->update(['show_in_selector' => false]); + } + + public function down(): void + { + Schema::table('item_attributes', function (Blueprint $table): void { + $table->dropColumn('show_in_selector'); + }); + } +}; diff --git a/database/seeders/FiestaFutbolInfantilProductSeeder.php b/database/seeders/FiestaFutbolInfantilProductSeeder.php index f6c70dd..1095fac 100644 --- a/database/seeders/FiestaFutbolInfantilProductSeeder.php +++ b/database/seeders/FiestaFutbolInfantilProductSeeder.php @@ -132,6 +132,7 @@ class FiestaFutbolInfantilProductSeeder extends Seeder 'has_tickets' => true, 'attribute_codes' => ['event_date'], 'multi_select_attribute_codes' => ['event_date'], + 'hidden_attribute_codes' => ['event_date'], 'variants' => [[ 'real_stock' => 120, 'event_date_ids' => $dateIds->all(), diff --git a/lang/en/api.php b/lang/en/api.php index e4ad48d..9eeb9bc 100644 --- a/lang/en/api.php +++ b/lang/en/api.php @@ -111,6 +111,7 @@ return [ 'direct_inventory_forbidden' => 'An item with variants cannot have direct inventory.', 'event_date_attribute_required' => 'The event_date attribute is required for event date variants.', 'multi_select_attribute_not_on_item' => 'Multi-select attributes must also be present in attribute_codes.', + 'hidden_attribute_not_on_item' => 'Hidden attributes must also be present in attribute_codes.', 'event_date_selection_required' => 'At least one event date must be selected.', 'single_event_date_required' => 'Exactly one event date must be selected.', 'event_date_wrong_tenant' => 'Every event date must belong to the catalog item tenant.', diff --git a/lang/es/api.php b/lang/es/api.php index 5727f87..a4722e9 100644 --- a/lang/es/api.php +++ b/lang/es/api.php @@ -111,6 +111,7 @@ return [ 'direct_inventory_forbidden' => 'Un ítem con variantes no puede tener inventario directo.', 'event_date_attribute_required' => 'El atributo event_date es obligatorio para las variantes con fecha de evento.', 'multi_select_attribute_not_on_item' => 'Los atributos multiselección también deben estar incluidos en attribute_codes.', + 'hidden_attribute_not_on_item' => 'Los atributos ocultos también deben estar incluidos en attribute_codes.', 'event_date_selection_required' => 'Debe seleccionar al menos una fecha de evento.', 'single_event_date_required' => 'Debe seleccionar exactamente una fecha de evento.', 'event_date_wrong_tenant' => 'Todas las fechas del evento deben pertenecer al tenant del ítem de catálogo.', diff --git a/tests/Feature/Catalog/CatalogItemDetailControllerTest.php b/tests/Feature/Catalog/CatalogItemDetailControllerTest.php index 18e9c28..33d5a7a 100644 --- a/tests/Feature/Catalog/CatalogItemDetailControllerTest.php +++ b/tests/Feature/Catalog/CatalogItemDetailControllerTest.php @@ -276,6 +276,26 @@ class CatalogItemDetailControllerTest extends TestCase ); } + public function test_it_exposes_whether_an_item_attribute_should_be_shown_in_the_selector(): void + { + $tenant = $this->createTenant('detail-hidden-attribute'); + $item = $this->createItem($tenant, 'Hidden attribute'); + $attribute = Attribute::query()->create([ + 'tenant_codigo' => $tenant->codigo, + 'codigo' => 'internal_type', + 'nombre' => 'Internal type', + 'type' => FieldType::String, + ]); + $item->itemAttributes()->create([ + 'attribute_id' => $attribute->id, + 'show_in_selector' => false, + ]); + + $this->getJson("/api/tenants/{$tenant->codigo}/catalog-items/{$item->id}") + ->assertOk() + ->assertJsonPath('data.attributes.0.show_in_selector', false); + } + private function createItem( Tenant $tenant, string $name, diff --git a/tests/Feature/Catalog/CatalogSchemaTest.php b/tests/Feature/Catalog/CatalogSchemaTest.php index 28a01d8..eb22b76 100644 --- a/tests/Feature/Catalog/CatalogSchemaTest.php +++ b/tests/Feature/Catalog/CatalogSchemaTest.php @@ -25,6 +25,7 @@ class CatalogSchemaTest extends TestCase $this->assertTrue(Schema::hasTable('variantes')); $this->assertTrue(Schema::hasTable('item_attributes')); $this->assertTrue(Schema::hasColumn('item_attributes', 'sort_order')); + $this->assertTrue(Schema::hasColumn('item_attributes', 'show_in_selector')); $this->assertTrue(Schema::hasTable('variant_values')); } diff --git a/tests/Feature/Catalog/CatalogServiceTest.php b/tests/Feature/Catalog/CatalogServiceTest.php index e671c98..b984854 100644 --- a/tests/Feature/Catalog/CatalogServiceTest.php +++ b/tests/Feature/Catalog/CatalogServiceTest.php @@ -93,6 +93,26 @@ class CatalogServiceTest extends TestCase } } + public function test_it_can_hide_an_item_attribute_from_the_product_selector(): void + { + $attribute = $this->createAttribute('internal_type'); + + $item = $this->service->create([ + 'tenant_code' => $this->tenant->codigo, + 'slug' => 'hidden-attribute-item', + 'nombre' => 'Hidden attribute item', + 'precio' => 100, + 'attribute_codes' => [$attribute->codigo], + 'hidden_attribute_codes' => [$attribute->codigo], + 'variants' => [[ + 'real_stock' => 5, + 'values' => [$attribute->codigo => 'internal'], + ]], + ]); + + $this->assertFalse($item->itemAttributes->sole()->show_in_selector); + } + public function test_it_allows_the_same_event_date_with_different_attribute_values(): void { $sector = $this->createAttribute('sector'); diff --git a/tests/Feature/Seeders/FiestaFutbolInfantilProductSeederTest.php b/tests/Feature/Seeders/FiestaFutbolInfantilProductSeederTest.php index 4fb1e96..f083750 100644 --- a/tests/Feature/Seeders/FiestaFutbolInfantilProductSeederTest.php +++ b/tests/Feature/Seeders/FiestaFutbolInfantilProductSeederTest.php @@ -128,6 +128,7 @@ class FiestaFutbolInfantilProductSeederTest extends TestCase ->sole(); $dateAttribute = $abono->itemAttributes->firstWhere('attribute.codigo', 'event_date'); $this->assertTrue($dateAttribute->allow_multi_select); + $this->assertFalse($dateAttribute->show_in_selector); $this->assertEqualsCanonicalizing( [4], $abono->variants->map(fn ($variant): int => $variant->eventDates->count())->all(),