feat: implement product and attribute management services with corresponding controllers and feature tests

This commit is contained in:
2026-06-29 11:37:22 -03:00
parent b5b57a753c
commit 9b7d728117
7 changed files with 365 additions and 137 deletions

View File

@@ -60,4 +60,184 @@ class Product extends Model
'attribute_id'
)->withTimestamps();
}
/**
* Create a variant for this product with its definitions.
*
* @param array<string, mixed> $data
*/
public function createVariant(array $data): ProductVariant
{
$definitions = $data['definitions'] ?? [];
$this->validateVariantDefinitions($definitions);
unset($data['definitions']);
/** @var ProductVariant $variant */
$variant = $this->variants()->create($data);
$variant->definitions()->createMany($definitions);
return $variant;
}
/**
* Create multiple variants for this product.
*
* @param array<int, array<string, mixed>> $variantsData
* @return \Illuminate\Database\Eloquent\Collection<int, ProductVariant>
*/
public function createVariants(array $variantsData): \Illuminate\Database\Eloquent\Collection
{
$variants = new \Illuminate\Database\Eloquent\Collection();
foreach ($variantsData as $variantData) {
$variants->push($this->createVariant($variantData));
}
return $variants;
}
/**
* Update a product variant.
*
* @param array<string, mixed> $data
*/
public function updateVariant(ProductVariant $variant, array $data): ProductVariant
{
$definitions = $data['definitions'] ?? [];
$this->validateVariantDefinitions($definitions);
unset($data['definitions']);
unset($data['producto_id']);
$variant->update($data);
$variant->definitions()->delete();
$variant->definitions()->createMany($definitions);
return $variant;
}
/**
* Delete a product variant.
*/
public function deleteVariant(ProductVariant $variant): void
{
$variant->definitions()->delete();
$variant->delete();
}
/**
* Validate variant definitions options against Attribute configuration.
*
* @param array $definitions
* @throws \InvalidArgumentException
*/
protected function validateVariantDefinitions(array $definitions): void
{
foreach ($definitions as $definition) {
$attributeId = $definition['attribute_id'] ?? null;
if (!$attributeId) {
continue;
}
$attribute = Attribute::find($attributeId);
if (!$attribute) {
throw new \InvalidArgumentException("Attribute with ID {$attributeId} not found.");
}
if ($attribute->type === \App\Domains\Shared\Enums\FieldType::Select) {
$allowedValues = $attribute->options()->pluck('value')->toArray();
$val = $definition['value'] ?? null;
if ($val !== null && !in_array($val, $allowedValues, true)) {
throw new \InvalidArgumentException("The value '{$val}' is not a valid option for the select attribute '{$attribute->nombre}'.");
}
} elseif ($attribute->type === \App\Domains\Shared\Enums\FieldType::Multiselect) {
$allowedValues = $attribute->options()->pluck('value')->toArray();
$val = $definition['value'] ?? null;
if ($val !== null) {
$values = [];
if (is_array($val)) {
$values = $val;
} else {
$decoded = json_decode($val, true);
if (is_array($decoded)) {
$values = $decoded;
} else {
$values = array_map('trim', explode(',', $val));
}
}
foreach ($values as $v) {
if (!in_array($v, $allowedValues, true)) {
throw new \InvalidArgumentException("The value '{$v}' is not a valid option for the multiselect attribute '{$attribute->nombre}'.");
}
}
}
}
}
}
/**
* Create an attribute.
*
* @param array<string, mixed> $data
*/
public static function createAttribute(Tenant $tenant, array $data): Attribute
{
$options = $data['options'] ?? [];
unset($data['options']);
$type = \App\Domains\Shared\Enums\FieldType::from((string) $data['type']);
if (! $type->supportsOptions() && ! empty($options)) {
throw new \InvalidArgumentException('Options are only allowed for select and multiselect attributes.');
}
if (! $type->supportsOptions()) {
$data['metadata_schema'] = null;
}
/** @var Attribute $attribute */
$attribute = Attribute::query()->create([
...$data,
'tenant_codigo' => $tenant->codigo,
]);
$attribute->options()->createMany($options);
return $attribute->load('options');
}
/**
* Update an attribute.
*
* @param array<string, mixed> $data
*/
public static function updateAttribute(Attribute $attribute, array $data): Attribute
{
$options = $data['options'] ?? [];
unset($data['options']);
$typeStr = $data['type'] ?? $attribute->type->value;
$type = \App\Domains\Shared\Enums\FieldType::from((string) $typeStr);
if (! $type->supportsOptions() && ! empty($options)) {
throw new \InvalidArgumentException('Options are only allowed for select and multiselect attributes.');
}
if (! $type->supportsOptions()) {
$data['metadata_schema'] = null;
}
$attribute->update($data);
$attribute->options()->delete();
$attribute->options()->createMany($options);
return $attribute->load('options');
}
/**
* Delete an attribute.
*/
public static function deleteAttribute(Attribute $attribute): void
{
$attribute->options()->delete();
$attribute->delete();
}
}