feat(catalog): add tenant-scoped product attributes and variant definitions
This commit is contained in:
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Catalog\Controllers;
|
||||||
|
|
||||||
|
use App\Domains\Catalog\Models\ProductAttribute;
|
||||||
|
use App\Domains\Catalog\Requests\StoreProductAttributeRequest;
|
||||||
|
use App\Domains\Catalog\Requests\UpdateProductAttributeRequest;
|
||||||
|
use App\Domains\Catalog\Resources\ProductAttributeResource;
|
||||||
|
use App\Domains\Catalog\Support\ProductAttributeType;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
class ProductAttributeController extends Controller
|
||||||
|
{
|
||||||
|
public function index(): JsonResponse
|
||||||
|
{
|
||||||
|
$query = ProductAttribute::query()->with('options')->latest();
|
||||||
|
|
||||||
|
if (request()->filled('tenant_codigo')) {
|
||||||
|
$query->where('tenant_codigo', request('tenant_codigo'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProductAttributeResource::collection($query->get())->response();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function store(StoreProductAttributeRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
$attribute = DB::transaction(function () use ($request): ProductAttribute {
|
||||||
|
$validated = $request->validated();
|
||||||
|
$options = $validated['options'] ?? [];
|
||||||
|
unset($validated['options']);
|
||||||
|
|
||||||
|
$type = ProductAttributeType::from((string) $validated['type']);
|
||||||
|
if (! $type->supportsOptions()) {
|
||||||
|
$validated['metadata_schema'] = null;
|
||||||
|
$options = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @var ProductAttribute $attribute */
|
||||||
|
$attribute = ProductAttribute::query()->create($validated);
|
||||||
|
$attribute->options()->createMany($options);
|
||||||
|
|
||||||
|
return $attribute->load('options');
|
||||||
|
});
|
||||||
|
|
||||||
|
return ProductAttributeResource::make($attribute)->response()->setStatusCode(201);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(ProductAttribute $productAttribute): ProductAttributeResource
|
||||||
|
{
|
||||||
|
return ProductAttributeResource::make($productAttribute->load('options'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(UpdateProductAttributeRequest $request, ProductAttribute $productAttribute): ProductAttributeResource
|
||||||
|
{
|
||||||
|
$productAttribute = DB::transaction(function () use ($request, $productAttribute): ProductAttribute {
|
||||||
|
$validated = $request->validated();
|
||||||
|
$options = $validated['options'] ?? [];
|
||||||
|
unset($validated['options']);
|
||||||
|
|
||||||
|
$type = ProductAttributeType::from((string) $validated['type']);
|
||||||
|
if (! $type->supportsOptions()) {
|
||||||
|
$validated['metadata_schema'] = null;
|
||||||
|
$options = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$productAttribute->update($validated);
|
||||||
|
$productAttribute->options()->delete();
|
||||||
|
$productAttribute->options()->createMany($options);
|
||||||
|
|
||||||
|
return $productAttribute->load('options');
|
||||||
|
});
|
||||||
|
|
||||||
|
return ProductAttributeResource::make($productAttribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(ProductAttribute $productAttribute): Response
|
||||||
|
{
|
||||||
|
$productAttribute->delete();
|
||||||
|
|
||||||
|
return response()->noContent();
|
||||||
|
}
|
||||||
|
}
|
||||||
62
app/Domains/Catalog/Models/ProductAttribute.php
Normal file
62
app/Domains/Catalog/Models/ProductAttribute.php
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Catalog\Models;
|
||||||
|
|
||||||
|
use App\Domains\Catalog\Support\ProductAttributeType;
|
||||||
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
#[Fillable([
|
||||||
|
'tenant_codigo',
|
||||||
|
'codigo',
|
||||||
|
'nombre',
|
||||||
|
'is_required',
|
||||||
|
'metadata_schema',
|
||||||
|
'type',
|
||||||
|
])]
|
||||||
|
class ProductAttribute extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'productos_attributes';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'is_required' => 'boolean',
|
||||||
|
'metadata_schema' => 'array',
|
||||||
|
'type' => ProductAttributeType::class,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return BelongsTo<Tenant, $this>
|
||||||
|
*/
|
||||||
|
public function tenant(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Tenant::class, 'tenant_codigo', 'codigo');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return HasMany<ProductAttributeOption, $this>
|
||||||
|
*/
|
||||||
|
public function options(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(ProductAttributeOption::class, 'attribute_id')->orderBy('sort_order');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return HasMany<ProductVariantDefinition, $this>
|
||||||
|
*/
|
||||||
|
public function variantDefinitions(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(ProductVariantDefinition::class, 'attribute_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
40
app/Domains/Catalog/Models/ProductAttributeOption.php
Normal file
40
app/Domains/Catalog/Models/ProductAttributeOption.php
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Catalog\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
#[Fillable([
|
||||||
|
'attribute_id',
|
||||||
|
'label',
|
||||||
|
'sort_order',
|
||||||
|
'metadata',
|
||||||
|
])]
|
||||||
|
class ProductAttributeOption extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'attribute_options';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, string>
|
||||||
|
*/
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'sort_order' => 'integer',
|
||||||
|
'metadata' => 'array',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return BelongsTo<ProductAttribute, $this>
|
||||||
|
*/
|
||||||
|
public function attribute(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ProductAttribute::class, 'attribute_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ use Illuminate\Database\Eloquent\Attributes\Fillable;
|
|||||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
#[Fillable([
|
#[Fillable([
|
||||||
'producto_id',
|
'producto_id',
|
||||||
@@ -37,4 +38,12 @@ class ProductVariant extends Model
|
|||||||
{
|
{
|
||||||
return $this->belongsTo(Product::class, 'producto_id');
|
return $this->belongsTo(Product::class, 'producto_id');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return HasMany<ProductVariantDefinition, $this>
|
||||||
|
*/
|
||||||
|
public function definitions(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(ProductVariantDefinition::class, 'producto_variante_id');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
36
app/Domains/Catalog/Models/ProductVariantDefinition.php
Normal file
36
app/Domains/Catalog/Models/ProductVariantDefinition.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Catalog\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Attributes\Fillable;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
#[Fillable([
|
||||||
|
'producto_variante_id',
|
||||||
|
'attribute_id',
|
||||||
|
'value',
|
||||||
|
])]
|
||||||
|
class ProductVariantDefinition extends Model
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
protected $table = 'productos_variantes_definiciones';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return BelongsTo<ProductVariant, $this>
|
||||||
|
*/
|
||||||
|
public function variant(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ProductVariant::class, 'producto_variante_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return BelongsTo<ProductAttribute, $this>
|
||||||
|
*/
|
||||||
|
public function attribute(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(ProductAttribute::class, 'attribute_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Catalog\Requests;
|
||||||
|
|
||||||
|
use App\Domains\Catalog\Support\ProductAttributeType;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Validation\Validator;
|
||||||
|
|
||||||
|
class StoreProductAttributeRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'tenant_codigo' => ['required', 'string', 'exists:tenants,codigo'],
|
||||||
|
'codigo' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'max:255',
|
||||||
|
Rule::unique('productos_attributes', 'codigo')->where(
|
||||||
|
fn ($query) => $query->where('tenant_codigo', $this->input('tenant_codigo'))
|
||||||
|
),
|
||||||
|
],
|
||||||
|
'nombre' => ['required', 'string', 'max:255'],
|
||||||
|
'is_required' => ['sometimes', 'boolean'],
|
||||||
|
'metadata_schema' => ['nullable', 'array'],
|
||||||
|
'type' => ['required', Rule::enum(ProductAttributeType::class)],
|
||||||
|
'options' => [
|
||||||
|
Rule::requiredIf(fn (): bool => in_array($this->input('type'), [
|
||||||
|
ProductAttributeType::Select->value,
|
||||||
|
ProductAttributeType::Multiselect->value,
|
||||||
|
], true)),
|
||||||
|
Rule::prohibitedIf(fn (): bool => ! in_array($this->input('type'), [
|
||||||
|
ProductAttributeType::Select->value,
|
||||||
|
ProductAttributeType::Multiselect->value,
|
||||||
|
], true)),
|
||||||
|
'array',
|
||||||
|
],
|
||||||
|
'options.*.label' => ['required', 'string', 'max:255'],
|
||||||
|
'options.*.sort_order' => ['sometimes', 'integer'],
|
||||||
|
'options.*.metadata' => ['nullable', 'array'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withValidator(Validator $validator): void
|
||||||
|
{
|
||||||
|
$validator->after(function (Validator $validator): void {
|
||||||
|
$type = $this->input('type');
|
||||||
|
$supportsOptions = in_array($type, [ProductAttributeType::Select->value, ProductAttributeType::Multiselect->value], true);
|
||||||
|
|
||||||
|
if (! $supportsOptions && $this->filled('metadata_schema')) {
|
||||||
|
$validator->errors()->add('metadata_schema', 'The metadata_schema field is only allowed for select and multiselect attributes.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $supportsOptions && $this->filled('options')) {
|
||||||
|
$validator->errors()->add('options', 'Options are only allowed for select and multiselect attributes.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Catalog\Requests;
|
||||||
|
|
||||||
|
use App\Domains\Catalog\Models\ProductAttribute;
|
||||||
|
use App\Domains\Catalog\Support\ProductAttributeType;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
use Illuminate\Validation\Validator;
|
||||||
|
|
||||||
|
class UpdateProductAttributeRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
/** @var ProductAttribute|null $attribute */
|
||||||
|
$attribute = $this->route('productAttribute');
|
||||||
|
|
||||||
|
return [
|
||||||
|
'codigo' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'max:255',
|
||||||
|
Rule::unique('productos_attributes', 'codigo')
|
||||||
|
->ignore($attribute?->id)
|
||||||
|
->where(fn ($query) => $query->where('tenant_codigo', $attribute?->tenant_codigo)),
|
||||||
|
],
|
||||||
|
'nombre' => ['required', 'string', 'max:255'],
|
||||||
|
'is_required' => ['sometimes', 'boolean'],
|
||||||
|
'metadata_schema' => ['nullable', 'array'],
|
||||||
|
'type' => ['required', Rule::enum(ProductAttributeType::class)],
|
||||||
|
'options' => ['sometimes', 'array'],
|
||||||
|
'options.*.label' => ['required', 'string', 'max:255'],
|
||||||
|
'options.*.sort_order' => ['sometimes', 'integer'],
|
||||||
|
'options.*.metadata' => ['nullable', 'array'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function withValidator(Validator $validator): void
|
||||||
|
{
|
||||||
|
$validator->after(function (Validator $validator): void {
|
||||||
|
$type = $this->input('type');
|
||||||
|
$supportsOptions = in_array($type, [ProductAttributeType::Select->value, ProductAttributeType::Multiselect->value], true);
|
||||||
|
|
||||||
|
if (! $supportsOptions && $this->filled('metadata_schema')) {
|
||||||
|
$validator->errors()->add('metadata_schema', 'The metadata_schema field is only allowed for select and multiselect attributes.');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $supportsOptions && $this->filled('options')) {
|
||||||
|
$validator->errors()->add('options', 'Options are only allowed for select and multiselect attributes.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Catalog\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @mixin \App\Domains\Catalog\Models\ProductAttributeOption
|
||||||
|
*/
|
||||||
|
class ProductAttributeOptionResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'label' => $this->label,
|
||||||
|
'sort_order' => $this->sort_order,
|
||||||
|
'metadata' => $this->metadata,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
29
app/Domains/Catalog/Resources/ProductAttributeResource.php
Normal file
29
app/Domains/Catalog/Resources/ProductAttributeResource.php
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Catalog\Resources;
|
||||||
|
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\Resources\Json\JsonResource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @mixin \App\Domains\Catalog\Models\ProductAttribute
|
||||||
|
*/
|
||||||
|
class ProductAttributeResource extends JsonResource
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function toArray(Request $request): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => $this->id,
|
||||||
|
'tenant_codigo' => $this->tenant_codigo,
|
||||||
|
'codigo' => $this->codigo,
|
||||||
|
'nombre' => $this->nombre,
|
||||||
|
'is_required' => $this->is_required,
|
||||||
|
'metadata_schema' => $this->metadata_schema,
|
||||||
|
'type' => $this->type?->value ?? $this->type,
|
||||||
|
'options' => ProductAttributeOptionResource::collection($this->whenLoaded('options')),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
16
app/Domains/Catalog/Support/ProductAttributeType.php
Normal file
16
app/Domains/Catalog/Support/ProductAttributeType.php
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Domains\Catalog\Support;
|
||||||
|
|
||||||
|
enum ProductAttributeType: string
|
||||||
|
{
|
||||||
|
case Text = 'text';
|
||||||
|
case Numeric = 'numeric';
|
||||||
|
case Select = 'select';
|
||||||
|
case Multiselect = 'multiselect';
|
||||||
|
|
||||||
|
public function supportsOptions(): bool
|
||||||
|
{
|
||||||
|
return in_array($this, [self::Select, self::Multiselect], true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,8 +3,11 @@
|
|||||||
use App\Domains\Catalog\Controllers\BrandController;
|
use App\Domains\Catalog\Controllers\BrandController;
|
||||||
use App\Domains\Catalog\Controllers\CategoryController;
|
use App\Domains\Catalog\Controllers\CategoryController;
|
||||||
use App\Domains\Catalog\Controllers\ProductController;
|
use App\Domains\Catalog\Controllers\ProductController;
|
||||||
|
use App\Domains\Catalog\Controllers\ProductAttributeController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
Route::apiResource('marcas', BrandController::class)->parameters(['marcas' => 'marca']);
|
Route::apiResource('marcas', BrandController::class)->parameters(['marcas' => 'marca']);
|
||||||
Route::apiResource('categorias', CategoryController::class)->parameters(['categorias' => 'categoria']);
|
Route::apiResource('categorias', CategoryController::class)->parameters(['categorias' => 'categoria']);
|
||||||
Route::apiResource('productos', ProductController::class);
|
Route::apiResource('productos', ProductController::class);
|
||||||
|
Route::apiResource('product-attributes', ProductAttributeController::class)
|
||||||
|
->parameters(['product-attributes' => 'productAttribute']);
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('productos_attributes', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->string('tenant_codigo');
|
||||||
|
$table->string('codigo');
|
||||||
|
$table->string('nombre');
|
||||||
|
$table->boolean('is_required')->default(false);
|
||||||
|
$table->json('metadata_schema')->nullable();
|
||||||
|
$table->string('type');
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->foreign('tenant_codigo')
|
||||||
|
->references('codigo')
|
||||||
|
->on('tenants')
|
||||||
|
->cascadeOnUpdate()
|
||||||
|
->cascadeOnDelete();
|
||||||
|
|
||||||
|
$table->unique(['tenant_codigo', 'codigo']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('productos_attributes');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('attribute_options', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('attribute_id')->constrained('productos_attributes')->cascadeOnUpdate()->cascadeOnDelete();
|
||||||
|
$table->string('label');
|
||||||
|
$table->unsignedInteger('sort_order')->default(0);
|
||||||
|
$table->json('metadata')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('attribute_options');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('productos_variantes_definiciones', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->unsignedBigInteger('producto_variante_id');
|
||||||
|
$table->foreignId('attribute_id')->constrained('productos_attributes')->cascadeOnUpdate()->cascadeOnDelete();
|
||||||
|
$table->text('value')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->foreign('producto_variante_id')
|
||||||
|
->references('id')
|
||||||
|
->on('productos_variantes')
|
||||||
|
->cascadeOnUpdate()
|
||||||
|
->cascadeOnDelete();
|
||||||
|
|
||||||
|
$table->unique(['producto_variante_id', 'attribute_id'], 'prod_var_def_variant_attr_unique');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('productos_variantes_definiciones');
|
||||||
|
}
|
||||||
|
};
|
||||||
86
tests/Feature/Catalog/ProductAttributeControllerTest.php
Normal file
86
tests/Feature/Catalog/ProductAttributeControllerTest.php
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Feature\Catalog;
|
||||||
|
|
||||||
|
use App\Domains\Tenant\Models\Tenant;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Tests\TestCase;
|
||||||
|
|
||||||
|
class ProductAttributeControllerTest extends TestCase
|
||||||
|
{
|
||||||
|
use RefreshDatabase;
|
||||||
|
|
||||||
|
public function test_it_creates_a_select_attribute_with_options(): void
|
||||||
|
{
|
||||||
|
Tenant::create([
|
||||||
|
'codigo' => 'acme',
|
||||||
|
'nombre' => 'Acme',
|
||||||
|
'dominio' => 'acme.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/product-attributes', [
|
||||||
|
'tenant_codigo' => 'acme',
|
||||||
|
'codigo' => 'color',
|
||||||
|
'nombre' => 'Color',
|
||||||
|
'is_required' => true,
|
||||||
|
'type' => 'select',
|
||||||
|
'metadata_schema' => [
|
||||||
|
'swatch' => ['type' => 'hex'],
|
||||||
|
],
|
||||||
|
'options' => [
|
||||||
|
[
|
||||||
|
'label' => 'Red',
|
||||||
|
'sort_order' => 1,
|
||||||
|
'metadata' => ['hex' => '#ff0000'],
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'label' => 'Blue',
|
||||||
|
'sort_order' => 2,
|
||||||
|
'metadata' => ['hex' => '#0000ff'],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertCreated()
|
||||||
|
->assertJsonPath('tenant_codigo', 'acme')
|
||||||
|
->assertJsonPath('codigo', 'color')
|
||||||
|
->assertJsonPath('type', 'select')
|
||||||
|
->assertJsonPath('options.0.label', 'Red')
|
||||||
|
->assertJsonPath('options.1.metadata.hex', '#0000ff');
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('productos_attributes', [
|
||||||
|
'tenant_codigo' => 'acme',
|
||||||
|
'codigo' => 'color',
|
||||||
|
'type' => 'select',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('attribute_options', [
|
||||||
|
'label' => 'Red',
|
||||||
|
'sort_order' => 1,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_it_rejects_options_for_text_attributes(): void
|
||||||
|
{
|
||||||
|
Tenant::create([
|
||||||
|
'codigo' => 'acme',
|
||||||
|
'nombre' => 'Acme',
|
||||||
|
'dominio' => 'acme.com',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $this->postJson('/api/product-attributes', [
|
||||||
|
'tenant_codigo' => 'acme',
|
||||||
|
'codigo' => 'material',
|
||||||
|
'nombre' => 'Material',
|
||||||
|
'type' => 'text',
|
||||||
|
'options' => [
|
||||||
|
['label' => 'Cotton'],
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response
|
||||||
|
->assertUnprocessable()
|
||||||
|
->assertJsonValidationErrors(['options']);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user