Jelajahi Sumber

refactor: fluxo para bloquear valores de rascunho de student

Gustavo Mantovani 1 hari lalu
induk
melakukan
cfb83e5450

+ 41 - 0
app/Console/Commands/ClearStudentRegistrationDrafts.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Console\Commands;
+
+use App\Services\StudentRegistrationDraftService;
+use Illuminate\Console\Command;
+
+class ClearStudentRegistrationDrafts extends Command
+{
+    /**
+     * The name and signature of the console command.
+     *
+     * @var string
+     */
+    protected $signature = 'students:clear-registration-drafts';
+
+    /**
+     * The console command description.
+     *
+     * @var string
+     */
+    protected $description = 'Remove os rascunhos de estudantes e suas reservas de CPF e e-mail';
+
+    /**
+     * Execute the console command.
+     */
+    public function handle(StudentRegistrationDraftService $service): int
+    {
+        $clearedDrafts = $service->clearAll();
+
+        $this->components->info(
+            trans_choice(
+                '{0} Nenhum rascunho de estudante foi encontrado.|{1} Um rascunho de estudante foi removido.|[2,*] :count rascunhos de estudantes foram removidos.',
+                $clearedDrafts,
+                ['count' => $clearedDrafts],
+            ),
+        );
+
+        return self::SUCCESS;
+    }
+}

+ 150 - 40
app/Services/StudentRegistrationDraftService.php

@@ -4,6 +4,7 @@
 
 use App\Models\User;
 use Closure;
+use Illuminate\Cache\DatabaseStore;
 use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\Validator;
 use Illuminate\Support\Str;
@@ -17,6 +18,8 @@ class StudentRegistrationDraftService
 
     private const LOCK_SECONDS = 60;
 
+    private const TOKENS_KEY = self::CACHE_PREFIX.'tokens';
+
     private const UNIQUE_FIELDS = [
         'document_number',
         'email',
@@ -98,6 +101,8 @@ public function store(User $user, array $data): array
 
             Cache::put($this->draftKey($token), $draft, $expiresAt);
 
+            $this->registerToken($token);
+
             return [
                 'registration_draft_token' => $token,
                 'expires_at'               => $draft['expires_at'],
@@ -105,22 +110,7 @@ public function store(User $user, array $data): array
         });
     }
 
-    public function findOwnedDraft(User $user, string $token, ?int $unitId = null): ?array
-    {
-        $draft = Cache::get($this->draftKey($token));
-
-        if (! is_array($draft)) {
-            return null;
-        }
-
-        $unitId ??= $this->resolveUnitId($user);
-
-        if ($draft['user_id'] !== $user->id || $draft['unit_id'] !== $unitId) {
-            return null;
-        }
-
-        return $draft;
-    }
+    //
 
     public function consume(User $user, string $token, Closure $callback): mixed
     {
@@ -146,6 +136,8 @@ public function consume(User $user, string $token, Closure $callback): mixed
 
             Cache::forget($this->draftKey($token));
 
+            $this->unregisterToken($token);
+
             return $result;
         });
     }
@@ -171,46 +163,85 @@ public function executeWithoutDraft(User $user, array $data, Closure $callback):
         });
     }
 
+    public function findOwnedDraft(User $user, string $token, ?int $unitId = null): ?array
+    {
+        $draft = Cache::get($this->draftKey($token));
+
+        if (! is_array($draft)) {
+            return null;
+        }
+
+        $unitId ??= $this->resolveUnitId($user);
+
+        if ($draft['user_id'] !== $user->id || $draft['unit_id'] !== $unitId) {
+            return null;
+        }
+
+        return $draft;
+    }
+
     //
 
-    private function resolveUnitId(User $user): int
+    public function clearAll(): int
     {
-        $activeUnitId = request()->input('active_unit_id');
+        return Cache::lock(self::LOCK_KEY, self::LOCK_SECONDS)->block(5, function (): int {
+            $databaseStoreResult = $this->clearDatabaseStore();
 
-        if ($activeUnitId) {
-            $unit = $user->units()->where('units.id', $activeUnitId)->first();
+            if ($databaseStoreResult !== null) {
+                return $databaseStoreResult;
+            }
 
-            abort_if(! $unit, 403, 'Unidade não autorizada para este usuário.');
+            $clearedDrafts = 0;
 
-            return $unit->id;
-        }
+            foreach ($this->registeredTokens() as $token) {
+                $draft = Cache::get($this->draftKey($token));
 
-        $unit = $user->units()->first();
+                if (is_array($draft)) {
+                    $this->releaseReservations($draft['reservations'] ?? [], $token);
+                    $clearedDrafts++;
+                }
 
-        abort_if(! $unit, 403, 'Usuário sem unidade associada.');
+                Cache::forget($this->draftKey($token));
+            }
 
-        return $unit->id;
+            Cache::forget(self::TOKENS_KEY);
+
+            return $clearedDrafts;
+        });
     }
 
-    private function reservationKeys(array $data): array
+    private function clearDatabaseStore(): ?int
     {
-        $keys = [];
+        $store = Cache::getStore();
 
-        foreach (self::UNIQUE_FIELDS as $field) {
-            $value = $data[$field] ?? null;
+        if (!$store instanceof DatabaseStore) {
+            return null;
+        }
 
-            if (! is_string($value) || $value === '') {
-                continue;
-            }
+        $table = config('cache.stores.'.config('cache.default').'.table', 'cache');
 
-            $normalizedValue = Str::lower(trim($value));
+        $keyPrefix = $store->getPrefix().self::CACHE_PREFIX;
 
-            $keys[$field] = self::CACHE_PREFIX."reservation:{$field}:".hash('sha256', $normalizedValue);
+        $keys = $store->getConnection()
+            ->table($table)
+            ->where('key', 'like', $keyPrefix.'%')
+            ->pluck('key')
+            ->filter(fn (string $key): bool => str_starts_with($key, $keyPrefix));
+
+        $clearedDrafts = $keys
+            ->map(fn (string $key): string => substr($key, strlen($keyPrefix)))
+            ->filter(fn (string $key): bool => Str::isUuid($key))
+            ->count();
+
+        if ($keys->isNotEmpty()) {
+            $store->getConnection()->table($table)->whereIn('key', $keys)->delete();
         }
 
-        return $keys;
+        return $clearedDrafts;
     }
 
+    //
+
     private function ownsAllReservations(array $reservations, string $token): bool
     {
         foreach ($reservations as $key) {
@@ -231,6 +262,32 @@ private function releaseReservations(array $reservations, string $token): void
         }
     }
 
+    private function registeredTokens(): array
+    {
+        $tokens = Cache::get(self::TOKENS_KEY, []);
+
+        return is_array($tokens) ? $tokens : [];
+    }
+
+    private function reservationKeys(array $data): array
+    {
+        $keys = [];
+
+        foreach (self::UNIQUE_FIELDS as $field) {
+            $value = $data[$field] ?? null;
+
+            if (! is_string($value) || $value === '') {
+                continue;
+            }
+
+            $normalizedValue = Str::lower(trim($value));
+
+            $keys[$field] = self::CACHE_PREFIX."reservation:{$field}:".hash('sha256', $normalizedValue);
+        }
+
+        return $keys;
+    }
+
     private function validateDatabaseUniques(array $data): void
     {
         Validator::make($data, [
@@ -248,15 +305,68 @@ private function uniqueReservationConflict(string $field): never
         ]);
     }
 
-    private function invalidToken(): never
+    //
+
+    private function resolveUnitId(User $user): int
     {
-        throw ValidationException::withMessages([
-            'registration_draft_token' => __('validation.registration_draft_invalid'),
-        ]);
+        $activeUnitId = request()->input('active_unit_id');
+
+        if ($activeUnitId) {
+            $unit = $user->units()->where('units.id', $activeUnitId)->first();
+
+            abort_if(! $unit, 403, 'Unidade não autorizada para este usuário.');
+
+            return $unit->id;
+        }
+
+        $unit = $user->units()->first();
+
+        abort_if(! $unit, 403, 'Usuário sem unidade associada.');
+
+        return $unit->id;
+    }
+
+    //
+
+    private function registerToken(string $token): void
+    {
+        $tokens = array_values(array_filter(
+            $this->registeredTokens(),
+            fn (string $registeredToken): bool => Cache::has($this->draftKey($registeredToken)),
+        ));
+
+        $tokens[] = $token;
+
+        Cache::forever(self::TOKENS_KEY, array_values(array_unique($tokens)));
     }
 
+    private function unregisterToken(string $token): void
+    {
+        $tokens = array_values(array_filter(
+            $this->registeredTokens(),
+            fn (string $registeredToken): bool => $registeredToken !== $token,
+        ));
+
+        if ($tokens === []) {
+            Cache::forget(self::TOKENS_KEY);
+
+            return;
+        }
+
+        Cache::forever(self::TOKENS_KEY, $tokens);
+    }
+
+    //
+
     private function draftKey(string $token): string
     {
         return self::CACHE_PREFIX.$token;
     }
+
+    private function invalidToken(): never
+    {
+        throw ValidationException::withMessages([
+            'registration_draft_token' => __('validation.registration_draft_invalid'),
+        ]);
+    }
 }