From 55694b2048f5e862c11848ba4392fcd4a3237d17 Mon Sep 17 00:00:00 2001 From: ncoronel Date: Mon, 22 Jun 2026 13:13:22 -0300 Subject: [PATCH] Refactor Prop model to use enum for data types; remove PropDataType model and update related requests and migrations --- app/Domains/Prop/Models/Prop.php | 23 +- app/Domains/Prop/Models/PropDataType.php | 25 -- .../Prop/Requests/StorePropRequest.php | 3 +- .../Prop/Requests/UpdatePropRequest.php | 3 +- app/Domains/Prop/Resources/PropResource.php | 8 +- app/Domains/Prop/Support/PropDataType.php | 26 ++ app/Domains/Prop/Support/PropableModels.php | 5 + ...2_120000_create_props_data_types_table.php | 29 -- .../2026_06_22_120100_create_props_table.php | 2 +- ...130000_convert_prop_data_types_to_enum.php | 106 ++++++++ ...nt-prop-management.postman_collection.json | 249 ++++++++++++++++++ tests/Feature/Prop/PropControllerTest.php | 45 ++++ 12 files changed, 454 insertions(+), 70 deletions(-) delete mode 100644 app/Domains/Prop/Models/PropDataType.php create mode 100644 app/Domains/Prop/Support/PropDataType.php delete mode 100644 database/migrations/2026_06_22_120000_create_props_data_types_table.php create mode 100644 database/migrations/2026_06_22_130000_convert_prop_data_types_to_enum.php create mode 100644 docs/postman/tenant-prop-management.postman_collection.json create mode 100644 tests/Feature/Prop/PropControllerTest.php diff --git a/app/Domains/Prop/Models/Prop.php b/app/Domains/Prop/Models/Prop.php index 813fb24..9b2e6c3 100644 --- a/app/Domains/Prop/Models/Prop.php +++ b/app/Domains/Prop/Models/Prop.php @@ -2,11 +2,11 @@ namespace App\Domains\Prop\Models; +use App\Domains\Prop\Support\PropDataType; use Illuminate\Database\Eloquent\Attributes\Fillable; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Relations\BelongsTo; use Illuminate\Database\Eloquent\Relations\HasMany; #[Fillable([ @@ -14,12 +14,23 @@ use Illuminate\Database\Eloquent\Relations\HasMany; 'codigo', 'nombre', 'is_required', - 'data_type_id', + 'data_type', ])] class Prop extends Model { use HasFactory; + /** + * @return array + */ + protected function casts(): array + { + return [ + 'is_required' => 'boolean', + 'data_type' => PropDataType::class, + ]; + } + /** * @param Builder $query */ @@ -28,14 +39,6 @@ class Prop extends Model $query->where('propable_type', is_string($model) ? $model : $model::class); } - /** - * @return BelongsTo - */ - public function dataType(): BelongsTo - { - return $this->belongsTo(PropDataType::class, 'data_type_id'); - } - /** * @return HasMany */ diff --git a/app/Domains/Prop/Models/PropDataType.php b/app/Domains/Prop/Models/PropDataType.php deleted file mode 100644 index e93aad0..0000000 --- a/app/Domains/Prop/Models/PropDataType.php +++ /dev/null @@ -1,25 +0,0 @@ - - */ - public function props(): HasMany - { - return $this->hasMany(Prop::class, 'data_type_id'); - } -} diff --git a/app/Domains/Prop/Requests/StorePropRequest.php b/app/Domains/Prop/Requests/StorePropRequest.php index cf5157a..181b2b8 100644 --- a/app/Domains/Prop/Requests/StorePropRequest.php +++ b/app/Domains/Prop/Requests/StorePropRequest.php @@ -2,6 +2,7 @@ namespace App\Domains\Prop\Requests; +use App\Domains\Prop\Support\PropDataType; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; @@ -21,7 +22,7 @@ class StorePropRequest extends FormRequest 'codigo' => ['required', 'string', 'max:255', Rule::unique('props', 'codigo')], 'nombre' => ['required', 'string', 'max:255'], 'is_required' => ['sometimes', 'boolean'], - 'data_type_id' => ['required', 'integer', 'exists:props_data_types,id'], + 'data_type' => ['required', Rule::enum(PropDataType::class)], ]; } } diff --git a/app/Domains/Prop/Requests/UpdatePropRequest.php b/app/Domains/Prop/Requests/UpdatePropRequest.php index ebe07ae..9fc5c90 100644 --- a/app/Domains/Prop/Requests/UpdatePropRequest.php +++ b/app/Domains/Prop/Requests/UpdatePropRequest.php @@ -3,6 +3,7 @@ namespace App\Domains\Prop\Requests; use App\Domains\Prop\Models\Prop; +use App\Domains\Prop\Support\PropDataType; use Illuminate\Foundation\Http\FormRequest; use Illuminate\Validation\Rule; @@ -25,7 +26,7 @@ class UpdatePropRequest extends FormRequest 'codigo' => ['required', 'string', 'max:255', Rule::unique('props', 'codigo')->ignore($prop?->id)], 'nombre' => ['required', 'string', 'max:255'], 'is_required' => ['sometimes', 'boolean'], - 'data_type_id' => ['required', 'integer', 'exists:props_data_types,id'], + 'data_type' => ['required', Rule::enum(PropDataType::class)], ]; } } diff --git a/app/Domains/Prop/Resources/PropResource.php b/app/Domains/Prop/Resources/PropResource.php index 4b17ec9..79b421d 100644 --- a/app/Domains/Prop/Resources/PropResource.php +++ b/app/Domains/Prop/Resources/PropResource.php @@ -2,11 +2,13 @@ namespace App\Domains\Prop\Resources; +use App\Domains\Prop\Models\Prop; +use App\Domains\Prop\Support\PropableModels; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\JsonResource; /** - * @mixin \App\Domains\Prop\Models\Prop + * @mixin Prop */ class PropResource extends JsonResource { @@ -17,11 +19,11 @@ class PropResource extends JsonResource { return [ 'id' => $this->id, - 'propable_type' => $this->propable_type, + 'propable_type' => PropableModels::aliasFor($this->propable_type) ?? $this->propable_type, 'codigo' => $this->codigo, 'nombre' => $this->nombre, 'is_required' => $this->is_required, - 'data_type_id' => $this->data_type_id, + 'data_type' => $this->data_type?->value, 'created_at' => $this->created_at, 'updated_at' => $this->updated_at, ]; diff --git a/app/Domains/Prop/Support/PropDataType.php b/app/Domains/Prop/Support/PropDataType.php new file mode 100644 index 0000000..4de2f95 --- /dev/null +++ b/app/Domains/Prop/Support/PropDataType.php @@ -0,0 +1,26 @@ + + */ + public static function values(): array + { + return array_map( + static fn (self $type): string => $type->value, + self::cases(), + ); + } +} diff --git a/app/Domains/Prop/Support/PropableModels.php b/app/Domains/Prop/Support/PropableModels.php index de3d7ea..f3b751b 100644 --- a/app/Domains/Prop/Support/PropableModels.php +++ b/app/Domains/Prop/Support/PropableModels.php @@ -39,4 +39,9 @@ class PropableModels return $modelClass; } + + public static function aliasFor(string $modelClass): ?string + { + return array_search($modelClass, static::map(), true) ?: null; + } } diff --git a/database/migrations/2026_06_22_120000_create_props_data_types_table.php b/database/migrations/2026_06_22_120000_create_props_data_types_table.php deleted file mode 100644 index ae7b41c..0000000 --- a/database/migrations/2026_06_22_120000_create_props_data_types_table.php +++ /dev/null @@ -1,29 +0,0 @@ -id(); - $table->string('name'); - $table->string('code')->unique(); - $table->timestamps(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('props_data_types'); - } -}; diff --git a/database/migrations/2026_06_22_120100_create_props_table.php b/database/migrations/2026_06_22_120100_create_props_table.php index df83c5e..87aec31 100644 --- a/database/migrations/2026_06_22_120100_create_props_table.php +++ b/database/migrations/2026_06_22_120100_create_props_table.php @@ -17,7 +17,7 @@ return new class extends Migration $table->string('codigo')->unique(); $table->string('nombre'); $table->boolean('is_required')->default(false); - $table->foreignId('data_type_id')->constrained('props_data_types')->cascadeOnUpdate()->restrictOnDelete(); + $table->string('data_type'); $table->timestamps(); $table->index('propable_type'); diff --git a/database/migrations/2026_06_22_130000_convert_prop_data_types_to_enum.php b/database/migrations/2026_06_22_130000_convert_prop_data_types_to_enum.php new file mode 100644 index 0000000..edee49e --- /dev/null +++ b/database/migrations/2026_06_22_130000_convert_prop_data_types_to_enum.php @@ -0,0 +1,106 @@ +string('data_type')->nullable()->after('is_required'); + }); + + if (Schema::hasTable('props_data_types')) { + $typeMap = DB::table('props_data_types') + ->pluck('code', 'id') + ->map(static fn (mixed $code): string => in_array($code, PropDataType::values(), true) ? (string) $code : PropDataType::String->value) + ->all(); + + DB::table('props') + ->select(['id', 'data_type_id']) + ->orderBy('id') + ->get() + ->each(function (object $prop) use ($typeMap): void { + DB::table('props') + ->where('id', $prop->id) + ->update([ + 'data_type' => $typeMap[$prop->data_type_id] ?? PropDataType::String->value, + ]); + }); + } else { + DB::table('props')->update([ + 'data_type' => PropDataType::String->value, + ]); + } + + Schema::table('props', function (Blueprint $table) { + $table->dropForeign(['data_type_id']); + $table->dropColumn('data_type_id'); + }); + + Schema::table('props', function (Blueprint $table) { + $table->string('data_type')->nullable(false)->change(); + }); + } + + if (Schema::hasTable('props_data_types')) { + Schema::drop('props_data_types'); + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + if (! Schema::hasTable('props_data_types')) { + Schema::create('props_data_types', function (Blueprint $table) { + $table->id(); + $table->string('name'); + $table->string('code')->unique(); + $table->timestamps(); + }); + + DB::table('props_data_types')->insert([ + [ + 'id' => 1, + 'name' => 'String', + 'code' => PropDataType::String->value, + 'created_at' => now(), + 'updated_at' => now(), + ], + ]); + } + + if (Schema::hasTable('props') && Schema::hasColumn('props', 'data_type') && ! Schema::hasColumn('props', 'data_type_id')) { + Schema::table('props', function (Blueprint $table) { + $table->foreignId('data_type_id')->nullable()->after('is_required')->constrained('props_data_types')->cascadeOnUpdate()->restrictOnDelete(); + }); + + $stringTypeId = DB::table('props_data_types') + ->where('code', PropDataType::String->value) + ->value('id'); + + DB::table('props')->update([ + 'data_type_id' => $stringTypeId, + ]); + + Schema::table('props', function (Blueprint $table) { + $table->foreignId('data_type_id')->nullable(false)->change(); + $table->dropColumn('data_type'); + }); + } + } +}; diff --git a/docs/postman/tenant-prop-management.postman_collection.json b/docs/postman/tenant-prop-management.postman_collection.json new file mode 100644 index 0000000..d613d4b --- /dev/null +++ b/docs/postman/tenant-prop-management.postman_collection.json @@ -0,0 +1,249 @@ +{ + "info": { + "_postman_id": "b0b0df2c-4e1e-4ef4-b6a8-1c5348d5b6f2", + "name": "Shopit Back - Tenants and Props", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "variable": [ + { + "key": "base_url", + "value": "http://127.0.0.1:8000/api" + }, + { + "key": "tenant_id", + "value": "1" + }, + { + "key": "prop_id", + "value": "1" + }, + { + "key": "propable_type", + "value": "tenant" + } + ], + "item": [ + { + "name": "Tenants", + "item": [ + { + "name": "List Tenants", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/tenants", + "host": [ + "{{base_url}}" + ], + "path": [ + "tenants" + ] + } + } + }, + { + "name": "Create Tenant", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"codigo\": \"tenant-demo\",\n \"nombre\": \"Tenant Demo\",\n \"dominio\": \"tenant-demo.local\",\n \"props\": {\n \"color_primario\": \"#ff6600\",\n \"pais\": \"AR\"\n }\n}" + }, + "url": { + "raw": "{{base_url}}/tenants", + "host": [ + "{{base_url}}" + ], + "path": [ + "tenants" + ] + } + } + }, + { + "name": "Show Tenant", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/tenants/{{tenant_id}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "tenants", + "{{tenant_id}}" + ] + } + } + }, + { + "name": "Update Tenant", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"codigo\": \"tenant-demo\",\n \"nombre\": \"Tenant Demo Updated\",\n \"dominio\": \"tenant-demo.example.com\",\n \"props\": {\n \"color_primario\": \"#0055ff\",\n \"pais\": \"UY\"\n }\n}" + }, + "url": { + "raw": "{{base_url}}/tenants/{{tenant_id}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "tenants", + "{{tenant_id}}" + ] + } + } + }, + { + "name": "Delete Tenant", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{base_url}}/tenants/{{tenant_id}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "tenants", + "{{tenant_id}}" + ] + } + } + } + ] + }, + { + "name": "Props", + "item": [ + { + "name": "List Props By Model", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/prop-models/{{propable_type}}/props", + "host": [ + "{{base_url}}" + ], + "path": [ + "prop-models", + "{{propable_type}}", + "props" + ] + } + } + }, + { + "name": "Create Prop For Model", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"codigo\": \"color_primario\",\n \"nombre\": \"Color primario\",\n \"is_required\": false,\n \"data_type\": \"string\"\n}" + }, + "url": { + "raw": "{{base_url}}/prop-models/{{propable_type}}/props", + "host": [ + "{{base_url}}" + ], + "path": [ + "prop-models", + "{{propable_type}}", + "props" + ] + } + } + }, + { + "name": "Show Prop", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{base_url}}/prop-models/{{propable_type}}/props/{{prop_id}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "prop-models", + "{{propable_type}}", + "props", + "{{prop_id}}" + ] + } + } + }, + { + "name": "Update Prop", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"codigo\": \"color_primario\",\n \"nombre\": \"Color primario actualizado\",\n \"is_required\": true,\n \"data_type\": \"string\"\n}" + }, + "url": { + "raw": "{{base_url}}/prop-models/{{propable_type}}/props/{{prop_id}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "prop-models", + "{{propable_type}}", + "props", + "{{prop_id}}" + ] + } + } + }, + { + "name": "Delete Prop", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{base_url}}/prop-models/{{propable_type}}/props/{{prop_id}}", + "host": [ + "{{base_url}}" + ], + "path": [ + "prop-models", + "{{propable_type}}", + "props", + "{{prop_id}}" + ] + } + } + } + ] + } + ] +} diff --git a/tests/Feature/Prop/PropControllerTest.php b/tests/Feature/Prop/PropControllerTest.php new file mode 100644 index 0000000..50f1928 --- /dev/null +++ b/tests/Feature/Prop/PropControllerTest.php @@ -0,0 +1,45 @@ +postJson('/api/prop-models/tenant/props', [ + 'codigo' => 'color_primario', + 'nombre' => 'Color primario', + 'is_required' => false, + 'data_type' => 'string', + ]); + + $response + ->assertCreated() + ->assertJsonPath('propable_type', 'tenant') + ->assertJsonPath('data_type', 'string'); + + $this->assertDatabaseHas('props', [ + 'codigo' => 'color_primario', + 'data_type' => 'string', + ]); + } + + public function test_it_rejects_unknown_enum_data_type(): void + { + $response = $this->postJson('/api/prop-models/tenant/props', [ + 'codigo' => 'color_primario', + 'nombre' => 'Color primario', + 'is_required' => false, + 'data_type' => 'unsupported-type', + ]); + + $response + ->assertUnprocessable() + ->assertJsonValidationErrors(['data_type']); + } +}