refactor(variant): enhance selection values handling for multi-select attributes

This commit is contained in:
2026-08-07 12:07:07 -03:00
parent 21b70777f2
commit 6318a99328
6 changed files with 153 additions and 19 deletions

View File

@@ -109,10 +109,23 @@ class Variant extends Model
public function selectionValues(): Collection public function selectionValues(): Collection
{ {
$values = $this->definitions $values = $this->definitions
->mapWithKeys(fn ($definition) => [ ->groupBy('item_attribute_id')
$definition->itemAttribute?->attribute?->codigo => $definition->value, ->mapWithKeys(function (Collection $definitions): array {
]) $itemAttribute = $definitions->first()?->itemAttribute;
->filter(fn ($value, $key): bool => $key !== null); $attributeCode = $itemAttribute?->attribute?->codigo;
if ($attributeCode === null) {
return [];
}
$definitionValues = $definitions->pluck('value')->values();
return [
$attributeCode => $itemAttribute->allow_multi_select
? $definitionValues->all()
: $definitionValues->first(),
];
});
$eventDateIds = $this->selectedEventDates() $eventDateIds = $this->selectedEventDates()
->pluck('id') ->pluck('id')

View File

@@ -109,7 +109,8 @@ class StoreCatalogItemRequest extends FormRequest
'variants.*.reserved_stock' => ['prohibited'], 'variants.*.reserved_stock' => ['prohibited'],
'variants.*.sold_units' => ['prohibited'], 'variants.*.sold_units' => ['prohibited'],
'variants.*.values' => ['sometimes', 'array'], 'variants.*.values' => ['sometimes', 'array'],
'variants.*.values.*' => ['nullable', 'string'], 'variants.*.values.*' => ['nullable'],
'variants.*.values.*.*' => ['required', 'string'],
'variants.*.images' => ['sometimes', 'array'], 'variants.*.images' => ['sometimes', 'array'],
'variants.*.images.*' => ['required', new ImageOrBase64Rule], 'variants.*.images.*' => ['required', new ImageOrBase64Rule],
'components' => [ 'components' => [

View File

@@ -569,10 +569,16 @@ class CatalogService
]); ]);
} }
$variant->definitions()->create([ foreach ($this->validatedVariantValues(
'item_attribute_id' => $itemAttribute->id, $itemAttribute,
'value' => $value, $value,
]); "variants.{$index}.values.{$attributeCode}",
) as $validatedValue) {
$variant->definitions()->create([
'item_attribute_id' => $itemAttribute->id,
'value' => $validatedValue,
]);
}
} }
return $variant; return $variant;
@@ -618,8 +624,14 @@ class CatalogService
$combination = [$eventDateIds]; $combination = [$eventDateIds];
foreach ($attributeCodes as $attributeCode) { foreach ($attributeCodes as $attributeCode) {
$value = trim((string) ($variant['values'][$attributeCode] ?? '')); $values = $variant['values'][$attributeCode] ?? '';
$combination[] = Str::ascii(mb_strtolower($value)); $normalizedValues = collect(is_array($values) ? $values : [$values])
->map(fn ($value): string => $this->normalizeVariantValue((string) $value))
->unique()
->sort()
->values()
->implode(',');
$combination[] = $normalizedValues;
} }
$key = implode('|', $combination); $key = implode('|', $combination);
@@ -633,6 +645,72 @@ class CatalogService
} }
} }
/** @return array<int, string> */
private function validatedVariantValues(
ItemAttribute $itemAttribute,
mixed $value,
string $validationKey,
): array {
$values = is_array($value) ? array_values($value) : [$value];
if ($values === [] || (! $itemAttribute->allow_multi_select && count($values) !== 1)) {
throw ValidationException::withMessages([
$validationKey => [
$itemAttribute->allow_multi_select
? 'At least one value must be selected.'
: 'Exactly one value must be selected.',
],
]);
}
if (collect($values)->contains(fn ($item): bool => ! is_string($item) || trim($item) === '')) {
throw ValidationException::withMessages([
$validationKey => ['Every selected value must be a non-empty string.'],
]);
}
$normalizedValues = collect($values)
->map(fn (string $item): string => $this->normalizeVariantValue($item));
if ($normalizedValues->unique()->count() !== count($values)) {
throw ValidationException::withMessages([
$validationKey => ['Selected values must be distinct.'],
]);
}
$attribute = $itemAttribute->attribute;
if ($attribute->type->supportsOptions() && ! $attribute->type->usesDynamicOptions()) {
$optionsByNormalizedValue = $attribute->options
->keyBy(fn ($option): string => $this->normalizeVariantValue($option->value));
$resolvedOptions = $normalizedValues->map(fn (string $normalizedValue) => $optionsByNormalizedValue->get($normalizedValue));
if ($resolvedOptions->contains(null)) {
throw ValidationException::withMessages([
$validationKey => ['One or more selected values are not valid attribute options.'],
]);
}
$validityTimeIds = $resolvedOptions
->pluck('validity_time_id')
->filter()
->unique();
if ($validityTimeIds->count() > 1) {
throw ValidationException::withMessages([
$validationKey => ['Selected values cannot have different validity windows.'],
]);
}
return $resolvedOptions->pluck('value')->all();
}
return collect($values)->map(fn (string $item): string => trim($item))->all();
}
private function normalizeVariantValue(string $value): string
{
return Str::ascii(mb_strtolower(trim($value)));
}
/** /**
* @param array<string, mixed> $data * @param array<string, mixed> $data
* @param array<int, array<string, mixed>> $variants * @param array<int, array<string, mixed>> $variants

View File

@@ -99,10 +99,18 @@ class PurchaseItemResource extends JsonResource
} }
$attributes = $variant->definitions $attributes = $variant->definitions
->map(fn ($definition): array => [ ->groupBy('item_attribute_id')
'name' => (string) ($definition->itemAttribute?->attribute?->nombre ?? ''), ->map(function ($definitions): array {
'value' => $definition->value, $itemAttribute = $definitions->first()?->itemAttribute;
]) $values = $definitions->pluck('value')->values();
return [
'name' => (string) ($itemAttribute?->attribute?->nombre ?? ''),
'value' => $itemAttribute?->allow_multi_select
? $values->all()
: $values->first(),
];
})
->filter(fn (array $attribute): bool => $attribute['name'] !== '' || $attribute['value'] !== null) ->filter(fn (array $attribute): bool => $attribute['name'] !== '' || $attribute['value'] !== null)
->values(); ->values();

View File

@@ -54,10 +54,18 @@ class PurchaseItemSnapshotFactory
private function snapshotAttributes(Variant $variant): array private function snapshotAttributes(Variant $variant): array
{ {
$attributes = $variant->definitions $attributes = $variant->definitions
->map(fn ($definition): array => [ ->groupBy('item_attribute_id')
'name' => (string) ($definition->itemAttribute?->attribute?->nombre ?? ''), ->map(function ($definitions): array {
'value' => $definition->value, $itemAttribute = $definitions->first()?->itemAttribute;
]) $values = $definitions->pluck('value')->values();
return [
'name' => (string) ($itemAttribute?->attribute?->nombre ?? ''),
'value' => $itemAttribute?->allow_multi_select
? $values->all()
: $values->first(),
];
})
->filter(fn (array $attribute): bool => $attribute['name'] !== '' || $attribute['value'] !== null) ->filter(fn (array $attribute): bool => $attribute['name'] !== '' || $attribute['value'] !== null)
->values(); ->values();

View File

@@ -172,6 +172,32 @@ class CatalogServiceTest extends TestCase
); );
} }
public function test_it_creates_multiple_values_for_any_multi_select_attribute(): void
{
$color = $this->createAttribute('color', FieldType::Select);
$color->options()->createMany([
['value' => 'Green', 'label' => 'Green', 'sort_order' => 1],
['value' => 'White', 'label' => 'White', 'sort_order' => 2],
]);
$item = $this->service->create([
'tenant_code' => $this->tenant->codigo,
'slug' => 'multi-color-shirt',
'nombre' => 'Multi-color shirt',
'precio' => 100,
'attribute_codes' => ['color'],
'multi_select_attribute_codes' => ['color'],
'variants' => [[
'real_stock' => 5,
'values' => ['color' => ['White', 'Green']],
]],
]);
$variant = $item->variants->sole();
$this->assertSame(['White', 'Green'], $variant->definitions->pluck('value')->all());
$this->assertSame(['White', 'Green'], $variant->selectionValues()->get('color'));
}
public function test_it_rejects_duplicate_variant_combinations(): void public function test_it_rejects_duplicate_variant_combinations(): void
{ {
$sector = $this->createAttribute('sector'); $sector = $this->createAttribute('sector');