findByRegistration($registration); if ($user?->first_access_completed_at) { return ['status' => self::STATUS_DONE]; } return [ 'status' => $user ? self::STATUS_PENDING : self::STATUS_NOT_FOUND, 'token' => $this->issueToken($registration), 'registration' => $registration, 'user' => $user ? $this->prefill($user) : null, 'missing_fields' => $user ? $this->missingFields($user) : self::PROFILE_FIELDS, ]; } /** * Conclui o primeiro acesso: cria o associado (ou completa o existente), define a senha e a foto. */ public function register(array $data, UploadedFile $photo): User { $registration = $data['registration']; $this->assertValidToken($data['token'] ?? null, $registration); $user = $this->findByRegistration($registration); if ($user?->first_access_completed_at) { throw ValidationException::withMessages([ 'registration' => __('messages.first_access.already_done'), ]); } return DB::transaction(function () use ($data, $photo, $registration, $user): User { if ($user) { // Associado já existente (importação ou landing page): completa apenas o que falta, // sem sobrescrever o que a administração já cadastrou, e mantém o status atual. foreach (self::PROFILE_FIELDS as $field) { if ($this->isEmpty($user->{$field})) { $user->{$field} = $data[$field]; } } } else { $user = new User([ 'registration' => $registration, 'type' => UserTypeEnum::ASSOCIADO, 'status' => UserStatusEnum::PENDING, 'language' => LanguageEnum::PORTUGUESE, ]); foreach (self::PROFILE_FIELDS as $field) { $user->{$field} = $data[$field]; } } $user->password = $data['password']; $user->first_access_completed_at = now(); $user->save(); $this->mediaService->uploadUserAvatar($photo, $user); return $user->fresh(); }); } private function findByRegistration(string $registration): ?User { return User::where('type', UserTypeEnum::ASSOCIADO) ->where('registration', $registration) ->first(); } private function prefill(User $user): array { return [ 'name' => $user->name, 'cpf' => $user->cpf, 'email' => $user->email, 'phone' => $user->phone, 'position_id' => $user->position_id, 'sector_id' => $user->sector_id, 'photo_url' => $user->photo_path ? Storage::disk('s3')->temporaryUrl($user->photo_path, now()->addHours(24)) : null, ]; } private function missingFields(User $user): array { $missing = array_values(array_filter( self::PROFILE_FIELDS, fn(string $field): bool => $this->isEmpty($user->{$field}), )); if (!$user->photo_path) { $missing[] = 'photo'; } return $missing; } private function isEmpty(mixed $value): bool { return $value === null || $value === ''; } private function issueToken(string $registration): string { return Crypt::encryptString(json_encode([ 'registration' => $registration, 'expires_at' => now()->addMinutes(self::TOKEN_TTL_MINUTES)->timestamp, ])); } /** * Garante que o formulário enviado corresponde ao crachá consultado no passo anterior. */ private function assertValidToken(?string $token, string $registration): void { $invalid = ValidationException::withMessages([ 'token' => __('messages.first_access.invalid_token'), ]); if (!$token) { throw $invalid; } try { $payload = json_decode(Crypt::decryptString($token), true); } catch (DecryptException) { throw $invalid; } if ( !is_array($payload) || ($payload['registration'] ?? null) !== $registration || ($payload['expires_at'] ?? 0) < now()->timestamp ) { throw $invalid; } } }