feat: add guest notification center
This commit is contained in:
162
app/Services/GuestNotificationService.php
Normal file
162
app/Services/GuestNotificationService.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Enums\GuestNotificationAudience;
|
||||
use App\Enums\GuestNotificationDeliveryStatus;
|
||||
use App\Enums\GuestNotificationState;
|
||||
use App\Enums\GuestNotificationType;
|
||||
use App\Events\GuestNotificationCreated;
|
||||
use App\Models\Event;
|
||||
use App\Models\GuestNotification;
|
||||
use App\Models\GuestNotificationReceipt;
|
||||
use Illuminate\Contracts\Events\Dispatcher;
|
||||
use Illuminate\Support\Arr;
|
||||
use Throwable;
|
||||
|
||||
class GuestNotificationService
|
||||
{
|
||||
public function __construct(private readonly Dispatcher $events) {}
|
||||
|
||||
/**
|
||||
* @param array{payload?: array|null, target_identifier?: string|null, priority?: int|null, expires_at?: \DateTimeInterface|null, status?: GuestNotificationState|null, audience_scope?: GuestNotificationAudience|string|null} $options
|
||||
*/
|
||||
public function createNotification(
|
||||
Event $event,
|
||||
GuestNotificationType $type,
|
||||
string $title,
|
||||
?string $body = null,
|
||||
array $options = []
|
||||
): GuestNotification {
|
||||
$audience = $this->normalizeAudience($options['audience_scope'] ?? null);
|
||||
$target = $audience === GuestNotificationAudience::GUEST
|
||||
? $this->sanitizeIdentifier($options['target_identifier'] ?? null)
|
||||
: null;
|
||||
|
||||
$notification = new GuestNotification([
|
||||
'tenant_id' => $event->tenant_id,
|
||||
'event_id' => $event->getKey(),
|
||||
'type' => $type,
|
||||
'title' => mb_substr(trim($title), 0, 160),
|
||||
'body' => $body ? mb_substr(trim($body), 0, 2000) : null,
|
||||
'payload' => $this->sanitizePayload($options['payload'] ?? null),
|
||||
'audience_scope' => $audience,
|
||||
'target_identifier' => $target,
|
||||
'status' => ($options['status'] ?? null) instanceof GuestNotificationState
|
||||
? $options['status']
|
||||
: GuestNotificationState::ACTIVE,
|
||||
'priority' => $this->normalizePriority($options['priority'] ?? null),
|
||||
'expires_at' => $options['expires_at'] ?? null,
|
||||
]);
|
||||
|
||||
$notification->save();
|
||||
|
||||
$this->events->dispatch(new GuestNotificationCreated($notification));
|
||||
|
||||
return $notification;
|
||||
}
|
||||
|
||||
public function markAsRead(GuestNotification $notification, string $guestIdentifier): GuestNotificationReceipt
|
||||
{
|
||||
return $this->storeReceipt($notification, $guestIdentifier, GuestNotificationDeliveryStatus::READ);
|
||||
}
|
||||
|
||||
public function dismiss(GuestNotification $notification, string $guestIdentifier): GuestNotificationReceipt
|
||||
{
|
||||
return $this->storeReceipt($notification, $guestIdentifier, GuestNotificationDeliveryStatus::DISMISSED);
|
||||
}
|
||||
|
||||
private function storeReceipt(GuestNotification $notification, string $guestIdentifier, GuestNotificationDeliveryStatus $status): GuestNotificationReceipt
|
||||
{
|
||||
$guestIdentifier = $this->sanitizeIdentifier($guestIdentifier) ?? 'anonymous';
|
||||
|
||||
/** @var GuestNotificationReceipt $receipt */
|
||||
$receipt = GuestNotificationReceipt::query()->updateOrCreate(
|
||||
[
|
||||
'guest_notification_id' => $notification->getKey(),
|
||||
'guest_identifier' => $guestIdentifier,
|
||||
],
|
||||
$this->buildReceiptAttributes($status)
|
||||
);
|
||||
|
||||
return $receipt;
|
||||
}
|
||||
|
||||
private function buildReceiptAttributes(GuestNotificationDeliveryStatus $status): array
|
||||
{
|
||||
$attributes = ['status' => $status];
|
||||
|
||||
if ($status === GuestNotificationDeliveryStatus::READ) {
|
||||
$attributes['read_at'] = now();
|
||||
$attributes['dismissed_at'] = null;
|
||||
} elseif ($status === GuestNotificationDeliveryStatus::DISMISSED) {
|
||||
$attributes['dismissed_at'] = now();
|
||||
$attributes['read_at'] = $attributes['read_at'] ?? now();
|
||||
}
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
private function sanitizePayload(?array $payload): ?array
|
||||
{
|
||||
if (! $payload) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$cta = Arr::get($payload, 'cta');
|
||||
if (is_array($cta)) {
|
||||
$cta = [
|
||||
'label' => mb_substr((string) ($cta['label'] ?? ''), 0, 80),
|
||||
'href' => mb_substr((string) ($cta['href'] ?? $cta['url'] ?? ''), 0, 2048),
|
||||
];
|
||||
|
||||
if (trim($cta['label']) === '' || trim($cta['href']) === '') {
|
||||
$cta = null;
|
||||
}
|
||||
} else {
|
||||
$cta = null;
|
||||
}
|
||||
|
||||
$clean = array_filter([
|
||||
'cta' => $cta,
|
||||
]);
|
||||
|
||||
return $clean === [] ? null : $clean;
|
||||
}
|
||||
|
||||
private function normalizePriority(mixed $value): int
|
||||
{
|
||||
if (! is_numeric($value)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
$int = (int) $value;
|
||||
|
||||
return max(0, min(5, $int));
|
||||
}
|
||||
|
||||
private function sanitizeIdentifier(?string $identifier): ?string
|
||||
{
|
||||
if ($identifier === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$sanitized = preg_replace('/[^A-Za-z0-9 _\-]/', '', $identifier) ?? '';
|
||||
$sanitized = trim(mb_substr($sanitized, 0, 120));
|
||||
|
||||
return $sanitized === '' ? null : $sanitized;
|
||||
}
|
||||
|
||||
private function normalizeAudience(mixed $audience): GuestNotificationAudience
|
||||
{
|
||||
if ($audience instanceof GuestNotificationAudience) {
|
||||
return $audience;
|
||||
}
|
||||
|
||||
try {
|
||||
return GuestNotificationAudience::from(is_string($audience) ? strtolower($audience) : 'all');
|
||||
} catch (Throwable) {
|
||||
return GuestNotificationAudience::ALL;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user