refactor(variant): enhance selection values handling for multi-select attributes
This commit is contained in:
@@ -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')
|
||||||
|
|||||||
@@ -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' => [
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user