Gustavo Zanatta 1 день тому
батько
коміт
ada73af122

+ 8 - 2
app/Http/Controllers/UserController.php

@@ -7,9 +7,11 @@ use App\Services\MediaService;
 use App\Services\UserService;
 use App\Http\Requests\UserRequest;
 use App\Http\Resources\UserResource;
+use App\Support\Cpf;
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;
 use Illuminate\Support\Facades\Auth;
+use Illuminate\Validation\Rule;
 
 class UserController extends Controller
 {
@@ -120,10 +122,14 @@ class UserController extends Controller
 
     public function updateMyProfile(Request $request): JsonResponse
     {
+        if ($request->has('cpf')) {
+            $request->merge(['cpf' => Cpf::digits($request->input('cpf'))]);
+        }
+
         $data = $request->validate([
             'name'           => 'sometimes|string|max:255',
-            'cpf'            => 'sometimes|string|max:20',
-            'email'          => 'sometimes|email|max:255',
+            'cpf'            => ['sometimes', 'string', 'digits:11', Rule::unique('users', 'cpf')->ignore(Auth::id())],
+            'email'          => ['sometimes', 'email', 'max:255', Rule::unique('users', 'email')->ignore(Auth::id())],
             'phone'          => 'sometimes|string|max:20',
             'position_id'    => 'sometimes|nullable|integer|exists:positions,id',
             'sector_id'      => 'sometimes|nullable|integer|exists:sectors,id',

+ 13 - 1
app/Http/Requests/FirstAccessRegisterRequest.php

@@ -4,12 +4,24 @@ namespace App\Http\Requests;
 
 use App\Enums\UserTypeEnum;
 use App\Models\User;
+use App\Support\Cpf;
 use Illuminate\Foundation\Http\FormRequest;
 use Illuminate\Validation\Rule;
 use Illuminate\Validation\Rules\Password;
 
 class FirstAccessRegisterRequest extends FormRequest
 {
+    protected function prepareForValidation(): void
+    {
+        if ($this->has('cpf')) {
+            $this->merge(['cpf' => Cpf::digits($this->input('cpf'))]);
+        }
+
+        if ($this->has('registration')) {
+            $this->merge(['registration' => trim((string) $this->input('registration'))]);
+        }
+    }
+
     public function rules(): array
     {
         $user   = $this->existingUser();
@@ -20,7 +32,7 @@ class FirstAccessRegisterRequest extends FormRequest
             'registration' => ['required', 'string', 'max:255', Rule::unique('users', 'registration')->ignore($userId)],
             'name'         => 'required|string|min:3|max:120',
             'last_name'    => 'required|string|min:3|max:120',
-            'cpf'          => ['required', 'string', 'max:14', Rule::unique('users', 'cpf')->ignore($userId)],
+            'cpf'          => ['required', 'string', 'digits:11', Rule::unique('users', 'cpf')->ignore($userId)],
             'email'        => ['required', 'email', 'max:255', Rule::unique('users', 'email')->ignore($userId)],
             'phone'        => 'required|string|max:20',
             'position_id'  => 'required|integer|exists:positions,id',

+ 13 - 1
app/Http/Requests/LandingPageRegisterRequest.php

@@ -4,16 +4,28 @@ namespace App\Http\Requests;
 
 use App\Enums\UserStatusEnum;
 use App\Models\User;
+use App\Support\Cpf;
 use Illuminate\Foundation\Http\FormRequest;
 
 class LandingPageRegisterRequest extends FormRequest
 {
+    protected function prepareForValidation(): void
+    {
+        if ($this->has('cpf')) {
+            $this->merge(['cpf' => Cpf::digits($this->input('cpf'))]);
+        }
+
+        if ($this->has('registration')) {
+            $this->merge(['registration' => trim((string) $this->input('registration'))]);
+        }
+    }
+
     public function rules(): array
     {
         return [
             'name'         => 'required|string|min:3|max:120',
             'last_name'    => 'required|string|min:3|max:120',
-            'cpf'          => 'required|string|max:14|unique:users,cpf',
+            'cpf'          => 'required|string|digits:11|unique:users,cpf',
             'registration' => 'required|string|max:255|unique:users,registration',
             'email'        => 'required|email|unique:users,email',
             'phone'        => 'required|string|max:20',

+ 13 - 1
app/Http/Requests/UserRequest.php

@@ -8,10 +8,22 @@ use App\Enums\{
     UserStatusEnum,
     UserTypeEnum
 };
+use App\Support\Cpf;
 use Illuminate\Validation\Rule;
 
 class UserRequest extends FormRequest
 {
+    protected function prepareForValidation(): void
+    {
+        if ($this->has('cpf')) {
+            $this->merge(['cpf' => Cpf::digits($this->input('cpf'))]);
+        }
+
+        if ($this->has('registration')) {
+            $this->merge(['registration' => trim((string) $this->input('registration'))]);
+        }
+    }
+
     public function rules(): array
     {
         $emailUnique        = 'unique:users,email';
@@ -30,7 +42,7 @@ class UserRequest extends FormRequest
             'password'       => 'sometimes|string|nullable',
             'type'           => ['sometimes', Rule::enum(UserTypeEnum::class)],
             'language'       => ['sometimes', Rule::enum(LanguageEnum::class)],
-            'cpf'            => ['sometimes', 'nullable', 'string', 'max:14', $cpfUnique],
+            'cpf'            => ['sometimes', 'nullable', 'string', 'digits:11', $cpfUnique],
             'registration'   => ['sometimes', 'nullable', 'string', 'max:50', $registrationUnique],
             'status'         => ['sometimes', Rule::enum(UserStatusEnum::class)],
             'admission_date' => 'sometimes|nullable|date',

+ 10 - 0
app/Models/User.php

@@ -5,7 +5,9 @@ namespace App\Models;
 use App\Enums\LanguageEnum;
 use App\Enums\UserStatusEnum;
 use App\Enums\UserTypeEnum;
+use App\Support\Cpf;
 use App\Traits\HasPermissions;
+use Illuminate\Database\Eloquent\Casts\Attribute;
 use Illuminate\Database\Eloquent\Factories\HasFactory;
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
 use Illuminate\Database\Eloquent\Relations\BelongsToMany;
@@ -84,6 +86,14 @@ class User extends Authenticatable
         ];
     }
 
+    protected function cpf(): Attribute
+    {
+        return Attribute::make(
+            get: fn(?string $value): ?string => Cpf::format($value),
+            set: fn(?string $value): ?string => Cpf::digits($value),
+        );
+    }
+
     public function isAdministrador(): bool
     {
         return $this->type === UserTypeEnum::ADMINISTRADOR;

+ 10 - 4
app/Services/AssociateValidationService.php

@@ -3,20 +3,26 @@
 namespace App\Services;
 
 use App\Models\User;
+use App\Support\Cpf;
 
 class AssociateValidationService
 {
     public function validate(array $data): ?User
     {
       $search = $data['search'] ?? null;
+      $cpfSearch = Cpf::digits($search);
+
         return User::query()
 
-            ->where(function ($query) use ($data, $search) {
+            ->where(function ($query) use ($data, $search, $cpfSearch) {
                 if (!empty($search)) {
-                    $query->where(function ($query) use ($search) {
-                        $query->where('cpf', $search)
-                            ->orWhere('registration', $search)
+                    $query->where(function ($query) use ($search, $cpfSearch) {
+                        $query->where('registration', $search)
                             ->orWhere('name', $search);
+
+                        if ($cpfSearch) {
+                            $query->orWhere('cpf', $cpfSearch);
+                        }
                     });
                 }
             })

+ 9 - 3
app/Services/UserAccessLogService.php

@@ -3,6 +3,7 @@
 namespace App\Services;
 
 use App\Models\UserAccessLog;
+use App\Support\Cpf;
 use Illuminate\Pagination\LengthAwarePaginator;
 
 class UserAccessLogService
@@ -26,12 +27,17 @@ class UserAccessLogService
 
         if (!empty($filters['search'])) {
             $term = '%' . mb_strtolower($filters['search']) . '%';
-            $query->where(function ($q) use ($term) {
-                $q->whereHas('user', function ($u) use ($term) {
+            $cpfTerm = ($digits = Cpf::digits($filters['search'])) ? '%' . $digits . '%' : null;
+
+            $query->where(function ($q) use ($term, $cpfTerm) {
+                $q->whereHas('user', function ($u) use ($term, $cpfTerm) {
                     $u->whereRaw('UNACCENT(LOWER(users.name)) LIKE UNACCENT(?)', [$term])
-                      ->orWhereRaw('UNACCENT(LOWER(COALESCE(users.cpf, \'\'))) LIKE UNACCENT(?)', [$term])
                       ->orWhereRaw('UNACCENT(LOWER(COALESCE(users.registration, \'\'))) LIKE UNACCENT(?)', [$term])
                       ->orWhereRaw('LOWER(users.type) LIKE ?', [$term]);
+
+                    if ($cpfTerm) {
+                        $u->orWhere('users.cpf', 'LIKE', $cpfTerm);
+                    }
                 })
                 ->orWhereRaw("TO_CHAR(users_access_logs.accessed_at, 'DD/MM/YYYY HH24:MI:SS') LIKE ?", [$term]);
             });

+ 8 - 2
app/Services/UserService.php

@@ -5,6 +5,7 @@ namespace App\Services;
 use App\Enums\UserStatusEnum;
 use App\Enums\UserTypeEnum;
 use App\Models\User;
+use App\Support\Cpf;
 use Illuminate\Database\Eloquent\Collection;
 use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Str;
@@ -71,11 +72,16 @@ class UserService
 
         if (!empty($filters['search'])) {
             $term = '%' . mb_strtolower($filters['search']) . '%';
-            $query->where(function ($q) use ($term) {
+            $cpfTerm = ($digits = Cpf::digits($filters['search'])) ? '%' . $digits . '%' : null;
+
+            $query->where(function ($q) use ($term, $cpfTerm) {
                 $q->whereRaw('UNACCENT(LOWER(name)) LIKE UNACCENT(?)', [$term])
                   ->orWhereRaw('UNACCENT(LOWER(email)) LIKE UNACCENT(?)', [$term])
-                  ->orWhereRaw('UNACCENT(LOWER(COALESCE(cpf, \'\'))) LIKE UNACCENT(?)', [$term])
                   ->orWhereRaw('UNACCENT(LOWER(COALESCE(registration, \'\'))) LIKE UNACCENT(?)', [$term]);
+
+                if ($cpfTerm) {
+                    $q->orWhere('cpf', 'LIKE', $cpfTerm);
+                }
             });
         }
 

+ 79 - 0
app/Support/Cpf.php

@@ -0,0 +1,79 @@
+<?php
+
+namespace App\Support;
+
+/**
+ * Fonte única de verdade para o formato do CPF.
+ *
+ * Regra: o banco guarda SOMENTE os 11 dígitos, garantindo que a unique de
+ * `users.cpf` compare CPFs de fato — e não strings com máscaras diferentes.
+ * A máscara existe apenas na exibição.
+ */
+class Cpf
+{
+    /**
+     * Formato canônico de persistência: apenas dígitos.
+     */
+    public static function digits(?string $cpf): ?string
+    {
+        if ($cpf === null) {
+            return null;
+        }
+
+        $digits = preg_replace('/\D/', '', $cpf);
+
+        return $digits === '' ? null : $digits;
+    }
+
+    /**
+     * Formato de exibição: xxx.xxx.xxx-xx.
+     *
+     * Normaliza antes de formatar para que registros legados já mascarados
+     * (ou com máscara parcial) também saiam corretos.
+     */
+    public static function format(?string $cpf): ?string
+    {
+        $digits = self::digits($cpf);
+
+        if ($digits === null) {
+            return null;
+        }
+
+        if (strlen($digits) !== 11) {
+            return $digits;
+        }
+
+        return substr($digits, 0, 3) . '.'
+            . substr($digits, 3, 3) . '.'
+            . substr($digits, 6, 3) . '-'
+            . substr($digits, 9, 2);
+    }
+
+    /**
+     * Valida os dígitos verificadores, rejeitando também sequências repetidas.
+     */
+    public static function isValid(?string $cpf): bool
+    {
+        $digits = self::digits($cpf);
+
+        if ($digits === null || strlen($digits) !== 11 || preg_match('/^(\d)\1{10}$/', $digits)) {
+            return false;
+        }
+
+        foreach ([9, 10] as $position) {
+            $sum = 0;
+
+            for ($i = 0; $i < $position; $i++) {
+                $sum += (int) $digits[$i] * (($position + 1) - $i);
+            }
+
+            $checkDigit = (($sum * 10) % 11) % 10;
+
+            if ((int) $digits[$position] !== $checkDigit) {
+                return false;
+            }
+        }
+
+        return true;
+    }
+}