Ver código fonte

feat(access-scope): add access_scope field to user and user_type models and update related logic

ebagabee 1 semana atrás
pai
commit
5c040725d4

+ 3 - 0
app/Http/Controllers/UserTypeController.php

@@ -27,6 +27,7 @@ public function index(): JsonResponse
                 'slug'      => $t->slug,
                 'label'     => $t->label,
                 'is_system' => $t->is_system,
+                'access_scope' => $t->access_scope,
             ])->values(),
         );
     }
@@ -36,6 +37,7 @@ public function store(Request $request): JsonResponse
         $data = $request->validate([
             'label' => 'required|string|max:100',
             'slug'  => 'nullable|string|max:100|regex:/^[A-Za-z0-9_]+$/|unique:user_types,slug',
+            'access_scope' => 'required|string|in:unit,franchisor',
         ]);
 
         $item = $this->service->create($data);
@@ -58,6 +60,7 @@ public function update(Request $request, int $id): JsonResponse
                 'regex:/^[A-Za-z0-9_]+$/',
                 Rule::unique('user_types', 'slug')->ignore($id),
             ],
+            'access_scope' => 'required|string|in:unit,franchisor',
         ]);
 
         $item = $this->service->update($id, $data);

+ 2 - 0
app/Http/Requests/UserRequest.php

@@ -21,6 +21,7 @@ public function rules(): array
             'email'     => 'sometimes|email',
             'password'  => 'sometimes|string|nullable|min:8',
             'user_type' => 'sometimes|nullable|string|exists:user_types,slug',
+            'access_scope' => 'sometimes|string|in:unit,franchisor',
             'language'  => ['sometimes', Rule::enum(LanguageEnum::class)],
             'state_id'  => 'sometimes|nullable|integer|exists:states,id',
             'unit_id'    => 'sometimes|nullable|integer|exists:units,id',
@@ -33,6 +34,7 @@ public function rules(): array
             $rules['email']     = 'required|email|unique:users,email';
             $rules['password']  = 'required|string|min:8';
             $rules['user_type'] = 'required|string|exists:user_types,slug';
+            $rules['access_scope'] = 'required|string|in:unit,franchisor';
 
             if (!$this->has('language')) {
                 $this->merge(['language' => LanguageEnum::PORTUGUESE->value]);

+ 1 - 0
app/Http/Resources/UserResource.php

@@ -20,6 +20,7 @@ public function toArray(Request $request): array
             'email'         => $this->email,
             'language'      => $this->language,
             'user_type'       => $this->user_type,
+            'access_scope'    => $this->access_scope,
             'user_type_label' => $this->whenLoaded('userType', fn() => $this->userType?->label),
             'status'        => $this->status,
             'state_id'      => $this->state_id,

+ 1 - 1
app/Http/Resources/UserTypeResource.php

@@ -10,7 +10,7 @@ class UserTypeResource extends JsonResource
 {
     public function toArray(Request $request): array
     {
-        $types = UserType::orderBy('label')->get();
+        $types = $this->resource;
 
         $result = [];
         foreach ($types as $type) {

+ 20 - 3
app/Services/UserService.php

@@ -42,12 +42,13 @@ public function findById(int $id): ?User
 
     public function create(array $data): User
     {
+        $this->assertMatchingAccessScope($data);
         // Suporta unit_ids[] (múltiplas) ou unit_id (retrocompatibilidade)
         $unitIds = $data['unit_ids'] ?? null;
         if (empty($unitIds) && isset($data['unit_id'])) {
             $unitIds = array_filter([$data['unit_id']]);
         }
-        if (empty($unitIds)) {
+        if (empty($unitIds) && ($data['access_scope'] ?? 'unit') === 'unit') {
             $fallback = Auth::user()->units->first()?->id;
             $unitIds  = $fallback ? [$fallback] : [];
         }
@@ -71,6 +72,8 @@ public function update(int $id, array $data): ?User
             return null;
         }
 
+        $this->assertMatchingAccessScope($data, $model);
+
         // Suporta unit_ids[] (múltiplas) com fallback para unit_id (retrocompatibilidade)
         $hasUnitIds = array_key_exists('unit_ids', $data);
         $unitIds    = $data['unit_ids'] ?? null;
@@ -81,7 +84,9 @@ public function update(int $id, array $data): ?User
         $data = $this->handleAvatar($data, $model->avatar_url);
         $model->update($data);
 
-        if ($hasUnitIds) {
+        if (($data['access_scope'] ?? $model->access_scope) === 'franchisor') {
+            $model->units()->sync([]);
+        } elseif ($hasUnitIds) {
             $model->units()->sync(array_filter($unitIds ?? []));
         } elseif ($hasUnitId) {
             $model->units()->sync($unitId ? [$unitId] : []);
@@ -107,7 +112,10 @@ public function delete(int $id): bool
 
     public function getUserTypes(): \Illuminate\Database\Eloquent\Collection
     {
-        return \App\Models\UserType::orderBy('label')->get();
+        return \App\Models\UserType::query()
+            ->when(request()->input('access_scope'), fn ($query, $scope) => $query->where('access_scope', $scope))
+            ->orderBy('label')
+            ->get();
     }
 
     private function handleAvatar(array $data, ?string $oldAvatarPath = null): array
@@ -131,4 +139,13 @@ private function handleAvatar(array $data, ?string $oldAvatarPath = null): array
         unset($data['avatar']);
         return $data;
     }
+
+    private function assertMatchingAccessScope(array $data, ?User $user = null): void
+    {
+        $slug = $data['user_type'] ?? $user?->user_type;
+        $scope = $data['access_scope'] ?? $user?->access_scope;
+        $type = \App\Models\UserType::where('slug', $slug)->first();
+
+        abort_if($type && $scope && $type->access_scope !== $scope, 422, 'O perfil não pertence ao tipo de acesso selecionado.');
+    }
 }

+ 2 - 0
app/Services/UserTypeService.php

@@ -24,6 +24,7 @@ public function create(array $data): UserType
             'slug'      => $this->normalizeSlug($data['slug'] ?? $data['label']),
             'label'     => $data['label'],
             'is_system' => false,
+            'access_scope' => $data['access_scope'],
         ]);
     }
 
@@ -41,6 +42,7 @@ public function update(int $id, array $data): ?UserType
         $model->update([
             'slug'  => $this->normalizeSlug($data['slug'] ?? $model->slug),
             'label' => $data['label'] ?? $model->label,
+            'access_scope' => $data['access_scope'] ?? $model->access_scope,
         ]);
 
         return $model->fresh();

+ 25 - 0
database/migrations/2026_07_17_000001_add_access_scope_to_user_types_table.php

@@ -0,0 +1,25 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('user_types', function (Blueprint $table) {
+            $table->string('access_scope', 20)->default('unit')->after('is_system');
+        });
+
+        DB::table('user_types')->where('slug', 'ADMIN')->update(['access_scope' => 'franchisor']);
+    }
+
+    public function down(): void
+    {
+        Schema::table('user_types', function (Blueprint $table) {
+            $table->dropColumn('access_scope');
+        });
+    }
+};

+ 25 - 0
database/migrations/2026_07_17_000002_add_access_scope_to_users_table.php

@@ -0,0 +1,25 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::table('users', function (Blueprint $table) {
+            $table->string('access_scope', 20)->default('unit')->after('user_type');
+        });
+
+        DB::table('users')->where('user_type', 'ADMIN')->update(['access_scope' => 'franchisor']);
+    }
+
+    public function down(): void
+    {
+        Schema::table('users', function (Blueprint $table) {
+            $table->dropColumn('access_scope');
+        });
+    }
+};

+ 2 - 0
database/seeders/UserSeeder.php

@@ -16,6 +16,7 @@ public function run(): void
                 'name'      => 'suporte',
                 'password'  => 'S@ft2080.',
                 'user_type' => UserTypeEnum::ADMIN,
+                'access_scope' => 'franchisor',
             ]
         );
 
@@ -25,6 +26,7 @@ public function run(): void
                 'name'      => 'Perfil da Franqueadora',
                 'password'  => 'S@ft2080.',
                 'user_type' => UserTypeEnum::ADMIN,
+                'access_scope' => 'franchisor',
             ]
         );
 

+ 2 - 2
database/seeders/UserTypeSeeder.php

@@ -10,8 +10,8 @@ class UserTypeSeeder extends Seeder
     public function run(): void
     {
         $types = [
-            ['slug' => 'ADMIN',            'label' => 'Administrador (Franqueadora)', 'is_system' => true],
-            ['slug' => 'ADMIN_FRANCHISEE', 'label' => 'Administrador (Franqueada)',   'is_system' => true],
+            ['slug' => 'ADMIN',            'label' => 'Administrador (Franqueadora)', 'is_system' => true,  'access_scope' => 'franchisor'],
+            ['slug' => 'ADMIN_FRANCHISEE', 'label' => 'Administrador (Franqueada)',   'is_system' => true,  'access_scope' => 'unit'],
             ['slug' => 'NEUROTRAINER',     'label' => 'Neurotrainer',                 'is_system' => false],
             ['slug' => 'PROFESSOR',        'label' => 'Professor',                    'is_system' => false],
             ['slug' => 'FINANCIAL',        'label' => 'Financeiro',                   'is_system' => false],