Files
fotospiel-app/app/Http/Controllers/Api/Tenant/EventGuestNotificationController.php
Codex Agent c180b37760
Some checks failed
linter / quality (push) Has been cancelled
tests / ci (push) Has been cancelled
tests / ui (push) Has been cancelled
Add guest policy settings
2026-01-01 20:25:39 +01:00

138 lines
4.4 KiB
PHP

<?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\Models\GuestPolicySetting;
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']);
} 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;
}
}