feat(catalog): add show_in_selector attribute to item attributes and update related logic
This commit is contained in:
@@ -13,6 +13,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
|||||||
'attribute_id',
|
'attribute_id',
|
||||||
'allow_multi_select',
|
'allow_multi_select',
|
||||||
'sort_order',
|
'sort_order',
|
||||||
|
'show_in_selector',
|
||||||
])]
|
])]
|
||||||
class ItemAttribute extends Model
|
class ItemAttribute extends Model
|
||||||
{
|
{
|
||||||
@@ -20,6 +21,10 @@ class ItemAttribute extends Model
|
|||||||
|
|
||||||
protected $table = 'item_attributes';
|
protected $table = 'item_attributes';
|
||||||
|
|
||||||
|
protected $attributes = [
|
||||||
|
'show_in_selector' => true,
|
||||||
|
];
|
||||||
|
|
||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
@@ -27,6 +32,7 @@ class ItemAttribute extends Model
|
|||||||
'attribute_id' => 'integer',
|
'attribute_id' => 'integer',
|
||||||
'allow_multi_select' => 'boolean',
|
'allow_multi_select' => 'boolean',
|
||||||
'sort_order' => 'integer',
|
'sort_order' => 'integer',
|
||||||
|
'show_in_selector' => 'boolean',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -82,6 +82,15 @@ class StoreCatalogItemRequest extends FormRequest
|
|||||||
fn ($query) => $query->where('tenant_codigo', $tenantCode)
|
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' => ['sometimes', 'array'],
|
||||||
'images.*' => ['required', new ImageOrBase64Rule],
|
'images.*' => ['required', new ImageOrBase64Rule],
|
||||||
'variants' => [Rule::prohibitedIf($isBundle), 'sometimes', 'array'],
|
'variants' => [Rule::prohibitedIf($isBundle), 'sometimes', 'array'],
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ class CatalogItemDetailResource extends JsonResource
|
|||||||
'codigo' => $attribute->codigo,
|
'codigo' => $attribute->codigo,
|
||||||
'nombre' => $attribute->nombre,
|
'nombre' => $attribute->nombre,
|
||||||
'sort_order' => $itemAttribute->sort_order,
|
'sort_order' => $itemAttribute->sort_order,
|
||||||
|
'show_in_selector' => $itemAttribute->show_in_selector,
|
||||||
'is_required' => $attribute->is_required,
|
'is_required' => $attribute->is_required,
|
||||||
'allow_multi_select' => $itemAttribute->allow_multi_select,
|
'allow_multi_select' => $itemAttribute->allow_multi_select,
|
||||||
'metadata_schema' => $attribute->metadata_schema,
|
'metadata_schema' => $attribute->metadata_schema,
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ class CatalogService
|
|||||||
$images = $data['images'] ?? [];
|
$images = $data['images'] ?? [];
|
||||||
$attributeCodes = $data['attribute_codes'] ?? [];
|
$attributeCodes = $data['attribute_codes'] ?? [];
|
||||||
$multiSelectAttributeCodes = $data['multi_select_attribute_codes'] ?? [];
|
$multiSelectAttributeCodes = $data['multi_select_attribute_codes'] ?? [];
|
||||||
|
$hiddenAttributeCodes = $data['hidden_attribute_codes'] ?? [];
|
||||||
$components = $data['components'] ?? [];
|
$components = $data['components'] ?? [];
|
||||||
$hasDirectStock = array_key_exists('real_stock', $data);
|
$hasDirectStock = array_key_exists('real_stock', $data);
|
||||||
$realStock = (int) ($data['real_stock'] ?? 0);
|
$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);
|
$this->validateUniqueVariantCombinations($variants, $attributeCodes);
|
||||||
|
|
||||||
if ($type === CatalogItemType::Bundle) {
|
if ($type === CatalogItemType::Bundle) {
|
||||||
@@ -88,6 +97,7 @@ class CatalogService
|
|||||||
$data['images'],
|
$data['images'],
|
||||||
$data['attribute_codes'],
|
$data['attribute_codes'],
|
||||||
$data['multi_select_attribute_codes'],
|
$data['multi_select_attribute_codes'],
|
||||||
|
$data['hidden_attribute_codes'],
|
||||||
$data['components'],
|
$data['components'],
|
||||||
$data['real_stock'],
|
$data['real_stock'],
|
||||||
$data['reserved_stock'],
|
$data['reserved_stock'],
|
||||||
@@ -109,7 +119,12 @@ class CatalogService
|
|||||||
|
|
||||||
$catalogItem = CatalogItem::query()->create($data);
|
$catalogItem = CatalogItem::query()->create($data);
|
||||||
$itemAttributes = $type === CatalogItemType::Standard
|
$itemAttributes = $type === CatalogItemType::Standard
|
||||||
? $this->createItemAttributes($catalogItem, $attributeCodes, $multiSelectAttributeCodes)
|
? $this->createItemAttributes(
|
||||||
|
$catalogItem,
|
||||||
|
$attributeCodes,
|
||||||
|
$multiSelectAttributeCodes,
|
||||||
|
$hiddenAttributeCodes,
|
||||||
|
)
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
if ($type === CatalogItemType::Bundle) {
|
if ($type === CatalogItemType::Bundle) {
|
||||||
@@ -488,12 +503,14 @@ class CatalogService
|
|||||||
/**
|
/**
|
||||||
* @param array<int, string> $attributeCodes
|
* @param array<int, string> $attributeCodes
|
||||||
* @param array<int, string> $multiSelectAttributeCodes
|
* @param array<int, string> $multiSelectAttributeCodes
|
||||||
|
* @param array<int, string> $hiddenAttributeCodes
|
||||||
* @return array<string, ItemAttribute>
|
* @return array<string, ItemAttribute>
|
||||||
*/
|
*/
|
||||||
private function createItemAttributes(
|
private function createItemAttributes(
|
||||||
CatalogItem $catalogItem,
|
CatalogItem $catalogItem,
|
||||||
array $attributeCodes,
|
array $attributeCodes,
|
||||||
array $multiSelectAttributeCodes = [],
|
array $multiSelectAttributeCodes = [],
|
||||||
|
array $hiddenAttributeCodes = [],
|
||||||
): array {
|
): array {
|
||||||
$itemAttributes = [];
|
$itemAttributes = [];
|
||||||
$attributeCodes = array_values(array_unique($attributeCodes));
|
$attributeCodes = array_values(array_unique($attributeCodes));
|
||||||
@@ -517,6 +534,7 @@ class CatalogService
|
|||||||
$itemAttribute = $catalogItem->itemAttributes()->create([
|
$itemAttribute = $catalogItem->itemAttributes()->create([
|
||||||
'attribute_id' => $attribute->id,
|
'attribute_id' => $attribute->id,
|
||||||
'allow_multi_select' => in_array($attributeCode, $multiSelectAttributeCodes, true),
|
'allow_multi_select' => in_array($attributeCode, $multiSelectAttributeCodes, true),
|
||||||
|
'show_in_selector' => ! in_array($attributeCode, $hiddenAttributeCodes, true),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$itemAttributes[$attributeCode] = $itemAttribute;
|
$itemAttributes[$attributeCode] = $itemAttribute;
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -132,6 +132,7 @@ class FiestaFutbolInfantilProductSeeder extends Seeder
|
|||||||
'has_tickets' => true,
|
'has_tickets' => true,
|
||||||
'attribute_codes' => ['event_date'],
|
'attribute_codes' => ['event_date'],
|
||||||
'multi_select_attribute_codes' => ['event_date'],
|
'multi_select_attribute_codes' => ['event_date'],
|
||||||
|
'hidden_attribute_codes' => ['event_date'],
|
||||||
'variants' => [[
|
'variants' => [[
|
||||||
'real_stock' => 120,
|
'real_stock' => 120,
|
||||||
'event_date_ids' => $dateIds->all(),
|
'event_date_ids' => $dateIds->all(),
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ return [
|
|||||||
'direct_inventory_forbidden' => 'An item with variants cannot have direct inventory.',
|
'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.',
|
'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.',
|
'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.',
|
'event_date_selection_required' => 'At least one event date must be selected.',
|
||||||
'single_event_date_required' => 'Exactly 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.',
|
'event_date_wrong_tenant' => 'Every event date must belong to the catalog item tenant.',
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ return [
|
|||||||
'direct_inventory_forbidden' => 'Un ítem con variantes no puede tener inventario directo.',
|
'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.',
|
'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.',
|
'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.',
|
'event_date_selection_required' => 'Debe seleccionar al menos una fecha de evento.',
|
||||||
'single_event_date_required' => 'Debe seleccionar exactamente 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.',
|
'event_date_wrong_tenant' => 'Todas las fechas del evento deben pertenecer al tenant del ítem de catálogo.',
|
||||||
|
|||||||
@@ -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(
|
private function createItem(
|
||||||
Tenant $tenant,
|
Tenant $tenant,
|
||||||
string $name,
|
string $name,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ class CatalogSchemaTest extends TestCase
|
|||||||
$this->assertTrue(Schema::hasTable('variantes'));
|
$this->assertTrue(Schema::hasTable('variantes'));
|
||||||
$this->assertTrue(Schema::hasTable('item_attributes'));
|
$this->assertTrue(Schema::hasTable('item_attributes'));
|
||||||
$this->assertTrue(Schema::hasColumn('item_attributes', 'sort_order'));
|
$this->assertTrue(Schema::hasColumn('item_attributes', 'sort_order'));
|
||||||
|
$this->assertTrue(Schema::hasColumn('item_attributes', 'show_in_selector'));
|
||||||
$this->assertTrue(Schema::hasTable('variant_values'));
|
$this->assertTrue(Schema::hasTable('variant_values'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
public function test_it_allows_the_same_event_date_with_different_attribute_values(): void
|
||||||
{
|
{
|
||||||
$sector = $this->createAttribute('sector');
|
$sector = $this->createAttribute('sector');
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ class FiestaFutbolInfantilProductSeederTest extends TestCase
|
|||||||
->sole();
|
->sole();
|
||||||
$dateAttribute = $abono->itemAttributes->firstWhere('attribute.codigo', 'event_date');
|
$dateAttribute = $abono->itemAttributes->firstWhere('attribute.codigo', 'event_date');
|
||||||
$this->assertTrue($dateAttribute->allow_multi_select);
|
$this->assertTrue($dateAttribute->allow_multi_select);
|
||||||
|
$this->assertFalse($dateAttribute->show_in_selector);
|
||||||
$this->assertEqualsCanonicalizing(
|
$this->assertEqualsCanonicalizing(
|
||||||
[4],
|
[4],
|
||||||
$abono->variants->map(fn ($variant): int => $variant->eventDates->count())->all(),
|
$abono->variants->map(fn ($variant): int => $variant->eventDates->count())->all(),
|
||||||
|
|||||||
Reference in New Issue
Block a user