From 4c237fb7299317e4644ea4a2e964d555a152e61f Mon Sep 17 00:00:00 2001 From: ncoronel Date: Fri, 26 Jun 2026 11:40:07 -0300 Subject: [PATCH] feat: implement product attributes and tenant properties management with CRUD controllers, requests, and domain models --- .../ProductAttributeController.php | 6 ++-- .../Catalog/Models/ProductAttribute.php | 4 +-- .../Requests/StoreProductAttributeRequest.php | 14 ++++---- .../UpdateProductAttributeRequest.php | 6 ++-- .../Catalog/Support/ProductAttributeType.php | 16 --------- app/Domains/Shared/Enums/FieldType.php | 27 ++++++++++++++ app/Domains/Tenant/Models/TenantProp.php | 4 +-- .../Requests/StoreTenantPropRequest.php | 4 +-- .../Requests/UpdateTenantPropRequest.php | 4 +-- .../Tenant/Support/TenantPropDataType.php | 18 ---------- ...00000_migrate_product_attributes_types.php | 35 +++++++++++++++++++ .../ProductAttributeControllerTest.php | 14 ++++---- .../Tenant/BootstrapTenantControllerTest.php | 20 +++++------ .../Tenant/TenantPropControllerTest.php | 4 +-- 14 files changed, 102 insertions(+), 74 deletions(-) delete mode 100644 app/Domains/Catalog/Support/ProductAttributeType.php create mode 100644 app/Domains/Shared/Enums/FieldType.php delete mode 100644 app/Domains/Tenant/Support/TenantPropDataType.php create mode 100644 database/migrations/2026_06_26_100000_migrate_product_attributes_types.php diff --git a/app/Domains/Catalog/Controllers/ProductAttributeController.php b/app/Domains/Catalog/Controllers/ProductAttributeController.php index 7dd7cd0..dff083d 100644 --- a/app/Domains/Catalog/Controllers/ProductAttributeController.php +++ b/app/Domains/Catalog/Controllers/ProductAttributeController.php @@ -6,7 +6,7 @@ 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\Domains\Shared\Enums\FieldType; use App\Domains\Tenant\Models\Tenant; use App\Http\Controllers\Controller; use Illuminate\Http\JsonResponse; @@ -33,7 +33,7 @@ class ProductAttributeController extends Controller $options = $validated['options'] ?? []; unset($validated['options']); - $type = ProductAttributeType::from((string) $validated['type']); + $type = FieldType::from((string) $validated['type']); if (! $type->supportsOptions()) { $validated['metadata_schema'] = null; $options = []; @@ -68,7 +68,7 @@ class ProductAttributeController extends Controller $options = $validated['options'] ?? []; unset($validated['options']); - $type = ProductAttributeType::from((string) $validated['type']); + $type = FieldType::from((string) $validated['type']); if (! $type->supportsOptions()) { $validated['metadata_schema'] = null; $options = []; diff --git a/app/Domains/Catalog/Models/ProductAttribute.php b/app/Domains/Catalog/Models/ProductAttribute.php index 9d3f333..82b06ed 100644 --- a/app/Domains/Catalog/Models/ProductAttribute.php +++ b/app/Domains/Catalog/Models/ProductAttribute.php @@ -2,7 +2,7 @@ namespace App\Domains\Catalog\Models; -use App\Domains\Catalog\Support\ProductAttributeType; +use App\Domains\Shared\Enums\FieldType; use App\Domains\Tenant\Models\Tenant; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Factories\HasFactory; @@ -32,7 +32,7 @@ class ProductAttribute extends Model return [ 'is_required' => 'boolean', 'metadata_schema' => 'array', - 'type' => ProductAttributeType::class, + 'type' => FieldType::class, ]; } diff --git a/app/Domains/Catalog/Requests/StoreProductAttributeRequest.php b/app/Domains/Catalog/Requests/StoreProductAttributeRequest.php index 9737901..2f7ebf4 100644 --- a/app/Domains/Catalog/Requests/StoreProductAttributeRequest.php +++ b/app/Domains/Catalog/Requests/StoreProductAttributeRequest.php @@ -2,7 +2,7 @@ namespace App\Domains\Catalog\Requests; -use App\Domains\Catalog\Support\ProductAttributeType; +use App\Domains\Shared\Enums\FieldType; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; use Illuminate\Validation\Validator; @@ -31,15 +31,15 @@ class StoreProductAttributeRequest extends FormRequest 'nombre' => ['required', 'string', 'max:255'], 'is_required' => ['sometimes', 'boolean'], 'metadata_schema' => ['nullable', 'array'], - 'type' => ['required', Rule::enum(ProductAttributeType::class)], + 'type' => ['required', Rule::enum(FieldType::class)], 'options' => [ Rule::requiredIf(fn (): bool => in_array($this->input('type'), [ - ProductAttributeType::Select->value, - ProductAttributeType::Multiselect->value, + FieldType::Select->value, + FieldType::Multiselect->value, ], true)), Rule::prohibitedIf(fn (): bool => ! in_array($this->input('type'), [ - ProductAttributeType::Select->value, - ProductAttributeType::Multiselect->value, + FieldType::Select->value, + FieldType::Multiselect->value, ], true)), 'array', ], @@ -53,7 +53,7 @@ class StoreProductAttributeRequest extends FormRequest { $validator->after(function (Validator $validator): void { $type = $this->input('type'); - $supportsOptions = in_array($type, [ProductAttributeType::Select->value, ProductAttributeType::Multiselect->value], true); + $supportsOptions = in_array($type, [FieldType::Select->value, FieldType::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.'); diff --git a/app/Domains/Catalog/Requests/UpdateProductAttributeRequest.php b/app/Domains/Catalog/Requests/UpdateProductAttributeRequest.php index f093b00..aa3ff7c 100644 --- a/app/Domains/Catalog/Requests/UpdateProductAttributeRequest.php +++ b/app/Domains/Catalog/Requests/UpdateProductAttributeRequest.php @@ -3,7 +3,7 @@ namespace App\Domains\Catalog\Requests; use App\Domains\Catalog\Models\ProductAttribute; -use App\Domains\Catalog\Support\ProductAttributeType; +use App\Domains\Shared\Enums\FieldType; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; use Illuminate\Validation\Validator; @@ -35,7 +35,7 @@ class UpdateProductAttributeRequest extends FormRequest 'nombre' => ['required', 'string', 'max:255'], 'is_required' => ['sometimes', 'boolean'], 'metadata_schema' => ['nullable', 'array'], - 'type' => ['required', Rule::enum(ProductAttributeType::class)], + 'type' => ['required', Rule::enum(FieldType::class)], 'options' => ['sometimes', 'array'], 'options.*.label' => ['required', 'string', 'max:255'], 'options.*.sort_order' => ['sometimes', 'integer'], @@ -47,7 +47,7 @@ class UpdateProductAttributeRequest extends FormRequest { $validator->after(function (Validator $validator): void { $type = $this->input('type'); - $supportsOptions = in_array($type, [ProductAttributeType::Select->value, ProductAttributeType::Multiselect->value], true); + $supportsOptions = in_array($type, [FieldType::Select->value, FieldType::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.'); diff --git a/app/Domains/Catalog/Support/ProductAttributeType.php b/app/Domains/Catalog/Support/ProductAttributeType.php deleted file mode 100644 index d3a67a8..0000000 --- a/app/Domains/Catalog/Support/ProductAttributeType.php +++ /dev/null @@ -1,16 +0,0 @@ - + */ + public static function values(): array + { + return array_column(self::cases(), 'value'); + } +} diff --git a/app/Domains/Tenant/Models/TenantProp.php b/app/Domains/Tenant/Models/TenantProp.php index 493edbc..03944a1 100644 --- a/app/Domains/Tenant/Models/TenantProp.php +++ b/app/Domains/Tenant/Models/TenantProp.php @@ -2,7 +2,7 @@ namespace App\Domains\Tenant\Models; -use App\Domains\Tenant\Support\TenantPropDataType; +use App\Domains\Shared\Enums\FieldType; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -28,7 +28,7 @@ class TenantProp extends Model { return [ 'is_required' => 'boolean', - 'data_type' => TenantPropDataType::class, + 'data_type' => FieldType::class, ]; } diff --git a/app/Domains/Tenant/Requests/StoreTenantPropRequest.php b/app/Domains/Tenant/Requests/StoreTenantPropRequest.php index 7d50af7..b318a77 100644 --- a/app/Domains/Tenant/Requests/StoreTenantPropRequest.php +++ b/app/Domains/Tenant/Requests/StoreTenantPropRequest.php @@ -2,7 +2,7 @@ namespace App\Domains\Tenant\Requests; -use App\Domains\Tenant\Support\TenantPropDataType; +use App\Domains\Shared\Enums\FieldType; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; @@ -23,7 +23,7 @@ class StoreTenantPropRequest extends FormRequest 'nombre' => ['required', 'string', 'max:255'], 'descripcion' => ['nullable', 'string'], 'is_required' => ['sometimes', 'boolean'], - 'data_type' => ['required', Rule::enum(TenantPropDataType::class)], + 'data_type' => ['required', Rule::enum(FieldType::class)], ]; } } diff --git a/app/Domains/Tenant/Requests/UpdateTenantPropRequest.php b/app/Domains/Tenant/Requests/UpdateTenantPropRequest.php index 140ec28..7cddfac 100644 --- a/app/Domains/Tenant/Requests/UpdateTenantPropRequest.php +++ b/app/Domains/Tenant/Requests/UpdateTenantPropRequest.php @@ -3,7 +3,7 @@ namespace App\Domains\Tenant\Requests; use App\Domains\Tenant\Models\TenantProp; -use App\Domains\Tenant\Support\TenantPropDataType; +use App\Domains\Shared\Enums\FieldType; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; @@ -27,7 +27,7 @@ class UpdateTenantPropRequest extends FormRequest 'nombre' => ['required', 'string', 'max:255'], 'descripcion' => ['nullable', 'string'], 'is_required' => ['sometimes', 'boolean'], - 'data_type' => ['required', Rule::enum(TenantPropDataType::class)], + 'data_type' => ['required', Rule::enum(FieldType::class)], ]; } } diff --git a/app/Domains/Tenant/Support/TenantPropDataType.php b/app/Domains/Tenant/Support/TenantPropDataType.php deleted file mode 100644 index d2dc5eb..0000000 --- a/app/Domains/Tenant/Support/TenantPropDataType.php +++ /dev/null @@ -1,18 +0,0 @@ - - */ - public static function values(): array - { - return array_column(self::cases(), 'value'); - } -} diff --git a/database/migrations/2026_06_26_100000_migrate_product_attributes_types.php b/database/migrations/2026_06_26_100000_migrate_product_attributes_types.php new file mode 100644 index 0000000..2ef7c94 --- /dev/null +++ b/database/migrations/2026_06_26_100000_migrate_product_attributes_types.php @@ -0,0 +1,35 @@ +where('type', 'text') + ->update(['type' => 'string']); + + DB::table('productos_attributes') + ->where('type', 'numeric') + ->update(['type' => 'number']); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + DB::table('productos_attributes') + ->where('type', 'string') + ->update(['type' => 'text']); + + DB::table('productos_attributes') + ->where('type', 'number') + ->update(['type' => 'numeric']); + } +}; diff --git a/tests/Feature/Catalog/ProductAttributeControllerTest.php b/tests/Feature/Catalog/ProductAttributeControllerTest.php index bfaefb4..4959fd9 100644 --- a/tests/Feature/Catalog/ProductAttributeControllerTest.php +++ b/tests/Feature/Catalog/ProductAttributeControllerTest.php @@ -42,11 +42,11 @@ class ProductAttributeControllerTest extends TestCase $response ->assertCreated() - ->assertJsonPath('tenant_codigo', 'acme') - ->assertJsonPath('codigo', 'color') - ->assertJsonPath('type', 'select') - ->assertJsonPath('options.0.label', 'Red') - ->assertJsonPath('options.1.metadata.hex', '#0000ff'); + ->assertJsonPath('data.tenant_codigo', 'acme') + ->assertJsonPath('data.codigo', 'color') + ->assertJsonPath('data.type', 'select') + ->assertJsonPath('data.options.0.label', 'Red') + ->assertJsonPath('data.options.1.metadata.hex', '#0000ff'); $this->assertDatabaseHas('productos_attributes', [ 'tenant_codigo' => 'acme', @@ -60,7 +60,7 @@ class ProductAttributeControllerTest extends TestCase ]); } - public function test_it_rejects_options_for_text_attributes(): void + public function test_it_rejects_options_for_string_attributes(): void { Tenant::create([ 'codigo' => 'acme', @@ -71,7 +71,7 @@ class ProductAttributeControllerTest extends TestCase $response = $this->postJson('/api/tenants/acme/product-attributes', [ 'codigo' => 'material', 'nombre' => 'Material', - 'type' => 'text', + 'type' => 'string', 'options' => [ ['label' => 'Cotton'], ], diff --git a/tests/Feature/Tenant/BootstrapTenantControllerTest.php b/tests/Feature/Tenant/BootstrapTenantControllerTest.php index 789d1ff..8a5e069 100644 --- a/tests/Feature/Tenant/BootstrapTenantControllerTest.php +++ b/tests/Feature/Tenant/BootstrapTenantControllerTest.php @@ -33,9 +33,9 @@ class BootstrapTenantControllerTest extends TestCase $response ->assertOk() - ->assertJsonPath('codigo', 'acme') - ->assertJsonPath('dominio', 'acme.com') - ->assertJsonPath('props.primary_color', 'blue'); + ->assertJsonPath('data.codigo', 'acme') + ->assertJsonPath('data.dominio', 'acme.com') + ->assertJsonPath('data.props.primary_color', 'blue'); } public function test_it_bootstraps_a_tenant_from_a_full_url(): void @@ -52,8 +52,8 @@ class BootstrapTenantControllerTest extends TestCase $response ->assertOk() - ->assertJsonPath('codigo', 'acme') - ->assertJsonPath('dominio', 'acme.com'); + ->assertJsonPath('data.codigo', 'acme') + ->assertJsonPath('data.dominio', 'acme.com'); } public function test_it_returns_not_found_when_the_domain_does_not_exist(): void @@ -84,8 +84,8 @@ class BootstrapTenantControllerTest extends TestCase $firstResponse ->assertCreated() - ->assertJsonPath('dominio', 'acme.com') - ->assertJsonPath('props.primary_color', 'blue'); + ->assertJsonPath('data.dominio', 'acme.com') + ->assertJsonPath('data.props.primary_color', 'blue'); $this->assertDatabaseHas('tenant_prop_values', [ 'tenant_codigo' => 'acme', @@ -137,9 +137,9 @@ class BootstrapTenantControllerTest extends TestCase $successfulResponse ->assertOk() - ->assertJsonPath('nombre', 'Acme Updated') - ->assertJsonPath('dominio', 'acme.com') - ->assertJsonPath('props.primary_color', 'green'); + ->assertJsonPath('data.nombre', 'Acme Updated') + ->assertJsonPath('data.dominio', 'acme.com') + ->assertJsonPath('data.props.primary_color', 'green'); $failingResponse = $this->putJson("/api/tenants/{$otherTenant->id}", [ 'codigo' => 'globex', diff --git a/tests/Feature/Tenant/TenantPropControllerTest.php b/tests/Feature/Tenant/TenantPropControllerTest.php index 490b628..0515016 100644 --- a/tests/Feature/Tenant/TenantPropControllerTest.php +++ b/tests/Feature/Tenant/TenantPropControllerTest.php @@ -21,8 +21,8 @@ class TenantPropControllerTest extends TestCase $response ->assertCreated() - ->assertJsonPath('codigo', 'primary_color') - ->assertJsonPath('data_type', 'string'); + ->assertJsonPath('data.codigo', 'primary_color') + ->assertJsonPath('data.data_type', 'string'); $this->assertDatabaseHas('tenant_props', [ 'codigo' => 'primary_color',