generateUniqueToken(); $payload = [ 'event_id' => $event->id, 'token' => $tokenValue, 'label' => Arr::get($attributes, 'label'), 'usage_limit' => Arr::get($attributes, 'usage_limit'), 'metadata' => Arr::get($attributes, 'metadata', []), ]; if ($expiresAt = Arr::get($attributes, 'expires_at')) { $payload['expires_at'] = $expiresAt instanceof Carbon ? $expiresAt : Carbon::parse($expiresAt); } else { $ttlHours = (int) (GuestPolicySetting::current()->join_token_ttl_hours ?? 0); if ($ttlHours > 0) { $payload['expires_at'] = now()->addHours($ttlHours); } } if ($createdBy = Arr::get($attributes, 'created_by')) { $payload['created_by'] = $createdBy; } return tap(EventJoinToken::create($payload), function (EventJoinToken $model) use ($tokenValue) { $model->setAttribute('plain_token', $tokenValue); }); }); } public function revoke(EventJoinToken $joinToken, ?string $reason = null): EventJoinToken { unset($joinToken->plain_token); $joinToken->revoked_at = now(); if ($reason) { $metadata = $joinToken->metadata ?? []; $metadata['revoked_reason'] = $reason; $joinToken->metadata = $metadata; } $joinToken->save(); return $joinToken; } public function incrementUsage(EventJoinToken $joinToken, ?string $deviceId = null, ?string $ipAddress = null): void { $joinToken->increment('usage_count'); $event = $joinToken->event() ->with(['eventPackage.package', 'eventPackages.package', 'tenant']) ->first(); if ($event && $event->tenant) { $usageTracker = app(\App\Services\Packages\PackageUsageTracker::class); $limitEvaluator = app(\App\Services\Packages\PackageLimitEvaluator::class); $eventPackage = $limitEvaluator->resolveEventPackageForPhotoUpload($event->tenant, $event->id, $event); if ($eventPackage && $eventPackage->package?->max_guests !== null) { $normalizedDeviceId = $this->normalizeDeviceId($deviceId); $normalizedIp = is_string($ipAddress) && trim($ipAddress) !== '' ? $ipAddress : null; if (! $this->shouldCountGuest($event->id, $normalizedDeviceId, $normalizedIp)) { return; } $previous = (int) $eventPackage->used_guests; $eventPackage->increment('used_guests'); $eventPackage->refresh(); $usageTracker->recordGuestUsage($eventPackage, $previous, 1); } } } public function hasSeenGuest(int $eventId, ?string $deviceId, ?string $ipAddress): bool { $normalizedDeviceId = $this->normalizeDeviceId($deviceId); $normalizedIp = is_string($ipAddress) && trim($ipAddress) !== '' ? $ipAddress : null; if (! $normalizedDeviceId && ! $normalizedIp) { return false; } $query = EventJoinTokenEvent::query() ->where('event_id', $eventId) ->where('event_type', 'access_granted'); if ($normalizedDeviceId) { $query->where('device_id', $normalizedDeviceId); } else { $query->where('ip_address', $normalizedIp); } return $query->exists(); } public function findToken(string $token, bool $includeInactive = false): ?EventJoinToken { $hash = $this->hashToken($token); return EventJoinToken::query() ->where(function ($query) use ($hash, $token) { $query->where('token_hash', $hash) ->orWhere(function ($inner) use ($token) { $inner->whereNull('token_hash') ->where('token', $token); }); }) ->when(! $includeInactive, function ($query) { $query->whereNull('revoked_at') ->where(function ($query) { $query->whereNull('expires_at') ->orWhere('expires_at', '>', now()); }) ->where(function ($query) { $query->whereNull('usage_limit') ->orWhereColumn('usage_limit', '>', 'usage_count'); }); }) ->first(); } public function findActiveToken(string $token): ?EventJoinToken { return $this->findToken($token); } protected function generateUniqueToken(int $length = 48): string { do { $token = Str::random($length); $hash = $this->hashToken($token); } while (EventJoinToken::where('token_hash', $hash)->exists()); return $token; } protected function hashToken(string $token): string { return hash('sha256', $token); } private function normalizeDeviceId(?string $deviceId): ?string { if (! is_string($deviceId) || trim($deviceId) === '') { return null; } $cleaned = preg_replace('/[^a-zA-Z0-9_-]/', '', $deviceId) ?? ''; $cleaned = substr($cleaned, 0, 64); return $cleaned !== '' ? $cleaned : null; } private function shouldCountGuest(int $eventId, ?string $deviceId, ?string $ipAddress): bool { if (! $deviceId && ! $ipAddress) { return false; } $query = EventJoinTokenEvent::query() ->where('event_id', $eventId) ->where('event_type', 'access_granted'); if ($deviceId) { $query->where('device_id', $deviceId); } else { $query->where('ip_address', $ipAddress); } return $query->count() <= 1; } }