resolveUnitId($user); $requestedToken = $data['registration_draft_token'] ?? null; unset($data['registration_draft_token']); return Cache::lock(self::LOCK_KEY, self::LOCK_SECONDS)->block(5, function () use ( $user, $unitId, $requestedToken, $data, ): array { $existingDraft = null; if ($requestedToken !== null) { $existingDraft = $this->findOwnedDraft($user, $requestedToken, $unitId); if ($existingDraft === null) { $this->invalidToken(); } } $token = $requestedToken ?? (string) Str::uuid(); $expiresAt = now()->addMinutes(config('students.registration_draft_ttl_minutes', 15)); $oldReservations = $existingDraft['reservations'] ?? []; $newReservations = $this->reservationKeys($data); $acquiredReservations = []; $this->validateDatabaseUniques($data); try { foreach ($newReservations as $field => $key) { if (($oldReservations[$field] ?? null) === $key) { if (Cache::get($key) !== $token) { $this->invalidToken(); } continue; } if (! Cache::add($key, $token, $expiresAt) && Cache::get($key) !== $token) { $this->uniqueReservationConflict($field); } $acquiredReservations[$field] = $key; } } catch (ValidationException $exception) { $this->releaseReservations($acquiredReservations, $token); throw $exception; } $removedReservations = array_diff($oldReservations, $newReservations); $this->releaseReservations($removedReservations, $token); foreach ($newReservations as $key) { Cache::put($key, $token, $expiresAt); } $draft = [ 'user_id' => $user->id, 'unit_id' => $unitId, 'data' => $data, 'reservations' => $newReservations, 'expires_at' => $expiresAt->toIso8601String(), ]; Cache::put($this->draftKey($token), $draft, $expiresAt); return [ 'registration_draft_token' => $token, 'expires_at' => $draft['expires_at'], ]; }); } 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 { $unitId = $this->resolveUnitId($user); return Cache::lock(self::LOCK_KEY, self::LOCK_SECONDS)->block(5, function () use ( $user, $token, $unitId, $callback, ): mixed { $draft = $this->findOwnedDraft($user, $token, $unitId); if ($draft === null || ! $this->ownsAllReservations($draft['reservations'], $token)) { $this->invalidToken(); } $this->validateDatabaseUniques($draft['data']); $result = $callback($draft['data'], $unitId); $this->releaseReservations($draft['reservations'], $token); Cache::forget($this->draftKey($token)); return $result; }); } public function executeWithoutDraft(User $user, array $data, Closure $callback): mixed { $unitId = $this->resolveUnitId($user); return Cache::lock(self::LOCK_KEY, self::LOCK_SECONDS)->block(5, function () use ( $data, $unitId, $callback, ): mixed { $this->validateDatabaseUniques($data); foreach ($this->reservationKeys($data) as $field => $key) { if (Cache::has($key)) { $this->uniqueReservationConflict($field); } } return $callback($data, $unitId); }); } private function resolveUnitId(User $user): int { $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 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 ownsAllReservations(array $reservations, string $token): bool { foreach ($reservations as $key) { if (Cache::get($key) !== $token) { return false; } } return true; } private function releaseReservations(array $reservations, string $token): void { foreach ($reservations as $key) { if (Cache::get($key) === $token) { Cache::forget($key); } } } private function validateDatabaseUniques(array $data): void { Validator::make($data, [ 'email' => 'sometimes|nullable|unique:students,email', ])->validate(); } private function uniqueReservationConflict(string $field): never { throw ValidationException::withMessages([ $field => __('validation.unique', [ 'attribute' => __("validation.attributes.{$field}"), ]), ]); } private function invalidToken(): never { throw ValidationException::withMessages([ 'registration_draft_token' => __('validation.registration_draft_invalid'), ]); } private function draftKey(string $token): string { return self::CACHE_PREFIX.$token; } }