feat: add guest notification center

This commit is contained in:
Codex Agent
2025-11-12 16:56:50 +01:00
parent 062932ce38
commit 4495ac1895
27 changed files with 2042 additions and 64 deletions

View File

@@ -0,0 +1,131 @@
<?php
namespace App\Http\Controllers\Api\Tenant;
use App\Enums\GuestNotificationAudience;
use App\Enums\GuestNotificationType;
use App\Http\Controllers\Controller;
use App\Http\Requests\Tenant\BroadcastGuestNotificationRequest;
use App\Http\Resources\Tenant\GuestNotificationResource;
use App\Models\Event;
use App\Models\GuestNotification;
use App\Services\GuestNotificationService;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpFoundation\Response;
class EventGuestNotificationController extends Controller
{
public function __construct(private readonly GuestNotificationService $notifications) {}
public function index(Request $request, Event $event): JsonResponse
{
$this->assertEventTenant($request, $event);
$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);
$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']);
}
$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;
}
}