207 lines
6.6 KiB
PHP
207 lines
6.6 KiB
PHP
<?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 Illuminate\Support\Facades\Log;
|
|
use Illuminate\Support\Facades\Schema;
|
|
use Throwable;
|
|
|
|
class GuestNotificationService
|
|
{
|
|
private bool $notificationsStorageAvailable;
|
|
|
|
public function __construct(private readonly Dispatcher $events)
|
|
{
|
|
$this->notificationsStorageAvailable = $this->detectStorageAvailability();
|
|
}
|
|
|
|
/**
|
|
* @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,
|
|
]);
|
|
|
|
if (! $this->notificationsStorageAvailable) {
|
|
$this->logStorageWarningOnce();
|
|
|
|
return $notification;
|
|
}
|
|
|
|
$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 */
|
|
if (! $this->notificationsStorageAvailable) {
|
|
$this->logStorageWarningOnce();
|
|
|
|
return new GuestNotificationReceipt([
|
|
'guest_notification_id' => $notification->getKey(),
|
|
'guest_identifier' => $guestIdentifier,
|
|
...$this->buildReceiptAttributes($status),
|
|
]);
|
|
}
|
|
|
|
$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;
|
|
}
|
|
}
|
|
|
|
private function detectStorageAvailability(): bool
|
|
{
|
|
try {
|
|
return Schema::hasTable('guest_notifications') && Schema::hasTable('guest_notification_receipts');
|
|
} catch (Throwable) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private function logStorageWarningOnce(): void
|
|
{
|
|
static $alreadyWarned = false;
|
|
|
|
if ($alreadyWarned) {
|
|
return;
|
|
}
|
|
|
|
$alreadyWarned = true;
|
|
Log::warning('Guest notifications storage tables are missing. Notification persistence skipped.');
|
|
}
|
|
}
|