assertEventTenant($request, $event); TenantMemberPermissions::ensureEventPermission($request, $event, 'guest-notifications:manage'); $limit = max(1, min(100, (int) $request->integer('limit', 25))); $notifications = GuestNotification::query() ->forEvent($event) ->orderByDesc('id') ->limit($limit) ->get(); return GuestNotificationResource::collection($notifications)->response(); } public function store(BroadcastGuestNotificationRequest $request, Event $event): JsonResponse { $this->assertEventTenant($request, $event); TenantMemberPermissions::ensureEventPermission($request, $event, 'guest-notifications:manage'); $data = $request->validated(); $type = $this->resolveType($data['type'] ?? null); $audience = $this->resolveAudience($data['audience'] ?? null); $targetIdentifier = $audience === GuestNotificationAudience::GUEST ? $this->sanitizeIdentifier($data['guest_identifier'] ?? '') : null; if ($audience === GuestNotificationAudience::GUEST && ! $targetIdentifier) { throw ValidationException::withMessages([ 'guest_identifier' => __('Ein Gastname oder Geräte-Token wird benötigt.'), ]); } $expiresAt = null; if (! empty($data['expires_in_minutes'])) { $expiresAt = now()->addMinutes((int) $data['expires_in_minutes']); } else { $policyTtl = GuestPolicySetting::current()->guest_notification_ttl_hours; if ($policyTtl !== null && $policyTtl > 0) { $expiresAt = now()->addHours((int) $policyTtl); } } $payload = null; if (! empty($data['cta'])) { $payload = [ 'cta' => [ 'label' => $data['cta']['label'], 'href' => $data['cta']['url'], ], ]; } $notification = $this->notifications->createNotification( $event, $type, $data['title'], $data['message'], [ 'payload' => $payload, 'audience_scope' => $audience, 'target_identifier' => $targetIdentifier, 'expires_at' => $expiresAt, 'priority' => $data['priority'] ?? 1, ] ); return (new GuestNotificationResource($notification)) ->response() ->setStatusCode(Response::HTTP_CREATED); } private function assertEventTenant(Request $request, Event $event): void { $tenantId = $request->attributes->get('tenant_id'); if ($tenantId === null || (int) $event->tenant_id !== (int) $tenantId) { abort(403, 'Event belongs to a different tenant.'); } } private function resolveType(?string $value): GuestNotificationType { if (! $value) { return GuestNotificationType::BROADCAST; } foreach (GuestNotificationType::cases() as $type) { if ($type->value === $value) { return $type; } } return GuestNotificationType::BROADCAST; } private function resolveAudience(?string $value): GuestNotificationAudience { if (! $value) { return GuestNotificationAudience::ALL; } return $value === GuestNotificationAudience::GUEST->value ? GuestNotificationAudience::GUEST : GuestNotificationAudience::ALL; } private function sanitizeIdentifier(string $value): ?string { $normalized = preg_replace('/[^A-Za-z0-9 _\-]/', '', $value) ?? ''; $normalized = trim(mb_substr($normalized, 0, 120)); return $normalized === '' ? null : $normalized; } }