feat(catalog): add show_in_selector attribute to item attributes and update related logic

This commit is contained in:
2026-08-12 16:11:53 -03:00
parent 8359f0831f
commit 7c7a295625
12 changed files with 115 additions and 1 deletions

View File

@@ -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',
];
}

View File

@@ -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'],

View File

@@ -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,

View File

@@ -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<int, string> $attributeCodes
* @param array<int, string> $multiSelectAttributeCodes
* @param array<int, string> $hiddenAttributeCodes
* @return array<string, ItemAttribute>
*/
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;

View File

@@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::table('item_attributes', function (Blueprint $table): void {
$table->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');
});
}
};

View File

@@ -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(),

View File

@@ -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.',

View File

@@ -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.',

View File

@@ -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,

View File

@@ -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'));
}

View File

@@ -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');

View File

@@ -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(),